package controllers import ( "context" "crypto/rand" "fmt" "math/big" "time" "doormile/config" "doormile/db" "doormile/dto" "doormile/internal/mail" "doormile/utils" "github.com/gofiber/fiber/v2" "github.com/redis/go-redis/v9" ) const ( otpTTL = 5 * time.Minute otpMaxAttempts = 5 ) func generateOtpCode() string { n, err := rand.Int(rand.Reader, big.NewInt(1000000)) if err != nil { return "000000" } return fmt.Sprintf("%06d", n.Int64()) } func otpKey(email string) string { return fmt.Sprintf("otp:email:%s", email) } func otpAttemptsKey(email string) string { return fmt.Sprintf("otp:email:%s:attempts", email) } func SendCustomerEmailOtp(cfg *config.Config) fiber.Handler { return func(c *fiber.Ctx) error { req := new(dto.SendEmailOtpRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Email == "" { return utils.BadRequest(c, "email is required") } if db.Rdb == nil { return utils.Internal(c, "verification service unavailable") } code := generateOtpCode() ctx := context.Background() if err := db.Rdb.Set(ctx, otpKey(req.Email), code, otpTTL).Err(); err != nil { return utils.Internal(c, "failed to generate verification code") } db.Rdb.Del(ctx, otpAttemptsKey(req.Email)) if err := mail.SendOTPEmail(cfg, req.Email, code); err != nil { utils.Warn("failed to send OTP email", "email", req.Email, "error", err) return utils.Internal(c, "failed to send verification email") } return utils.Message(c, "verification code sent") } } func VerifyCustomerEmailOtp() fiber.Handler { return func(c *fiber.Ctx) error { req := new(dto.VerifyEmailOtpRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Email == "" || req.Otp == "" { return utils.BadRequest(c, "email and otp are required") } if db.Rdb == nil { return utils.Internal(c, "verification service unavailable") } ctx := context.Background() key := otpKey(req.Email) stored, err := db.Rdb.Get(ctx, key).Result() if err == redis.Nil { return utils.BadRequest(c, "verification code expired or not found, please resend") } else if err != nil { return utils.Internal(c, "failed to verify code") } if stored != req.Otp { attemptsKey := otpAttemptsKey(req.Email) attempts, _ := db.Rdb.Incr(ctx, attemptsKey).Result() db.Rdb.Expire(ctx, attemptsKey, otpTTL) if attempts >= otpMaxAttempts { db.Rdb.Del(ctx, key, attemptsKey) return utils.BadRequest(c, "too many incorrect attempts, please request a new code") } return utils.Unauthorized(c, "incorrect verification code") } db.Rdb.Del(ctx, key, otpAttemptsKey(req.Email)) return utils.Message(c, "email verified successfully") } }