summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVidhu Kant Sharma <vidhukant@vidhukant.xyz>2022-11-16 20:49:10 +0530
committerVidhu Kant Sharma <vidhukant@vidhukant.xyz>2022-11-16 20:49:10 +0530
commit6bc184bacb12b6ede89b8780138bacee5312950e (patch)
tree3213a9bb4f22a24d63373e66c9d7dbfe4ceb9fe4
parent0a885e5b7d76562a9d7c9c8f3353a53dd247eced (diff)
added endpoint to validate and create new user
-rw-r--r--auth/auth.go60
-rw-r--r--go.mod2
-rw-r--r--main.go6
-rw-r--r--user/db_actions.go45
-rw-r--r--user/router.go96
-rw-r--r--user/user.go52
-rw-r--r--user/validate.go153
7 files changed, 412 insertions, 2 deletions
diff --git a/auth/auth.go b/auth/auth.go
new file mode 100644
index 0000000..ae20d23
--- /dev/null
+++ b/auth/auth.go
@@ -0,0 +1,60 @@
+/* OpenBills-server - Server for libre billing software OpenBills-web
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package auth
+
+import (
+ "github.com/gin-gonic/gin"
+ "context"
+ "fmt"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "github.com/MikunoNaka/OpenBills-server/database"
+ "github.com/MikunoNaka/OpenBills-server/user"
+ "net/http"
+ //"golang.org/x/crypto/bcrypt"
+)
+
+var db *mongo.Collection = database.DB.Collection("Users")
+
+func checkPassword() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ var u user.User
+ ctx.BindJSON(&u)
+
+ filter := bson.M{
+ "UserName": u.UserName,
+ "$or": bson.M{"Email": u.Email},
+ }
+
+ err := db.FindOne(context.TODO(), filter).Decode(&u)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(u)
+ }
+}
+
+func Routes(route *gin.Engine) {
+ u := route.Group("/auth")
+ {
+ u.POST("/login", func(ctx *gin.Context) {
+ checkPassword()(ctx)
+ ctx.HTML(http.StatusOK, "<h1>Hello World</h1>", nil)
+ })
+ }
+}
diff --git a/go.mod b/go.mod
index 7573e09..fe7e664 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.19
require (
github.com/gin-gonic/gin v1.8.1
go.mongodb.org/mongo-driver v1.10.2
+ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
)
require (
@@ -28,7 +29,6 @@ require (
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
- golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
diff --git a/main.go b/main.go
index 29378cb..d70ab89 100644
--- a/main.go
+++ b/main.go
@@ -22,7 +22,9 @@ import (
"github.com/MikunoNaka/OpenBills-server/item"
"github.com/MikunoNaka/OpenBills-server/client"
"github.com/MikunoNaka/OpenBills-server/invoice"
+ "github.com/MikunoNaka/OpenBills-server/user"
"github.com/MikunoNaka/OpenBills-server/database"
+ "github.com/MikunoNaka/OpenBills-server/auth"
"github.com/gin-gonic/gin"
)
@@ -31,12 +33,14 @@ func main() {
defer database.DisconnectDB()
r := gin.New()
- r.LoadHTMLGlob("web/templates/**/*")
+ r.LoadHTMLGlob("web/templates/**/*")
item.Routes(r)
brand.Routes(r)
client.Routes(r)
invoice.Routes(r)
+ user.Routes(r)
+ auth.Routes(r)
r.Run(":6969")
}
diff --git a/user/db_actions.go b/user/db_actions.go
new file mode 100644
index 0000000..611507d
--- /dev/null
+++ b/user/db_actions.go
@@ -0,0 +1,45 @@
+/* OpenBills-server - Server for libre billing software OpenBills-web
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package user
+
+import (
+ "context"
+ "fmt"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// Add user to db
+func saveUser(u User) (primitive.ObjectID, error) {
+ u.hashPassword()
+ res, err := db.InsertOne(context.TODO(), u)
+ return res.InsertedID.(primitive.ObjectID), err
+}
+
+// Delete user from DB
+func deleteUser(id primitive.ObjectID) error {
+ _, err := db.DeleteOne(context.TODO(), bson.M{"_id": id})
+ return err
+}
+
+// modify user in DB
+func modifyUser(id primitive.ObjectID, nu User) error {
+ fmt.Println(nu.Password)
+ _, err := db.UpdateOne(context.TODO(), bson.D{{"_id", id}}, bson.D{{"$set", nu}})
+ return err
+}
diff --git a/user/router.go b/user/router.go
new file mode 100644
index 0000000..ab1dff8
--- /dev/null
+++ b/user/router.go
@@ -0,0 +1,96 @@
+/* OpenBills-server - Server for libre billing software OpenBills-web
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package user
+
+import (
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "golang.org/x/crypto/bcrypt"
+ "log"
+ "net/http"
+)
+
+
+func Routes(route *gin.Engine) {
+ u := route.Group("/user")
+ {
+ u.POST("/new", validateMiddleware(), func(ctx *gin.Context) {
+ u := ctx.MustGet("user").(User)
+ // hash password
+ pass := []byte(u.Password)
+ hash, err := bcrypt.GenerateFromPassword(pass, bcrypt.DefaultCost)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{"error": "could not login"})
+ log.Printf("ERROR: Failed to hash password: %v\n", err.Error())
+ }
+ u.Password = string(hash)
+
+ _, err = saveUser(u)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{"error": "could not login"})
+ log.Printf("ERROR: Failed to add new user %v to DB: %v\n", u, err.Error())
+ return
+ }
+
+ log.Printf("Successfully saved new user to DB: %s", u.UserName)
+ ctx.JSON(http.StatusOK, nil)
+ })
+
+ u.PUT("/:userId", func(ctx *gin.Context) {
+ id := ctx.Param("userId")
+ objectId, err := primitive.ObjectIDFromHex(id)
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ log.Printf("ERROR: Failed to modify user, Error parsing ID: %v\n", err.Error())
+ return
+ }
+
+ var u User
+ ctx.BindJSON(&u)
+ err = modifyUser(objectId, u)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ log.Printf("ERROR: Failed to modify user %v: %v\n", objectId, err.Error())
+ return
+ }
+
+ log.Printf("Modified user %v to %v.\n", objectId, u)
+ ctx.JSON(http.StatusOK, nil)
+ })
+
+ u.DELETE("/:userId", func(ctx *gin.Context) {
+ id := ctx.Param("userId")
+ objectId, err := primitive.ObjectIDFromHex(id)
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ log.Printf("ERROR: Failed to delete user, Error parsing ID: %v\n", err.Error())
+ return
+ }
+
+ err = deleteUser(objectId)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ log.Printf("ERROR: Failed to delete user %v: %v\n", objectId, err.Error())
+ return
+ }
+
+ log.Printf("Deleted user %v from database.\n", objectId )
+ ctx.JSON(http.StatusOK, nil)
+ })
+ }
+}
diff --git a/user/user.go b/user/user.go
new file mode 100644
index 0000000..c6c3d53
--- /dev/null
+++ b/user/user.go
@@ -0,0 +1,52 @@
+/* OpenBills-server - Server for libre billing software OpenBills-web
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package user
+
+import (
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "github.com/MikunoNaka/OpenBills-server/database"
+ "golang.org/x/crypto/bcrypt"
+)
+
+var db *mongo.Collection = database.DB.Collection("Users")
+
+// per-user config can be shared to DB
+type Config struct {
+}
+
+type User struct {
+ Id primitive.ObjectID `bson:"_id,omitempty" json:"Id"`
+ UserName string `bson:"UserName" json:"UserName"`
+ Email string `bson:"Email" json:"Email"`
+ Password string `bson:"Password" json:"Password"`
+ Config Config `bson:"Config" json:"Config"`
+ // some actions are only available when email is verified
+ Verified bool `bson:"Verified" json:"Verified"`
+}
+
+func (u *User) hashPassword() error {
+ // TODO: password validation
+ hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+
+ u.Password = string(hash)
+ return nil
+}
diff --git a/user/validate.go b/user/validate.go
new file mode 100644
index 0000000..b51757f
--- /dev/null
+++ b/user/validate.go
@@ -0,0 +1,153 @@
+/* OpenBills-server - Server for libre billing software OpenBills-web
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package user
+
+import (
+ "context"
+ "strings"
+ "net/mail"
+ "net/http"
+ "log"
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/bson"
+ "errors"
+)
+
+var (
+ errUsernameTaken = errors.New("username is taken")
+ errUsernameTooShort = errors.New("username is too short")
+ errUsernameInvalid = errors.New("invalid username")
+ errEmailTaken = errors.New("email is taken")
+ errEmailInvalid = errors.New("email is invalid")
+ errPasswordTooShort = errors.New("password is too short")
+)
+
+func isUsernameTaken(username string) error {
+ var x User
+ err := db.FindOne(context.TODO(), bson.M{"UserName": username}).Decode(&x)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil
+ } else {
+ return err
+ }
+ } else {
+ return errUsernameTaken
+ }
+ return nil
+}
+
+func isEmailTaken(email string) error {
+ var x User
+ err := db.FindOne(context.TODO(), bson.M{"Email": email}).Decode(&x)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil
+ } else {
+ return err
+ }
+ } else {
+ return errEmailTaken
+ }
+ return nil
+}
+
+func validateUsername(username string) error {
+ username = strings.Trim(username, " ")
+
+ if len(username) < 2 {
+ return errUsernameTooShort
+ }
+
+ if strings.Contains(username, " ") {
+ return errUsernameInvalid
+ }
+
+ return isUsernameTaken(username)
+}
+
+func validateEmail(email string) error {
+ email = strings.Trim(email, " ")
+
+ // verify if string is valid email
+ _, err := mail.ParseAddress(email)
+ if err != nil {
+ return errEmailInvalid
+ }
+
+ return isEmailTaken(email)
+}
+
+func validatePassword(password string) error {
+ // TODO: load password length from config
+ if len(password) < 12 {
+ return errPasswordTooShort
+ }
+
+ return nil
+}
+
+func validateMiddleware() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ var u User
+ ctx.BindJSON(&u)
+
+ // validate username
+ isUsernameValid := validateUsername(u.UserName)
+ switch isUsernameValid {
+ case nil:
+ break;
+ case errUsernameTooShort, errUsernameInvalid:
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": isUsernameValid.Error()})
+ case errUsernameTaken:
+ ctx.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": isUsernameValid.Error()})
+ default:
+ log.Printf("Error while creating new user '%s': %s", u.UserName, isUsernameValid.Error())
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error (cannot create user)"})
+ }
+
+ // validate email
+ isEmailValid := validateEmail(u.Email)
+ switch isEmailValid {
+ case nil:
+ break;
+ case errEmailInvalid:
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": isEmailValid.Error()})
+ case errEmailTaken:
+ ctx.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": isEmailValid.Error()})
+ default:
+ log.Printf("Error while creating new user '%s': %s", u.UserName, isEmailValid.Error())
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error (cannot create user)"})
+ }
+
+ // validate password
+ isPasswordValid := validatePassword(u.Password)
+ switch isPasswordValid {
+ case nil:
+ break;
+ case errPasswordTooShort:
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": isPasswordValid.Error()})
+ default:
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error (cannot create user)"})
+ log.Printf("Error while creating new user '%s': %s", u.UserName, isPasswordValid.Error())
+ }
+
+ ctx.Set("user", u)
+ }
+}