diff options
Diffstat (limited to 'user')
| -rw-r--r-- | user/controller.go | 105 | ||||
| -rw-r--r-- | user/password.go | 70 | ||||
| -rw-r--r-- | user/refresh.go | 117 | ||||
| -rw-r--r-- | user/router.go | 87 | ||||
| -rw-r--r-- | user/service.go (renamed from user/db_actions.go) | 0 | 
5 files changed, 296 insertions, 83 deletions
diff --git a/user/controller.go b/user/controller.go new file mode 100644 index 0000000..df13a06 --- /dev/null +++ b/user/controller.go @@ -0,0 +1,105 @@ +/* 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 ( +	"errors" +	"github.com/gin-gonic/gin" +	"go.mongodb.org/mongo-driver/bson/primitive" +	"go.mongodb.org/mongo-driver/mongo" +	"log" +	"net/http" +) + +func getSelf(ctx *gin.Context) { +	hex := ctx.MustGet("userId").(string) +	id, err := primitive.ObjectIDFromHex(hex) +	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 +	} + +	user, err := getUser(id) +	if err != nil { +		log.Printf("ERROR: Failed to read user %d info from DB: %v\n", id, err.Error()) +		if errors.Is(err, mongo.ErrNoDocuments) { +			ctx.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) +		} else { +			ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) +		} +	} + +	ctx.JSON(http.StatusOK, user) +} + +func save(ctx *gin.Context) { +	u := ctx.MustGet("user").(User) +	// TODO: maybe add an invite code for some instances + +	_, err := saveUser(u) +	if err != nil { +		log.Printf("ERROR: Failed to add new user %v to DB: %v\n", u, err.Error()) +		ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "could not login"}) +	} + +	log.Printf("Successfully saved new user to DB: %s", u.UserName) +	ctx.JSON(http.StatusOK, nil) +} + +func modify(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) +} + +func remove(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/password.go b/user/password.go new file mode 100644 index 0000000..d667ebc --- /dev/null +++ b/user/password.go @@ -0,0 +1,70 @@ +/* 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" +	"go.mongodb.org/mongo-driver/mongo" +	"golang.org/x/crypto/bcrypt" +	"log" +	"net/http" +) + +func checkPassword() gin.HandlerFunc { +	return func(ctx *gin.Context) { +		var u User +		ctx.BindJSON(&u) + +		filter := bson.M{ +			"$or": []bson.M{ +				// u.UserName in this case can be either username or email +				{"Email": u.UserName}, +				{"UserName": u.UserName}, +			}, +		} + +		// check if the user exists in DB +		var user User +		err := db.FindOne(ctx, filter).Decode(&user) +		if err != nil { +			if err == mongo.ErrNoDocuments { +				ctx.JSON(http.StatusNotFound, gin.H{"error": "user does not exist"}) +			} else { +				log.Printf("Error while reading user from DB to check password: %v", err.Error()) +				ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) +			} +			ctx.Abort() +		} else { +			// compare hash and password +			err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password)) +			if err != nil { +				if err == bcrypt.ErrMismatchedHashAndPassword { +					ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "incorrect password"}) +				} else { +					log.Printf("Error while checking password: %v", err.Error()) +					ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) +				} +			} +		} + +		// everything's fine! +		ctx.Set("user", user) +		ctx.Next() +	} +} diff --git a/user/refresh.go b/user/refresh.go new file mode 100644 index 0000000..72a7655 --- /dev/null +++ b/user/refresh.go @@ -0,0 +1,117 @@ +/* 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" +	"errors" +	"fmt" +	"github.com/MikunoNaka/OpenBills-server/util" +	"github.com/gin-gonic/gin" +	"github.com/golang-jwt/jwt/v4" +	"go.mongodb.org/mongo-driver/bson" +	"go.mongodb.org/mongo-driver/bson/primitive" +	"net/http" +	"time" +) + +var ( +	errUserNotFound error = errors.New("user does not exist") +	refreshSecret   []byte +) + +func init() { +	conf := util.GetConfig().Crypto +	refreshSecret = []byte(conf.RefreshTokenSecret) +} + +// middleware to check refresh token +func verifyRefreshToken() gin.HandlerFunc { +	return func(ctx *gin.Context) { +		refreshToken, err := ctx.Cookie("refreshToken") +		fmt.Println(refreshToken) +		if err == nil { +			token, err := jwt.ParseWithClaims(refreshToken, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { +				return []byte(refreshSecret), nil +			}) +			if err != nil { // invalid token +				ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "invalid token"}) +			} else { // valid token +				// convert id from string to ObjectID +				id, _ := primitive.ObjectIDFromHex(token.Claims.(*jwt.StandardClaims).Issuer) + +				// check if user exists +				var u User +				if err := db.FindOne(context.TODO(), bson.M{"_id": id}).Decode(&u); err != nil { +					ctx.AbortWithStatusJSON(http.StatusNotFound, gin.H{"message": "user not found"}) +				} else { +					// check if this refreshToken is in DB +					for _, i := range u.Sessions { +						if i.Token == refreshToken { +							ctx.Set("user", u) +							ctx.Next() +						} +					} +					ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "refresh token expired"}) +				} +			} +		} else { +			// invalid Authorization header +			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "not logged in"}) +		} +	} +} + +/* + * the refresh token has a long lifespan and is stored in + * the database in case it needs to be revoked. + * + * this can be stored as an HTTP only cookie and will be used + * when creating a new access token + * + * I'm using a different secret key for refresh tokens + * for enhanced security + */ +func newRefreshToken(userId string) (string, int64, error) { +	// convert id from string to ObjectID +	id, _ := primitive.ObjectIDFromHex(userId) + +	// check if user exists +	var u User +	if err := db.FindOne(context.TODO(), bson.M{"_id": id}).Decode(&u); err != nil { +		return "", 0, errUserNotFound +	} + +	// generate refresh token +	expiresAt := time.Now().Add(time.Hour * 12).Unix() +	claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ +		Issuer:    userId, +		ExpiresAt: expiresAt, +	}) +	token, err := claims.SignedString(refreshSecret) +	if err != nil { +		return "", expiresAt, err +	} + +	// store refresh token in db with unique session name for ease in identification +	sessionName := time.Now().Format("01-02-2006.15:04:05") + "-" + u.UserName +	u.Sessions = append(u.Sessions, Session{Name: sessionName, Token: token}) +	db.UpdateOne(context.TODO(), bson.M{"_id": id}, bson.D{{"$set", u}}) + +	return token, expiresAt, nil +} diff --git a/user/router.go b/user/router.go index 6e84185..ad9b4df 100644 --- a/user/router.go +++ b/user/router.go @@ -19,94 +19,15 @@ package user  import (  	"github.com/MikunoNaka/OpenBills-server/util" -	"errors"  	"github.com/gin-gonic/gin" -	"go.mongodb.org/mongo-driver/bson/primitive" -	"go.mongodb.org/mongo-driver/mongo" -	"log" -	"net/http"  ) -  func Routes(route *gin.Engine) {  	u := route.Group("/user")  	{ -		u.GET("/", util.Authorize(), func(ctx *gin.Context) { -			hex := ctx.MustGet("userId").(string) -			id, err := primitive.ObjectIDFromHex(hex) -      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 -      } - -			user, err := getUser(id) -			if err != nil { -				log.Printf("ERROR: Failed to read user %d info from DB: %v\n", id, err.Error()) -				if errors.Is(err, mongo.ErrNoDocuments) { -					ctx.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) -				} else { -					ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) -				} -			} - -			ctx.JSON(http.StatusOK, user) -		}) - -		u.POST("/new", validateMiddleware(), func(ctx *gin.Context) { -			u := ctx.MustGet("user").(User) -			// TODO: maybe add an invite code for some instances - -			_, err := saveUser(u) -			if err != nil { -				log.Printf("ERROR: Failed to add new user %v to DB: %v\n", u, err.Error()) -				ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "could not login"}) -			} - -			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) -		}) +		u.GET("/", util.Authorize(), getSelf) +		u.POST("/new", validateMiddleware(), save) +		u.PUT("/:userId", checkPassword(), modify) +		u.DELETE("/:userId", checkPassword(), remove)  	}  } diff --git a/user/db_actions.go b/user/service.go index 51490e7..51490e7 100644 --- a/user/db_actions.go +++ b/user/service.go  |