From e277d2076bc39980cc7dd574c1561e278b6c884b Mon Sep 17 00:00:00 2001 From: Moon Patel Date: Fri, 8 Nov 2024 11:15:40 +0530 Subject: [PATCH 1/4] add: verify otp endpoint (incomplete) --- .env.example | 12 +++++++ go.mod | 3 ++ go.sum | 6 ++++ internal/app/container.go | 13 ++++--- internal/controllers/auth_controller.go | 8 +++++ internal/database/redis.go | 30 ++++++++++++++++ internal/repositories/redis_repository.go | 11 ++++++ internal/server/routes.go | 1 + internal/services/auth_service.go | 4 ++- internal/validators/verify_otp.go | 5 +++ test.go | 44 +++++++++++++++++++++++ 11 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 internal/database/redis.go create mode 100644 internal/repositories/redis_repository.go create mode 100644 internal/validators/verify_otp.go create mode 100644 test.go diff --git a/.env.example b/.env.example index e11882e4..13d3228f 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,15 @@ DB_SCHEMA=public MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=minioadmin MINIO_PORT=9000 + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD=password + +# STMP +SMTP_HOST= +SMTP_PORT= +EMAIL_FROM= +SMPT_USER= +SMPT_PASS= \ No newline at end of file diff --git a/go.mod b/go.mod index de4b093e..f7d4968e 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,13 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -56,6 +58,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/redis/go-redis/v9 v9.7.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect diff --git a/go.sum b/go.sum index 9bf522f3..30bcd86a 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -23,6 +25,8 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= @@ -117,6 +121,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= diff --git a/internal/app/container.go b/internal/app/container.go index 9884baf2..9393fce2 100644 --- a/internal/app/container.go +++ b/internal/app/container.go @@ -9,8 +9,9 @@ import ( ) type Container struct { - DB database.Service - AuthService *services.AuthService + DB database.Service + RedisService database.RedisService + AuthService *services.AuthService } var ( @@ -22,13 +23,15 @@ func GetContainer() *Container { once.Do(func() { db := database.New() gormDB := database.GetDB() + rds := database.NewRedisClient() userRepo := repositories.NewUserRepository(gormDB) - authService := services.NewAuthService(userRepo) + authService := services.NewAuthService(userRepo, rds) container = &Container{ - DB: db, - AuthService: authService, + DB: db, + AuthService: authService, + RedisService: *rds, } }) return container diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index 04d65a6b..d97b965d 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -49,3 +49,11 @@ func (ac *AuthController) SignUp(c *fiber.Ctx) error { return c.JSON(fiber.Map{"message": "User Signed Up!"}) } + +func (ac *AuthController) VerifyOTP(c *fiber.Ctx) error { + userEmail := new(validators.VerifyOTP) + + if err := c.BodyParser(userEmail); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + } +} diff --git a/internal/database/redis.go b/internal/database/redis.go new file mode 100644 index 00000000..29f1c95f --- /dev/null +++ b/internal/database/redis.go @@ -0,0 +1,30 @@ +package database + +import ( + "context" + "os" + + "github.com/redis/go-redis/v9" +) + +var rdb *RedisService + +type RedisService struct { + redisClient *redis.Client + ctx context.Context +} + +func NewRedisClient() *RedisService { + if rdb != nil { + return rdb + } + redisClient := redis.NewClient(&redis.Options{ + Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"), + DB: 0, + }) + ctx := context.Background() + + rdb = &RedisService{redisClient: redisClient, ctx: ctx} + + return rdb +} diff --git a/internal/repositories/redis_repository.go b/internal/repositories/redis_repository.go new file mode 100644 index 00000000..cfd73ac7 --- /dev/null +++ b/internal/repositories/redis_repository.go @@ -0,0 +1,11 @@ +package repositories + +import "keizer-auth-api/internal/database" + +type RedisRepository struct { + rdb *database.RedisService +} + +func NewRedisRepository(rds *database.RedisService) *RedisRepository { + return &RedisRepository{rdb: rds} +} diff --git a/internal/server/routes.go b/internal/server/routes.go index cf1c2e71..08d93058 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -18,6 +18,7 @@ func (s *FiberServer) RegisterFiberRoutes() { auth := api.Group("/auth") auth.Post("/sign-up", s.controllers.Auth.SignUp) auth.Post("/sign-in", s.controllers.Auth.SignIn) + auth.Post("/verify-otp", s.controllers.Auth.VerifyOTP) s.Static("/", "./web/dist") s.Static("*", "./web/dist/index.html") diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 27d6cafd..03bf667d 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -3,6 +3,7 @@ package services import ( "fmt" + "keizer-auth-api/internal/database" "keizer-auth-api/internal/models" "keizer-auth-api/internal/repositories" "keizer-auth-api/internal/utils" @@ -11,9 +12,10 @@ import ( type AuthService struct { userRepo *repositories.UserRepository + rds *database.RedisService } -func NewAuthService(userRepo *repositories.UserRepository) *AuthService { +func NewAuthService(userRepo *repositories.UserRepository, rds *database.RedisService) *AuthService { return &AuthService{userRepo: userRepo} } diff --git a/internal/validators/verify_otp.go b/internal/validators/verify_otp.go new file mode 100644 index 00000000..add8747c --- /dev/null +++ b/internal/validators/verify_otp.go @@ -0,0 +1,5 @@ +package validators + +type VerifyOTP struct { + Email string `validate:"required|email" label:"Email"` +} diff --git a/test.go b/test.go new file mode 100644 index 00000000..c67e4c0c --- /dev/null +++ b/test.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "time" + + "github.com/redis/go-redis/v9" +) + +func main() { + rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 0}) + + println("rdb", rdb) + // Creating two channels + ch1 := make(chan string) + ch2 := make(chan string) + + // Launching goroutines to send data to the channels after delays + go func() { + time.Sleep(2 * time.Second) + ch1 <- "message from ch1" + }() + + go func() { + time.Sleep(4 * time.Second) + ch2 <- "message from ch2" + }() + // go func() { + // time.Sleep(1 * time.Second) + // ch2 <- "message from ch2 again" + // }() + + go time.Sleep(time.Second * 4) + + // Using select to wait on multiple channels + select { + case msg := <-ch1: + fmt.Println("Received:", msg) + case msg := <-ch2: + fmt.Println("Received:", msg) + case <-time.After(1 * time.Second): + fmt.Println("Timeout: No messages received within 3 seconds") + } +} From c686c979f113b13810fbda7f2ce3c3d19134e450 Mon Sep 17 00:00:00 2001 From: Moon Patel Date: Fri, 8 Nov 2024 21:21:43 +0530 Subject: [PATCH 2/4] add: otp verification - otp verification route added - architecture revamped in some places - added session service and removed session repo - removed unused code --- internal/app/container.go | 10 +-- internal/app/controllers.go | 2 +- internal/controllers/auth_controller.go | 41 +++++++++-- internal/database/redis.go | 6 +- internal/models/user.go | 14 ++-- internal/repositories/redis_repository.go | 30 +++++++- internal/repositories/session_repository.go | 51 ------------- internal/repositories/user_repository.go | 15 ++++ internal/services/auth_service.go | 32 +++++++-- internal/services/session_service.go | 79 +++++++++++++++++++++ internal/utils/session_helpers.go | 41 ++++------- internal/validators/verify_otp.go | 2 + 12 files changed, 216 insertions(+), 107 deletions(-) delete mode 100644 internal/repositories/session_repository.go create mode 100644 internal/services/session_service.go diff --git a/internal/app/container.go b/internal/app/container.go index 9393fce2..3bdee13e 100644 --- a/internal/app/container.go +++ b/internal/app/container.go @@ -9,9 +9,10 @@ import ( ) type Container struct { - DB database.Service - RedisService database.RedisService - AuthService *services.AuthService + DB database.Service + RedisService database.RedisService + AuthService *services.AuthService + SessionService *services.SessionService } var ( @@ -26,7 +27,8 @@ func GetContainer() *Container { rds := database.NewRedisClient() userRepo := repositories.NewUserRepository(gormDB) - authService := services.NewAuthService(userRepo, rds) + redisRepo := repositories.NewRedisRepository(rds) + authService := services.NewAuthService(userRepo, redisRepo) container = &Container{ DB: db, diff --git a/internal/app/controllers.go b/internal/app/controllers.go index 1c131880..e13c4557 100644 --- a/internal/app/controllers.go +++ b/internal/app/controllers.go @@ -8,6 +8,6 @@ type ServerControllers struct { func GetControllers(container *Container) *ServerControllers { return &ServerControllers{ - Auth: controllers.NewAuthController(container.AuthService), + Auth: controllers.NewAuthController(container.AuthService, container.SessionService), } } diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index d97b965d..1bb2c810 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -2,20 +2,24 @@ package controllers import ( "errors" + "fmt" "keizer-auth-api/internal/services" + "keizer-auth-api/internal/utils" "keizer-auth-api/internal/validators" "github.com/gofiber/fiber/v2" + "github.com/google/uuid" "gorm.io/gorm" ) type AuthController struct { - authService *services.AuthService + authService *services.AuthService + sessionService *services.SessionService } -func NewAuthController(as *services.AuthService) *AuthController { - return &AuthController{authService: as} +func NewAuthController(as *services.AuthService, ss *services.SessionService) *AuthController { + return &AuthController{authService: as, sessionService: ss} } func (ac *AuthController) SignIn(c *fiber.Ctx) error { @@ -51,9 +55,36 @@ func (ac *AuthController) SignUp(c *fiber.Ctx) error { } func (ac *AuthController) VerifyOTP(c *fiber.Ctx) error { - userEmail := new(validators.VerifyOTP) + verifyOtpBody := new(validators.VerifyOTP) - if err := c.BodyParser(userEmail); err != nil { + if err := c.BodyParser(verifyOtpBody); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } + + isOtpValid, err := ac.authService.VerifyOTP(verifyOtpBody) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "OTP not found"}) + } + if err.Error() == "otp expired" { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "OTP expired"}) + } + + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to verify OTP"}) + } + if !isOtpValid { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "OTP not valid"}) + } + + parsedUuid, err := uuid.Parse(verifyOtpBody.Id) + if err != nil { + return fmt.Errorf("error parsing uuid %w", err) + } + sessionId, err := ac.sessionService.CreateSession(parsedUuid) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create session"}) + } + utils.SetSessionCookie(c, sessionId) + + return c.JSON(fiber.Map{"message": "OTP Verified!"}) } diff --git a/internal/database/redis.go b/internal/database/redis.go index 29f1c95f..afebb87f 100644 --- a/internal/database/redis.go +++ b/internal/database/redis.go @@ -10,8 +10,8 @@ import ( var rdb *RedisService type RedisService struct { - redisClient *redis.Client - ctx context.Context + RedisClient *redis.Client + Ctx context.Context } func NewRedisClient() *RedisService { @@ -24,7 +24,7 @@ func NewRedisClient() *RedisService { }) ctx := context.Background() - rdb = &RedisService{redisClient: redisClient, ctx: ctx} + rdb = &RedisService{RedisClient: redisClient, Ctx: ctx} return rdb } diff --git a/internal/models/user.go b/internal/models/user.go index 8f9481a1..09fe5246 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -3,13 +3,13 @@ package models import "time" type User struct { - LastLogin time.Time - Email string `gorm:"not null;default:null;unique,index;type:varchar(100)"` - PasswordHash string - FirstName string `gorm:"not null;type:varchar(100);default:null"` - LastName string `gorm:"type:varchar(100);default:null"` + LastLogin time.Time `json:"lastLogin"` + Email string `gorm:"not null;default:null;unique,index;type:varchar(100)" json:"email"` + PasswordHash string `json:"-"` + FirstName string `gorm:"not null;type:varchar(100);default:null" json:"fName"` + LastName string `gorm:"type:varchar(100);default:null" json:"lName"` Base Sessions []Session - IsVerified bool `gorm:"not null;default:false"` - IsActive bool `gorm:"not null;default:false"` + IsVerified bool `gorm:"not null;default:false" json:"isVerified"` + IsActive bool `gorm:"not null;default:false" json:"isActive"` } diff --git a/internal/repositories/redis_repository.go b/internal/repositories/redis_repository.go index cfd73ac7..bb34d9ab 100644 --- a/internal/repositories/redis_repository.go +++ b/internal/repositories/redis_repository.go @@ -1,11 +1,35 @@ package repositories -import "keizer-auth-api/internal/database" +import ( + "keizer-auth-api/internal/database" + "time" +) type RedisRepository struct { - rdb *database.RedisService + rds *database.RedisService } func NewRedisRepository(rds *database.RedisService) *RedisRepository { - return &RedisRepository{rdb: rds} + return &RedisRepository{rds: rds} +} + +func (rr *RedisRepository) Get(key string) (string, error) { + value, err := rr.rds.RedisClient.Get(rr.rds.Ctx, key).Result() + return value, err +} + +func (rr *RedisRepository) Set(key string, value string) error { + err := rr.rds.RedisClient.Set(rr.rds.Ctx, key, value, 0).Err() + return err +} + +// set a key's value with expiration +func (rr *RedisRepository) SetEx(key string, value string, expiration time.Duration) error { + err := rr.rds.RedisClient.Set(rr.rds.Ctx, key, value, expiration).Err() + return err +} + +func (rr *RedisRepository) TTL(key string) (time.Duration, error) { + result, err := rr.rds.RedisClient.TTL(rr.rds.Ctx, key).Result() + return result, err } diff --git a/internal/repositories/session_repository.go b/internal/repositories/session_repository.go deleted file mode 100644 index 2fd5936d..00000000 --- a/internal/repositories/session_repository.go +++ /dev/null @@ -1,51 +0,0 @@ -package repositories - -import ( - "time" - - "keizer-auth-api/internal/models" - - "gorm.io/gorm" -) - -type SessionRepository struct { - db *gorm.DB -} - -// NewSessionRepository creates a new instance of SessionRepository -func NewSessionRepository(db *gorm.DB) *SessionRepository { - return &SessionRepository{db: db} -} - -// CreateSession saves a new session in the database -func (r *SessionRepository) CreateSession(session *models.Session) error { - return r.db.Create(session).Error -} - -// GetSession retrieves a session by its session ID -func (r *SessionRepository) GetSession(sessionId string) (*models.Session, error) { - var session models.Session - if err := r.db.First(&session, "session_id = ?", sessionId).Error; err != nil { - return nil, err - } - return &session, nil -} - -// DeleteSession removes a session from the database -func (r *SessionRepository) DeleteSession(sessionId string) error { - return r.db.Delete(&models.Session{}, sessionId).Error -} - -// UpdateSession updates an existing session -func (r *SessionRepository) UpdateSession(session *models.Session) error { - return r.db.Save(session).Error -} - -// FindValidSession checks if the session is valid (not expired) -func (r *SessionRepository) FindValidSession(token string) (*models.Session, error) { - var session models.Session - if err := r.db.First(&session, "token = ? AND expires_at > ?", token, time.Now()).Error; err != nil { - return nil, err - } - return &session, nil -} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go index 78ad551e..f16e9ca6 100644 --- a/internal/repositories/user_repository.go +++ b/internal/repositories/user_repository.go @@ -1,8 +1,10 @@ package repositories import ( + "fmt" "keizer-auth-api/internal/models" + "github.com/google/uuid" "gorm.io/gorm" ) @@ -17,3 +19,16 @@ func NewUserRepository(db *gorm.DB) *UserRepository { func (r *UserRepository) CreateUser(user *models.User) error { return r.db.Create(user).Error } + +func (r *UserRepository) GetUser(uuid uuid.UUID) (*models.User, error) { + var user models.User + result := r.db.First(&user, uuid.String()) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("error in getting user: %w", result.Error) + } + + return &user, nil +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 03bf667d..10023d33 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -2,8 +2,10 @@ package services import ( "fmt" + "time" + + "github.com/redis/go-redis/v9" - "keizer-auth-api/internal/database" "keizer-auth-api/internal/models" "keizer-auth-api/internal/repositories" "keizer-auth-api/internal/utils" @@ -11,12 +13,12 @@ import ( ) type AuthService struct { - userRepo *repositories.UserRepository - rds *database.RedisService + userRepo *repositories.UserRepository + redisRepo *repositories.RedisRepository } -func NewAuthService(userRepo *repositories.UserRepository, rds *database.RedisService) *AuthService { - return &AuthService{userRepo: userRepo} +func NewAuthService(userRepo *repositories.UserRepository, redisRepo *repositories.RedisRepository) *AuthService { + return &AuthService{userRepo: userRepo, redisRepo: redisRepo} } func (as *AuthService) RegisterUser(userRegister *validators.SignUpUser) error { @@ -30,6 +32,11 @@ func (as *AuthService) RegisterUser(userRegister *validators.SignUpUser) error { return fmt.Errorf("failed to generate OTP: %w", err) } + err = as.redisRepo.SetEx("registration-verification-otp-"+userRegister.Email, otp, time.Minute) + if err != nil { + return fmt.Errorf("failed to save otp in redis: %w", err) + } + // TODO: email should be sent using async func if err = SendOTPEmail(userRegister.Email, otp); err != nil { return fmt.Errorf("failed to send OTP email: %w", err) @@ -46,3 +53,18 @@ func (as *AuthService) RegisterUser(userRegister *validators.SignUpUser) error { return nil } + +func (as *AuthService) VerifyOTP(verifyOtpBody *validators.VerifyOTP) (bool, error) { + val, err := as.redisRepo.Get(verifyOtpBody.Email) + if err != nil { + if err == redis.Nil { + return false, fmt.Errorf("otp expired") + } + return false, fmt.Errorf("failed to get otp from redis %w", err) + } + + if val != verifyOtpBody.Otp { + return false, nil + } + return true, nil +} diff --git a/internal/services/session_service.go b/internal/services/session_service.go new file mode 100644 index 00000000..86dccd40 --- /dev/null +++ b/internal/services/session_service.go @@ -0,0 +1,79 @@ +package services + +import ( + "encoding/json" + "fmt" + "keizer-auth-api/internal/models" + "keizer-auth-api/internal/repositories" + "keizer-auth-api/internal/utils" + "time" + + "github.com/google/uuid" + "github.com/redis/go-redis/v9" +) + +type SessionService struct { + redisRepo *repositories.RedisRepository + userRepo *repositories.UserRepository +} + +func NewSessionService(redisRepo *repositories.RedisRepository) *SessionService { + return &SessionService{redisRepo: redisRepo} +} + +func (ss *SessionService) CreateSession(uuid uuid.UUID) (string, error) { + sessionId, err := utils.GenerateSessionID() + if err != nil { + return "", fmt.Errorf("error in generating session %w", err) + } + + var user *models.User + user, err = ss.userRepo.GetUser(uuid) + if err != nil { + return "", fmt.Errorf("error in getting user %w", err) + } + userJson, err := json.Marshal(user) + if err != nil { + return "", fmt.Errorf("error occured %w", err) + } + + err = ss.redisRepo.SetEx("dashboard-user-session-"+sessionId, string(userJson), utils.SessionExpiresIn) + if err != nil { + return "", fmt.Errorf("error in setting session %w", err) + } + + return sessionId, nil +} + +func (ss *SessionService) GetSession(sessionId string) (*models.User, error) { + userSession, err := ss.redisRepo.Get("dashboard-user-session-" + sessionId) + if err != nil { + return nil, fmt.Errorf("no session found") + } + var userData *models.User + err = json.Unmarshal([]byte(userSession), userData) + if err != nil { + return nil, fmt.Errorf("error in unmarshalling") + } + return userData, nil +} + +func (ss *SessionService) UpdateSession(sessionId string) error { + val, err := ss.redisRepo.Get("dashboard-user-session-" + sessionId) + if err != nil { + if err == redis.Nil { + return fmt.Errorf("session not found") + } + return err + } + err = ss.redisRepo.SetEx("dashboard-user-session-"+sessionId, val, utils.SessionExpiresIn) + if err != nil { + return fmt.Errorf("error in updating session %w", err) + } + return nil +} + +func (ss *SessionService) TTL(sessionId string) (time.Duration, error) { + ttl, err := ss.redisRepo.TTL("dashboard-user-session-" + sessionId) + return ttl, err +} diff --git a/internal/utils/session_helpers.go b/internal/utils/session_helpers.go index 512a5fa7..b15c9112 100644 --- a/internal/utils/session_helpers.go +++ b/internal/utils/session_helpers.go @@ -3,16 +3,12 @@ package utils import ( "crypto/rand" "encoding/base32" - "errors" "time" - "keizer-auth-api/internal/models" - "keizer-auth-api/internal/repositories" - "github.com/gofiber/fiber/v2" ) -const sessionExpiresIn = 30 * 24 * time.Hour +const SessionExpiresIn = 30 * 24 * time.Hour func GenerateSessionID() (string, error) { bytes := make([]byte, 15) @@ -22,34 +18,23 @@ func GenerateSessionID() (string, error) { return base32.StdEncoding.EncodeToString(bytes), nil } -func ValidateSession( - sessionID string, - repo *repositories.SessionRepository, -) (*models.Session, error) { - session, err := repo.GetSession(sessionID) - if err != nil { - return nil, errors.New("invalid session ID") - } - - if time.Now().After(session.ExpiresAt) { - return nil, errors.New("expired session") - } - - if time.Now().After(session.ExpiresAt.Add(-sessionExpiresIn / 2)) { - session.ExpiresAt = time.Now().Add(sessionExpiresIn) - if err := repo.UpdateSession(session); err != nil { - return nil, err - } - } - - return session, nil -} +// func ValidateSession( +// sessionID string, +// ttl time.Duration, +// ) error { +// if ttl < SessionExpiresIn { +// if err := sessionService.UpdateSession(sessionID); err != nil { +// return nil, err +// } +// } +// return nil +// } func SetSessionCookie(c *fiber.Ctx, sessionID string) { c.Cookie(&fiber.Cookie{ Name: "session_id", Value: sessionID, - Expires: time.Now().Add(sessionExpiresIn), + Expires: time.Now().Add(SessionExpiresIn), HTTPOnly: true, Secure: true, SameSite: "None", diff --git a/internal/validators/verify_otp.go b/internal/validators/verify_otp.go index add8747c..6f32f5e5 100644 --- a/internal/validators/verify_otp.go +++ b/internal/validators/verify_otp.go @@ -2,4 +2,6 @@ package validators type VerifyOTP struct { Email string `validate:"required|email" label:"Email"` + Otp string `validate:"required" label:"OTP"` + Id string `validate:"required" label:"Id"` } From 35805f2dcfdd2f1a7d77c63471e12876c44f394f Mon Sep 17 00:00:00 2001 From: Moon Patel Date: Sat, 9 Nov 2024 02:28:31 +0530 Subject: [PATCH 3/4] made the required changes --- internal/app/container.go | 6 ++---- internal/models/user.go | 10 +++++----- internal/repositories/redis_repository.go | 7 +------ internal/services/auth_service.go | 2 +- internal/services/session_service.go | 4 ++-- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/internal/app/container.go b/internal/app/container.go index 3bdee13e..b9a1e543 100644 --- a/internal/app/container.go +++ b/internal/app/container.go @@ -10,7 +10,6 @@ import ( type Container struct { DB database.Service - RedisService database.RedisService AuthService *services.AuthService SessionService *services.SessionService } @@ -31,9 +30,8 @@ func GetContainer() *Container { authService := services.NewAuthService(userRepo, redisRepo) container = &Container{ - DB: db, - AuthService: authService, - RedisService: *rds, + DB: db, + AuthService: authService, } }) return container diff --git a/internal/models/user.go b/internal/models/user.go index 09fe5246..a271e60b 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -3,13 +3,13 @@ package models import "time" type User struct { - LastLogin time.Time `json:"lastLogin"` + LastLogin time.Time `json:"last_login"` Email string `gorm:"not null;default:null;unique,index;type:varchar(100)" json:"email"` PasswordHash string `json:"-"` - FirstName string `gorm:"not null;type:varchar(100);default:null" json:"fName"` - LastName string `gorm:"type:varchar(100);default:null" json:"lName"` + FirstName string `gorm:"not null;type:varchar(100);default:null" json:"first_name"` + LastName string `gorm:"type:varchar(100);default:null" json:"last_name"` Base Sessions []Session - IsVerified bool `gorm:"not null;default:false" json:"isVerified"` - IsActive bool `gorm:"not null;default:false" json:"isActive"` + IsVerified bool `gorm:"not null;default:false" json:"is_verified"` + IsActive bool `gorm:"not null;default:false" json:"is_active"` } diff --git a/internal/repositories/redis_repository.go b/internal/repositories/redis_repository.go index bb34d9ab..3cff6a48 100644 --- a/internal/repositories/redis_repository.go +++ b/internal/repositories/redis_repository.go @@ -18,13 +18,8 @@ func (rr *RedisRepository) Get(key string) (string, error) { return value, err } -func (rr *RedisRepository) Set(key string, value string) error { - err := rr.rds.RedisClient.Set(rr.rds.Ctx, key, value, 0).Err() - return err -} - // set a key's value with expiration -func (rr *RedisRepository) SetEx(key string, value string, expiration time.Duration) error { +func (rr *RedisRepository) Set(key string, value string, expiration time.Duration) error { err := rr.rds.RedisClient.Set(rr.rds.Ctx, key, value, expiration).Err() return err } diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 10023d33..6b582768 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -32,7 +32,7 @@ func (as *AuthService) RegisterUser(userRegister *validators.SignUpUser) error { return fmt.Errorf("failed to generate OTP: %w", err) } - err = as.redisRepo.SetEx("registration-verification-otp-"+userRegister.Email, otp, time.Minute) + err = as.redisRepo.Set("registration-verification-otp-"+userRegister.Email, otp, time.Minute) if err != nil { return fmt.Errorf("failed to save otp in redis: %w", err) } diff --git a/internal/services/session_service.go b/internal/services/session_service.go index 86dccd40..471dc54a 100644 --- a/internal/services/session_service.go +++ b/internal/services/session_service.go @@ -37,7 +37,7 @@ func (ss *SessionService) CreateSession(uuid uuid.UUID) (string, error) { return "", fmt.Errorf("error occured %w", err) } - err = ss.redisRepo.SetEx("dashboard-user-session-"+sessionId, string(userJson), utils.SessionExpiresIn) + err = ss.redisRepo.Set("dashboard-user-session-"+sessionId, string(userJson), utils.SessionExpiresIn) if err != nil { return "", fmt.Errorf("error in setting session %w", err) } @@ -66,7 +66,7 @@ func (ss *SessionService) UpdateSession(sessionId string) error { } return err } - err = ss.redisRepo.SetEx("dashboard-user-session-"+sessionId, val, utils.SessionExpiresIn) + err = ss.redisRepo.Set("dashboard-user-session-"+sessionId, val, utils.SessionExpiresIn) if err != nil { return fmt.Errorf("error in updating session %w", err) } From f34d23adacb77895517d9f4b75df071e5a7ee476 Mon Sep 17 00:00:00 2001 From: Moon Patel Date: Sat, 9 Nov 2024 11:56:52 +0530 Subject: [PATCH 4/4] cleanup --- internal/utils/session_helpers.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/internal/utils/session_helpers.go b/internal/utils/session_helpers.go index b15c9112..e33eca1f 100644 --- a/internal/utils/session_helpers.go +++ b/internal/utils/session_helpers.go @@ -18,18 +18,6 @@ func GenerateSessionID() (string, error) { return base32.StdEncoding.EncodeToString(bytes), nil } -// func ValidateSession( -// sessionID string, -// ttl time.Duration, -// ) error { -// if ttl < SessionExpiresIn { -// if err := sessionService.UpdateSession(sessionID); err != nil { -// return nil, err -// } -// } -// return nil -// } - func SetSessionCookie(c *fiber.Ctx, sessionID string) { c.Cookie(&fiber.Cookie{ Name: "session_id",