feat: hub console backend — complete API surface
This commit is contained in:
105
controllers/otpController.go
Normal file
105
controllers/otpController.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user