package handlers import ( "fmt" "net/http" "strings" "time" "github.com/gin-gonic/gin" "sproutgate-backend/internal/auth" "sproutgate-backend/internal/email" "sproutgate-backend/internal/models" ) func (h *Handler) RequestSecondaryEmail(c *gin.Context) { token := bearerToken(c.GetHeader("Authorization")) if token == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"}) return } claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), token) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } var req secondaryEmailRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } emailAddr := strings.TrimSpace(req.Email) if emailAddr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "email is required"}) return } user, found, err := h.store.GetUser(claims.Account) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"}) return } if !found { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) return } if abortIfUserBanned(c, user) { return } if strings.TrimSpace(user.Email) == emailAddr { c.JSON(http.StatusBadRequest, gin.H{"error": "email already used as primary"}) return } for _, e := range user.SecondaryEmails { if e == emailAddr { c.JSON(http.StatusBadRequest, gin.H{"error": "email already verified"}) return } } code, err := generateVerificationCode() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate code"}) return } expiresAt := time.Now().Add(10 * time.Minute) record := models.SecondaryEmailVerification{ Account: user.Account, Email: emailAddr, CodeHash: hashCode(code), CreatedAt: models.NowISO(), ExpiresAt: expiresAt.Format(time.RFC3339), } if err := h.store.SaveSecondaryVerification(record); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save verification"}) return } if err := email.SendVerificationEmail(h.store.EmailConfig(), emailAddr, code, 10*time.Minute); err != nil { _ = h.store.DeleteSecondaryVerification(user.Account, emailAddr) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to send email: %v", err)}) return } c.JSON(http.StatusOK, gin.H{ "sent": true, "expiresAt": expiresAt.Format(time.RFC3339), }) } func (h *Handler) VerifySecondaryEmail(c *gin.Context) { token := bearerToken(c.GetHeader("Authorization")) if token == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"}) return } claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), token) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } var req verifySecondaryEmailRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } emailAddr := strings.TrimSpace(req.Email) code := strings.TrimSpace(req.Code) if emailAddr == "" || code == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "email and code are required"}) return } record, found, err := h.store.GetSecondaryVerification(claims.Account, emailAddr) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load verification"}) return } if !found { c.JSON(http.StatusBadRequest, gin.H{"error": "verification not found"}) return } expiresAt, err := time.Parse(time.RFC3339, record.ExpiresAt) if err != nil || time.Now().After(expiresAt) { _ = h.store.DeleteSecondaryVerification(claims.Account, emailAddr) c.JSON(http.StatusBadRequest, gin.H{"error": "verification code expired"}) return } if !verifyCode(code, record.CodeHash) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification code"}) return } user, found, err := h.store.GetUser(claims.Account) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"}) return } if !found { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) return } if abortIfUserBanned(c, user) { return } for _, e := range user.SecondaryEmails { if e == emailAddr { _ = h.store.DeleteSecondaryVerification(claims.Account, emailAddr) c.JSON(http.StatusOK, gin.H{"verified": true, "user": user.OwnerPublic()}) return } } user.SecondaryEmails = append(user.SecondaryEmails, emailAddr) if err := h.store.SaveUser(user); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save user"}) return } _ = h.store.DeleteSecondaryVerification(claims.Account, emailAddr) c.JSON(http.StatusOK, gin.H{"verified": true, "user": user.OwnerPublic()}) }