From ea46225d55148cdba1874f45d9b49a55145e3947 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Wed, 2 Oct 2024 01:30:47 +0530 Subject: [PATCH 01/11] chore: fix jwt generation and verification to include userid --- controllers/url.go | 115 +++++++++++++++++++++++++++++++++----------- middlewares/auth.go | 8 ++- routes/url.go | 4 +- utils/jwt.go | 54 ++++++++------------- 4 files changed, 116 insertions(+), 65 deletions(-) diff --git a/controllers/url.go b/controllers/url.go index 0dd0962..bceccff 100644 --- a/controllers/url.go +++ b/controllers/url.go @@ -1,6 +1,8 @@ package controller import ( + "fmt" + "math" "net/http" "strconv" "strings" @@ -31,10 +33,27 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { return } + userIDFloat, exists := ctx.Get("userID") + if !exists { + ctx.JSON(http.StatusUnauthorized, dtos.URLCreationResponse{ + Message: "User not authenticated", + }) + return + } + + userIDFloat64, ok := userIDFloat.(float64) + if !ok { + ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ + Message: "Invalid user ID type", + }) + return + } + userID := int64(math.Round(userIDFloat64)) + var existingOriginalURL models.Tinyurl if err := db.NewSelect().Model(&existingOriginalURL). Where("original_url = ?", body.OriginalUrl). - Where("user_id = ?", body.UserID). + Where("user_id = ?", userID). Where("is_deleted = ?", false). Scan(ctx); err == nil { ctx.JSON(http.StatusOK, dtos.URLCreationResponse{ @@ -76,7 +95,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { count, err := db.NewSelect(). Model(&models.Tinyurl{}). - Where("user_id = ?", body.UserID). + Where("user_id = ?", userID). Where("is_deleted = ?", false). Count(ctx) @@ -94,16 +113,21 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { return } - body.CreatedAt = time.Now().UTC() + newTinyURL := models.Tinyurl{ + OriginalUrl: body.OriginalUrl, + ShortUrl: body.ShortUrl, + UserID: userID, + CreatedAt: time.Now().UTC(), + } - if _, err := db.NewInsert().Model(&body).Exec(ctx); err != nil { + if _, err := db.NewInsert().Model(&newTinyURL).Exec(ctx); err != nil { ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ Message: "Failed to create tiny URL", }) return } - if err := utils.IncrementURLCount(body.UserID, db, ctx); err != nil { + if err := utils.IncrementURLCount(userID, db, ctx); err != nil { ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ Message: "Failed to increment URL count: " + err.Error(), }) @@ -112,7 +136,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { updatedCount, err := db.NewSelect(). Model(&models.Tinyurl{}). - Where("user_id = ?", body.UserID). + Where("user_id = ?", userID). Where("is_deleted = ?", false). Count(ctx) @@ -124,9 +148,9 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { } ctx.JSON(http.StatusOK, dtos.URLCreationResponse{ - Message: "Tiny URL created successfully", - ShortURL: body.ShortUrl, - URLCount: updatedCount, + Message: "Tiny URL created successfully", + ShortURL: newTinyURL.ShortUrl, + URLCount: updatedCount, }) } @@ -216,42 +240,76 @@ func GetAllURLs(ctx *gin.Context, db *bun.DB) { func DeleteURL(ctx *gin.Context, db *bun.DB) { id, _ := ctx.Params.Get("id") + + urlId, err := strconv.Atoi(id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid URL ID", + }) + return + } - var body struct { - UserID int64 `json:"user_id"` + userId, exists := ctx.Get("userId") + if !exists { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "message": "Unauthorized", + }) + return } - if err := ctx.BindJSON(&body); err != nil { - ctx.JSON(http.StatusBadRequest, dtos.UserURLsResponse{ - Message: "Invalid Request.", + + userIdInt, ok := userId.(int64) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "message": "User ID type assertion failed", }) return } - _, err := db.NewUpdate().Model(&models.Tinyurl{}).Set("is_deleted=?", true).Set("deleted_at=?", time.Now().UTC()).Where("id = ?", id).Exec(ctx) + var url models.Tinyurl + err = db.NewSelect(). + Model(&url). + Where("id = ?", urlId). + Where("user_id = ?", userIdInt). + Where("is_deleted = ?", false). + Scan(ctx) if err != nil { - ctx.JSON(http.StatusNotFound, dtos.UserURLsResponse{ - Message: "No URLs found", + fmt.Println("Error during URL query:", err) + ctx.JSON(http.StatusNotFound, gin.H{ + "message": "URL not found or already deleted", }) return } - - if err := utils.DecrementURLCount(body.UserID, db, ctx); err != nil { - ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ - Message: "Failed to decrement URL count: " + err.Error(), + _, err = db.NewUpdate(). + Model(&url). + Set("is_deleted = ?", true). + Set("deleted_at = ?", time.Now().UTC()). + Where("id = ?", urlId). + Where("user_id = ?", userIdInt). + Exec(ctx) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "message": "Failed to delete URL", }) return } - updatedCount, err := db.NewSelect(). - Model(&models.Tinyurl{}). - Where("user_id = ?", body.UserID). - Where("is_deleted = ?", false). - Count(ctx) + err = utils.DecrementURLCount(userIdInt, db, ctx) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "message": "Failed to decrement URL count: " + err.Error(), + }) + return + } + updatedCount, err := db.NewSelect(). + Model(&models.Tinyurl{}). + Where("user_id = ?", userIdInt). + Where("is_deleted = ?", false). + Count(ctx) if err != nil { - ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ - Message: "Failed to fetch updated URL count", + ctx.JSON(http.StatusInternalServerError, gin.H{ + "message": "Failed to fetch updated URL count", }) return } @@ -262,6 +320,7 @@ func DeleteURL(ctx *gin.Context, db *bun.DB) { }) } + func GetURLDetails(ctx *gin.Context, db *bun.DB) { shortURL := ctx.Param("shortURL") var tinyURL models.Tinyurl diff --git a/middlewares/auth.go b/middlewares/auth.go index e632476..c40d799 100644 --- a/middlewares/auth.go +++ b/middlewares/auth.go @@ -18,7 +18,7 @@ func AuthMiddleware() gin.HandlerFunc { token := tokenCookie.Value - email, err := utils.VerifyToken(token) + claims, err := utils.VerifyToken(token) if err != nil { ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"}) @@ -26,7 +26,11 @@ func AuthMiddleware() gin.HandlerFunc { return } - ctx.Set("user", email) + ctx.Set("user", claims["email"]) + ctx.Set("userID", claims["userID"]) ctx.Next() } } + + + diff --git a/routes/url.go b/routes/url.go index 91be58c..aaeb872 100644 --- a/routes/url.go +++ b/routes/url.go @@ -29,7 +29,7 @@ func TinyURLRoutes(rg *gin.RouterGroup, db *bun.DB) { redirect.GET("/:shortURL", func(ctx *gin.Context) { controller.RedirectShortURL(ctx, db) }) - urls.DELETE("/:id", func(ctx *gin.Context) { + urls.DELETE("/:id", middleware.AuthMiddleware(), func(ctx *gin.Context) { controller.DeleteURL(ctx, db) - }) + }) } diff --git a/utils/jwt.go b/utils/jwt.go index 46cd732..057f6a9 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -15,52 +15,40 @@ import ( func GenerateToken(user *models.User) (string, error) { issuer := config.JwtIssuer key := []byte(config.JwtSecret) - tokenValidityInHours := config.JwtValidity - - tokenExpiryTime := time.Now().Add(time.Duration(tokenValidityInHours) * time.Hour).UTC().Format(time.RFC3339) - - t := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{ - "iss": issuer, - "exp": tokenExpiryTime, - "email": user.Email, - }) - - token, error := t.SignedString(key) - - return token, error + tokenExpiryTime := time.Now().Add(time.Duration(tokenValidityInHours) * time.Hour).UTC().Unix() + claims := jwt.MapClaims{ + "iss": issuer, + "exp": tokenExpiryTime, + "email": user.Email, + "userID": user.ID, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) + tokenString, err := token.SignedString(key) + return tokenString, err } /* * VerifyToken verifies the token and returns the email of the user */ -func VerifyToken(tokenString string) (string, error) { - var claims jwt.MapClaims = nil - +func VerifyToken(tokenString string) (jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - - if token.Method.Alg() != jwt.SigningMethodHS512.Alg() { - return nil, jwt.ErrSignatureInvalid + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") } - return []byte(config.JwtSecret), nil }) - if c, ok := token.Claims.(jwt.MapClaims); !ok && !token.Valid { - return "", err - } else { - claims = c - } - - expiryTime, err := time.Parse(time.RFC3339, claims["exp"].(string)) - if err != nil { - return "", err + return nil, err } - if time.Now().UTC().After(expiryTime) { - return "", errors.New("token has expired") + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if float64(time.Now().Unix()) > claims["exp"].(float64) { + return nil, errors.New("token has expired") + } + return claims, nil } - return claims["email"].(string), nil -} + return nil, errors.New("invalid token") +} \ No newline at end of file From 77b7eb947fae111b1cd6dda4149f09cdd6142183 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Wed, 2 Oct 2024 22:49:55 +0530 Subject: [PATCH 02/11] chore: add issued at in jwt --- utils/jwt.go | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/jwt.go b/utils/jwt.go index 057f6a9..ca284aa 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -22,6 +22,7 @@ func GenerateToken(user *models.User) (string, error) { "exp": tokenExpiryTime, "email": user.Email, "userID": user.ID, + "iat": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) tokenString, err := token.SignedString(key) From b138a9ac3b46645a4130ebf32738dd62f6ff0191 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Thu, 3 Oct 2024 01:30:56 +0530 Subject: [PATCH 03/11] chore: fix userid from context --- controllers/url.go | 16 +++------------- middlewares/auth.go | 16 +++++++++++----- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/controllers/url.go b/controllers/url.go index bceccff..487bd67 100644 --- a/controllers/url.go +++ b/controllers/url.go @@ -2,7 +2,6 @@ package controller import ( "fmt" - "math" "net/http" "strconv" "strings" @@ -33,7 +32,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { return } - userIDFloat, exists := ctx.Get("userID") + userID , exists := ctx.Get("userID") if !exists { ctx.JSON(http.StatusUnauthorized, dtos.URLCreationResponse{ Message: "User not authenticated", @@ -41,15 +40,6 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { return } - userIDFloat64, ok := userIDFloat.(float64) - if !ok { - ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ - Message: "Invalid user ID type", - }) - return - } - userID := int64(math.Round(userIDFloat64)) - var existingOriginalURL models.Tinyurl if err := db.NewSelect().Model(&existingOriginalURL). Where("original_url = ?", body.OriginalUrl). @@ -116,7 +106,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { newTinyURL := models.Tinyurl{ OriginalUrl: body.OriginalUrl, ShortUrl: body.ShortUrl, - UserID: userID, + UserID: userID.(int64), CreatedAt: time.Now().UTC(), } @@ -127,7 +117,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { return } - if err := utils.IncrementURLCount(userID, db, ctx); err != nil { + if err := utils.IncrementURLCount(userID.(int64), db, ctx); err != nil { ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ Message: "Failed to increment URL count: " + err.Error(), }) diff --git a/middlewares/auth.go b/middlewares/auth.go index c40d799..9d72654 100644 --- a/middlewares/auth.go +++ b/middlewares/auth.go @@ -2,6 +2,7 @@ package middleware import ( "net/http" + "strconv" "github.com/Real-Dev-Squad/tiny-site-backend/utils" "github.com/gin-gonic/gin" @@ -26,11 +27,16 @@ func AuthMiddleware() gin.HandlerFunc { return } + userIDStr := claims["userID"].(string) + userID, err := strconv.ParseInt(userIDStr, 10, 64) + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid UserID"}) + ctx.Abort() + return + } + ctx.Set("user", claims["email"]) - ctx.Set("userID", claims["userID"]) + ctx.Set("userID", userID) ctx.Next() } -} - - - +} \ No newline at end of file From beb0d48889030305d340937ddaaf4dc4dc31201a Mon Sep 17 00:00:00 2001 From: vinit717 Date: Fri, 4 Oct 2024 17:54:48 +0530 Subject: [PATCH 04/11] chore: fix jwt and auth middleware --- controllers/url.go | 30 +++++++++++----------------- middlewares/auth.go | 17 ++++++++-------- utils/jwt.go | 48 ++++++++++++++++++++++++--------------------- 3 files changed, 45 insertions(+), 50 deletions(-) diff --git a/controllers/url.go b/controllers/url.go index 487bd67..17ed7f7 100644 --- a/controllers/url.go +++ b/controllers/url.go @@ -239,27 +239,19 @@ func DeleteURL(ctx *gin.Context, db *bun.DB) { return } - userId, exists := ctx.Get("userId") - if !exists { - ctx.JSON(http.StatusUnauthorized, gin.H{ - "message": "Unauthorized", - }) - return - } - - userIdInt, ok := userId.(int64) - if !ok { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "message": "User ID type assertion failed", - }) - return - } + userId, exists := ctx.Get("userID") // Correct key here + if !exists { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "message": "Unauthorized", + }) + return + } var url models.Tinyurl err = db.NewSelect(). Model(&url). Where("id = ?", urlId). - Where("user_id = ?", userIdInt). + Where("user_id = ?", userId). Where("is_deleted = ?", false). Scan(ctx) if err != nil { @@ -275,7 +267,7 @@ func DeleteURL(ctx *gin.Context, db *bun.DB) { Set("is_deleted = ?", true). Set("deleted_at = ?", time.Now().UTC()). Where("id = ?", urlId). - Where("user_id = ?", userIdInt). + Where("user_id = ?", userId). Exec(ctx) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ @@ -284,7 +276,7 @@ func DeleteURL(ctx *gin.Context, db *bun.DB) { return } - err = utils.DecrementURLCount(userIdInt, db, ctx) + err = utils.DecrementURLCount(userId.(int64), db, ctx) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "message": "Failed to decrement URL count: " + err.Error(), @@ -294,7 +286,7 @@ func DeleteURL(ctx *gin.Context, db *bun.DB) { updatedCount, err := db.NewSelect(). Model(&models.Tinyurl{}). - Where("user_id = ?", userIdInt). + Where("user_id = ?", userId). Where("is_deleted = ?", false). Count(ctx) if err != nil { diff --git a/middlewares/auth.go b/middlewares/auth.go index 9d72654..1926924 100644 --- a/middlewares/auth.go +++ b/middlewares/auth.go @@ -2,7 +2,6 @@ package middleware import ( "net/http" - "strconv" "github.com/Real-Dev-Squad/tiny-site-backend/utils" "github.com/gin-gonic/gin" @@ -27,16 +26,16 @@ func AuthMiddleware() gin.HandlerFunc { return } - userIDStr := claims["userID"].(string) - userID, err := strconv.ParseInt(userIDStr, 10, 64) - if err != nil { - ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid UserID"}) - ctx.Abort() - return - } + userID, ok := claims["userID"].(float64) + + if !ok { + ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid UserID format"}) + ctx.Abort() + return + } ctx.Set("user", claims["email"]) - ctx.Set("userID", userID) + ctx.Set("userID", int64(userID)) ctx.Next() } } \ No newline at end of file diff --git a/utils/jwt.go b/utils/jwt.go index ca284aa..97c351c 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -9,47 +9,51 @@ import ( "github.com/golang-jwt/jwt/v5" ) -/* - * GenerateToken generates a JWT token for the user - */ +var ( + ErrUnexpectedSigningMethod = errors.New("unexpected signing method") + ErrInvalidToken = errors.New("invalid token") + ErrTokenExpired = errors.New("token has expired") +) + func GenerateToken(user *models.User) (string, error) { - issuer := config.JwtIssuer key := []byte(config.JwtSecret) - tokenValidityInHours := config.JwtValidity - tokenExpiryTime := time.Now().Add(time.Duration(tokenValidityInHours) * time.Hour).UTC().Unix() + expiryTime := time.Now().Add(time.Duration(config.JwtValidity) * time.Hour).UTC() + claims := jwt.MapClaims{ - "iss": issuer, - "exp": tokenExpiryTime, + "iss": config.JwtIssuer, + "exp": expiryTime.Unix(), + "iat": time.Now().UTC().Unix(), "email": user.Email, "userID": user.ID, - "iat": time.Now().Unix(), } + token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) - tokenString, err := token.SignedString(key) - return tokenString, err + return token.SignedString(key) } -/* - * VerifyToken verifies the token and returns the email of the user - */ func VerifyToken(tokenString string) (jwt.MapClaims, error) { + key := []byte(config.JwtSecret) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, errors.New("unexpected signing method") + return nil, ErrUnexpectedSigningMethod } - return []byte(config.JwtSecret), nil + return key, nil }) if err != nil { return nil, err } - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - if float64(time.Now().Unix()) > claims["exp"].(float64) { - return nil, errors.New("token has expired") - } - return claims, nil + claims, ok := token.Claims.(jwt.MapClaims) + if !ok || !token.Valid { + return nil, ErrInvalidToken + } + + expiryTime := time.Unix(int64(claims["exp"].(float64)), 0) + if time.Now().UTC().After(expiryTime) { + return nil, ErrTokenExpired } - return nil, errors.New("invalid token") + return claims, nil } \ No newline at end of file From 0833cb638f7c4d984c6d0b923950f3541a1f47b0 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Fri, 4 Oct 2024 18:06:57 +0530 Subject: [PATCH 05/11] chore: remove comment --- controllers/url.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/url.go b/controllers/url.go index 17ed7f7..c356890 100644 --- a/controllers/url.go +++ b/controllers/url.go @@ -239,7 +239,7 @@ func DeleteURL(ctx *gin.Context, db *bun.DB) { return } - userId, exists := ctx.Get("userID") // Correct key here + userId, exists := ctx.Get("userID") if !exists { ctx.JSON(http.StatusUnauthorized, gin.H{ "message": "Unauthorized", From 5a5b49bfdb4559c7089977608db958ac1aae6622 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Fri, 4 Oct 2024 19:41:11 +0530 Subject: [PATCH 06/11] chore: fix tests --- tests/unit/jwt_test.go | 54 +++++++++++++++++++++++++++++++----------- utils/jwt.go | 8 +++---- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/tests/unit/jwt_test.go b/tests/unit/jwt_test.go index 1992993..1ce8806 100644 --- a/tests/unit/jwt_test.go +++ b/tests/unit/jwt_test.go @@ -3,9 +3,12 @@ package unit import ( "os" "testing" + "time" + "github.com/Real-Dev-Squad/tiny-site-backend/config" "github.com/Real-Dev-Squad/tiny-site-backend/models" "github.com/Real-Dev-Squad/tiny-site-backend/utils" + "github.com/golang-jwt/jwt/v5" ) func TestMain(m *testing.M) { @@ -19,6 +22,7 @@ func TestMain(m *testing.M) { func TestGenerateJWT(t *testing.T) { dummyUser := &models.User{ Email: "test@gmail.com", + ID: 123, } token, err := utils.GenerateToken(dummyUser) @@ -36,32 +40,51 @@ func TestVerifyJWT(t *testing.T) { t.Run("ValidToken", func(t *testing.T) { dummyUser := &models.User{ Email: "test@gmail.com", + ID: 123, } validToken, generateTokenError := utils.GenerateToken(dummyUser) - if generateTokenError != nil { t.Fatalf("Error: %v", generateTokenError) } - email, validTokenError := utils.VerifyToken(validToken) - + claims, validTokenError := utils.VerifyToken(validToken) if validTokenError != nil { t.Fatalf("Error: %v", validTokenError) } - if email != dummyUser.Email { - t.Fatalf("Expected %v but got %v", dummyUser.Email, email) + if claims["email"] != dummyUser.Email { + t.Fatalf("Expected email %v but got %v", dummyUser.Email, claims["email"]) + } + + if claims["userID"] != float64(dummyUser.ID) { + t.Fatalf("Expected userID %v but got %v", dummyUser.ID, claims["userID"]) } }) t.Run("ExpiredToken", func(t *testing.T) { - expiredToken := "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAZ21haWwuY29tIiwiZXhwIjoiMjAyMy0xMC0wMVQxOTo1Njo0OS4zOTc5NzEyWiIsImlzcyI6Indpc2VlLWJhY2tlbmQifQ.h11JtaPg-ITKR8UXTyz_Q7pJU_3gYyXwIkqX7lI1UK2nVkvxQvkyN23-u3wj8fV5mNIvp-ePTOp-7odsPcGC_g" + expiredToken := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{ + "iss": config.JwtIssuer, + "exp": time.Now().Add(-time.Hour).Unix(), + "email": "test@gmail.com", + "userID": 123, + }) + + key := []byte(config.JwtSecret) + expiredTokenString, _ := expiredToken.SignedString(key) + + _, expiredTokenError := utils.VerifyToken(expiredTokenString) + if expiredTokenError != utils.ErrTokenExpired { + t.Fatalf("Expected error %v but got %v", utils.ErrTokenExpired, expiredTokenError) + } + }) - _, expiredTokenError := utils.VerifyToken(expiredToken) + t.Run("InvalidToken", func(t *testing.T) { + invalidToken := "invalid.token.here" - if expiredTokenError == nil { - t.Fatalf("Expected error but got nil") + _, invalidTokenError := utils.VerifyToken(invalidToken) + if invalidTokenError == nil { + t.Fatalf("Expected an error but got nil") } }) } @@ -72,21 +95,24 @@ func TestVerifyJWTForOneYear(t *testing.T) { dummyUser := &models.User{ Email: "test@gmail.com", + ID: 123, } validToken, generateTokenError := utils.GenerateToken(dummyUser) - if generateTokenError != nil { t.Fatalf("Error: %v", generateTokenError) } - email, validTokenError := utils.VerifyToken(validToken) - + claims, validTokenError := utils.VerifyToken(validToken) if validTokenError != nil { t.Fatalf("Error: %v", validTokenError) } - if email != dummyUser.Email { - t.Fatalf("Expected %v but got %v", dummyUser.Email, email) + if claims["email"] != dummyUser.Email { + t.Fatalf("Expected email %v but got %v", dummyUser.Email, claims["email"]) + } + + if claims["userID"] != float64(dummyUser.ID) { + t.Fatalf("Expected userID %v but got %v", dummyUser.ID, claims["userID"]) } } diff --git a/utils/jwt.go b/utils/jwt.go index 97c351c..fe7b8d8 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -42,6 +42,9 @@ func VerifyToken(tokenString string) (jwt.MapClaims, error) { }) if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, ErrTokenExpired + } return nil, err } @@ -50,10 +53,5 @@ func VerifyToken(tokenString string) (jwt.MapClaims, error) { return nil, ErrInvalidToken } - expiryTime := time.Unix(int64(claims["exp"].(float64)), 0) - if time.Now().UTC().After(expiryTime) { - return nil, ErrTokenExpired - } - return claims, nil } \ No newline at end of file From 8f586924d678212fcaa973d2febf5136ba7e5294 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Fri, 4 Oct 2024 20:47:11 +0530 Subject: [PATCH 07/11] chore: fix failing tests --- tests/integration/url_test.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/integration/url_test.go b/tests/integration/url_test.go index acefb50..3b6eda8 100644 --- a/tests/integration/url_test.go +++ b/tests/integration/url_test.go @@ -15,6 +15,12 @@ import ( func (suite *AppTestSuite) TestCreateTinyURLSuccess() { // Setup the router and route for creating a tiny URL router := gin.Default() + + router.Use(func(ctx *gin.Context) { + ctx.Set("userID", int64(1)) + ctx.Next() + }) + router.POST("/v1/tinyurl", func(ctx *gin.Context) { controller.CreateTinyURL(ctx, suite.db) }) @@ -81,6 +87,12 @@ func (suite *AppTestSuite) TestCreateTinyURLEmptyOriginalURL() { // TestCreateTinyURLCustomShortURL tests the creation of a tiny URL with a custom short URL and expects a successful response. func (suite *AppTestSuite) TestCreateTinyURLCustomShortURL() { router := gin.Default() + + router.Use(func(ctx *gin.Context) { + ctx.Set("userID", int64(1)) + ctx.Next() + }) + router.POST("/v1/tinyurl", func(ctx *gin.Context) { controller.CreateTinyURL(ctx, suite.db) }) @@ -88,7 +100,6 @@ func (suite *AppTestSuite) TestCreateTinyURLCustomShortURL() { requestBody := map[string]interface{}{ "OriginalUrl": "https://example.com", "ShortUrl": "short", - "UserId": 1, } requestJSON, _ := json.Marshal(requestBody) req, _ := http.NewRequest("POST", "/v1/tinyurl", bytes.NewBuffer(requestJSON)) @@ -102,6 +113,12 @@ func (suite *AppTestSuite) TestCreateTinyURLCustomShortURL() { func (suite *AppTestSuite) TestCreateTinyURLCustomShortURLExists() { router := gin.Default() + + router.Use(func(ctx *gin.Context) { + ctx.Set("userID", int64(1)) + ctx.Next() + }) + router.POST("/v1/tinyurl", func(ctx *gin.Context) { controller.CreateTinyURL(ctx, suite.db) }) @@ -109,7 +126,6 @@ func (suite *AppTestSuite) TestCreateTinyURLCustomShortURLExists() { requestBody := map[string]interface{}{ "OriginalUrl": "https://rds.com", "ShortUrl": "37fff", - "UserId": 1, } requestJSON, _ := json.Marshal(requestBody) req, _ := http.NewRequest("POST", "/v1/tinyurl", bytes.NewBuffer(requestJSON)) @@ -123,6 +139,12 @@ func (suite *AppTestSuite) TestCreateTinyURLCustomShortURLExists() { func (suite *AppTestSuite) TestCreateTinyURLExistingOriginalURL() { router := gin.Default() + + router.Use(func(ctx *gin.Context) { + ctx.Set("userID", int64(1)) + ctx.Next() + }) + router.POST("/v1/tinyurl", func(ctx *gin.Context) { controller.CreateTinyURL(ctx, suite.db) }) @@ -131,7 +153,6 @@ func (suite *AppTestSuite) TestCreateTinyURLExistingOriginalURL() { requestBody := map[string]interface{}{ "OriginalUrl": existingOriginalURL, - "UserId": 1, } requestJSON, _ := json.Marshal(requestBody) req, _ := http.NewRequest("POST", "/v1/tinyurl", bytes.NewBuffer(requestJSON)) From 4430c7090f88a0f5fd96b6ee33c5b98cd99e6db9 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Sat, 5 Oct 2024 01:18:01 +0530 Subject: [PATCH 08/11] chore: refactor delete url function --- controllers/url.go | 63 +++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/controllers/url.go b/controllers/url.go index c356890..a1f11aa 100644 --- a/controllers/url.go +++ b/controllers/url.go @@ -1,7 +1,6 @@ package controller import ( - "fmt" "net/http" "strconv" "strings" @@ -230,68 +229,46 @@ func GetAllURLs(ctx *gin.Context, db *bun.DB) { func DeleteURL(ctx *gin.Context, db *bun.DB) { id, _ := ctx.Params.Get("id") - - urlId, err := strconv.Atoi(id) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "message": "Invalid URL ID", - }) - return - } - userId, exists := ctx.Get("userID") - if !exists { - ctx.JSON(http.StatusUnauthorized, gin.H{ - "message": "Unauthorized", - }) - return - } - - var url models.Tinyurl - err = db.NewSelect(). - Model(&url). - Where("id = ?", urlId). - Where("user_id = ?", userId). - Where("is_deleted = ?", false). - Scan(ctx) - if err != nil { - fmt.Println("Error during URL query:", err) - ctx.JSON(http.StatusNotFound, gin.H{ - "message": "URL not found or already deleted", + userID, exists := ctx.Get("userID") + if !exists { + ctx.JSON(http.StatusUnauthorized, dtos.UserURLsResponse{ + Message: "User not authenticated", }) return } - _, err = db.NewUpdate(). - Model(&url). - Set("is_deleted = ?", true). - Set("deleted_at = ?", time.Now().UTC()). - Where("id = ?", urlId). - Where("user_id = ?", userId). + _, err := db.NewUpdate(). + Model(&models.Tinyurl{}). + Set("is_deleted=?", true). + Set("deleted_at=?", time.Now().UTC()). + Where("id = ?", id). + Where("user_id = ?", userID). Exec(ctx) + if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "message": "Failed to delete URL", + ctx.JSON(http.StatusNotFound, dtos.UserURLsResponse{ + Message: "No URLs found", }) return } - err = utils.DecrementURLCount(userId.(int64), db, ctx) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "message": "Failed to decrement URL count: " + err.Error(), + if err := utils.DecrementURLCount(userID.(int64), db, ctx); err != nil { + ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ + Message: "Failed to decrement URL count: " + err.Error(), }) return } updatedCount, err := db.NewSelect(). Model(&models.Tinyurl{}). - Where("user_id = ?", userId). + Where("user_id = ?", userID). Where("is_deleted = ?", false). Count(ctx) + if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "message": "Failed to fetch updated URL count", + ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ + Message: "Failed to fetch updated URL count", }) return } From 5f592d4b5b784fe67471d5996e04c081ede2bad6 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Sat, 5 Oct 2024 01:42:48 +0530 Subject: [PATCH 09/11] chore: fix query syntax --- controllers/url.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/controllers/url.go b/controllers/url.go index a1f11aa..cf6b7ac 100644 --- a/controllers/url.go +++ b/controllers/url.go @@ -42,8 +42,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { var existingOriginalURL models.Tinyurl if err := db.NewSelect().Model(&existingOriginalURL). Where("original_url = ?", body.OriginalUrl). - Where("user_id = ?", userID). - Where("is_deleted = ?", false). + Where("user_id = ? AND is_deleted = ?", userID, false). Scan(ctx); err == nil { ctx.JSON(http.StatusOK, dtos.URLCreationResponse{ Message: "Shortened URL already exists", @@ -84,8 +83,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { count, err := db.NewSelect(). Model(&models.Tinyurl{}). - Where("user_id = ?", userID). - Where("is_deleted = ?", false). + Where("user_id = ? AND is_deleted = ?", userID, false). Count(ctx) if err != nil { @@ -125,8 +123,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { updatedCount, err := db.NewSelect(). Model(&models.Tinyurl{}). - Where("user_id = ?", userID). - Where("is_deleted = ?", false). + Where("user_id = ? AND is_deleted = ?", userID, false). Count(ctx) if err != nil { From 2eda15fb4e29e2d934a0ffa848b3780c7f997c47 Mon Sep 17 00:00:00 2001 From: Amit Prakash <34869115+iamitprakash@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:04:27 +0530 Subject: [PATCH 10/11] used ParseWithClaims --- utils/jwt.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/utils/jwt.go b/utils/jwt.go index fe7b8d8..aede20a 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -34,7 +34,11 @@ func GenerateToken(user *models.User) (string, error) { func VerifyToken(tokenString string) (jwt.MapClaims, error) { key := []byte(config.JwtSecret) - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Parsing the token + + token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { + + //validatint the algo if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, ErrUnexpectedSigningMethod } @@ -42,16 +46,16 @@ func VerifyToken(tokenString string) (jwt.MapClaims, error) { }) if err != nil { - if errors.Is(err, jwt.ErrTokenExpired) { + if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) { return nil, ErrTokenExpired } return nil, err } - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || !token.Valid { - return nil, ErrInvalidToken + // Validating the token and casting the claims :P + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil } - return claims, nil -} \ No newline at end of file + return nil, ErrInvalidToken +} From e8bcf4b1fdfaa2a42c7ee3f02dde20f7fddcd14e Mon Sep 17 00:00:00 2001 From: Amit Prakash <34869115+iamitprakash@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:08:17 +0530 Subject: [PATCH 11/11] query adjusted --- controllers/url.go | 108 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/controllers/url.go b/controllers/url.go index cf6b7ac..3815732 100644 --- a/controllers/url.go +++ b/controllers/url.go @@ -31,7 +31,7 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { return } - userID , exists := ctx.Get("userID") + userID, exists := ctx.Get("userID") if !exists { ctx.JSON(http.StatusUnauthorized, dtos.URLCreationResponse{ Message: "User not authenticated", @@ -41,12 +41,11 @@ func CreateTinyURL(ctx *gin.Context, db *bun.DB) { var existingOriginalURL models.Tinyurl if err := db.NewSelect().Model(&existingOriginalURL). - Where("original_url = ?", body.OriginalUrl). - Where("user_id = ? AND is_deleted = ?", userID, false). + Where("original_url = ? AND user_id = ? AND is_deleted = ?", body.OriginalUrl, userID, false). Scan(ctx); err == nil { ctx.JSON(http.StatusOK, dtos.URLCreationResponse{ - Message: "Shortened URL already exists", - ShortURL: existingOriginalURL.ShortUrl, + Message: "Shortened URL already exists", + ShortURL: existingOriginalURL.ShortUrl, CreatedAt: existingOriginalURL.CreatedAt, }) return @@ -225,57 +224,56 @@ func GetAllURLs(ctx *gin.Context, db *bun.DB) { } func DeleteURL(ctx *gin.Context, db *bun.DB) { - id, _ := ctx.Params.Get("id") - - userID, exists := ctx.Get("userID") - if !exists { - ctx.JSON(http.StatusUnauthorized, dtos.UserURLsResponse{ - Message: "User not authenticated", - }) - return - } - - _, err := db.NewUpdate(). - Model(&models.Tinyurl{}). - Set("is_deleted=?", true). - Set("deleted_at=?", time.Now().UTC()). - Where("id = ?", id). - Where("user_id = ?", userID). - Exec(ctx) - - if err != nil { - ctx.JSON(http.StatusNotFound, dtos.UserURLsResponse{ - Message: "No URLs found", - }) - return - } - - if err := utils.DecrementURLCount(userID.(int64), db, ctx); err != nil { - ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ - Message: "Failed to decrement URL count: " + err.Error(), - }) - return - } - - updatedCount, err := db.NewSelect(). - Model(&models.Tinyurl{}). - Where("user_id = ?", userID). - Where("is_deleted = ?", false). - Count(ctx) - - if err != nil { - ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ - Message: "Failed to fetch updated URL count", - }) - return - } - - ctx.JSON(http.StatusOK, dtos.URLDeleteResponse{ - Message: "URL deleted", - URLCount: updatedCount, - }) -} + id, _ := ctx.Params.Get("id") + + userID, exists := ctx.Get("userID") + if !exists { + ctx.JSON(http.StatusUnauthorized, dtos.UserURLsResponse{ + Message: "User not authenticated", + }) + return + } + + _, err := db.NewUpdate(). + Model(&models.Tinyurl{}). + Set("is_deleted=?", true). + Set("deleted_at=?", time.Now().UTC()). + Where("id = ?", id). + Where("user_id = ?", userID). + Exec(ctx) + + if err != nil { + ctx.JSON(http.StatusNotFound, dtos.UserURLsResponse{ + Message: "No URLs found", + }) + return + } + + if err := utils.DecrementURLCount(userID.(int64), db, ctx); err != nil { + ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ + Message: "Failed to decrement URL count: " + err.Error(), + }) + return + } + + updatedCount, err := db.NewSelect(). + Model(&models.Tinyurl{}). + Where("user_id = ?", userID). + Where("is_deleted = ?", false). + Count(ctx) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{ + Message: "Failed to fetch updated URL count", + }) + return + } + ctx.JSON(http.StatusOK, dtos.URLDeleteResponse{ + Message: "URL deleted", + URLCount: updatedCount, + }) +} func GetURLDetails(ctx *gin.Context, db *bun.DB) { shortURL := ctx.Param("shortURL")