veza/veza-backend-api/internal/handlers/password_reset_handler.go

185 lines
5.7 KiB
Go

package handlers
import (
"net/http"
"veza-backend-api/internal/core/auth" // Added import for authcore
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// RequestPasswordResetRequest represents a request to reset password
// T0193: Request structure for password reset endpoint
type RequestPasswordResetRequest struct {
Email string `json:"email" binding:"required,email"`
}
// RequestPasswordReset handles password reset request
// T0193: Creates endpoint POST /api/v1/auth/password/reset-request
func RequestPasswordReset(
passwordResetService *services.PasswordResetService,
passwordService *services.PasswordService,
emailService *services.EmailService,
logger *zap.Logger,
) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
var req RequestPasswordResetRequest
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// Find user by email
user, err := passwordService.GetUserByEmail(req.Email)
if err != nil {
// Always return success for security (prevent email enumeration)
RespondSuccess(c, http.StatusOK, gin.H{"message": "If the email exists, a reset link has been sent"})
return
}
// Invalidate old tokens
if err := passwordResetService.InvalidateOldTokens(user.ID); err != nil {
logger.Error("Failed to invalidate old tokens",
zap.String("user_id", user.ID.String()),
zap.Error(err),
)
// Continue anyway, not critical
}
// Generate token
token, err := passwordResetService.GenerateToken()
if err != nil {
logger.Error("Failed to generate password reset token",
zap.String("user_id", user.ID.String()),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
// Store token
if err := passwordResetService.StoreToken(user.ID, token); err != nil {
logger.Error("Failed to store password reset token",
zap.String("user_id", user.ID.String()),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store token"})
return
}
// Send email
if err := emailService.SendPasswordResetEmail(user.ID, user.Email, token); err != nil {
// Log but don't fail - user should still get success message
logger.Error("Failed to send password reset email",
zap.String("user_id", user.ID.String()),
zap.String("email", user.Email),
zap.Error(err),
)
}
// Always return generic success message for security
RespondSuccess(c, http.StatusOK, gin.H{"message": "If the email exists, a reset link has been sent"})
}
}
// ResetPasswordRequest represents a request to complete password reset
// T0194: Request structure for password reset completion
type ResetPasswordRequest struct {
Token string `json:"token" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
// ResetPassword handles password reset completion
// T0194: Creates endpoint POST /api/v1/auth/password/reset
// T0200: Uses AuthService.InvalidateAllUserSessions to invalidate sessions and update token_version
func ResetPassword(
passwordResetService *services.PasswordResetService,
passwordService *services.PasswordService,
authService *auth.AuthService, // Changed to *auth.AuthService
sessionService *services.SessionService,
logger *zap.Logger,
) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
var req ResetPasswordRequest
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// Verify token
userID, err := passwordResetService.VerifyToken(req.Token)
if err != nil {
logger.Warn("Password reset token verification failed",
zap.String("token", req.Token[:min(len(req.Token), 8)]+"..."),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or expired token"})
return
}
// Validate password strength
if err := passwordService.ValidatePassword(req.NewPassword); err != nil {
logger.Warn("Password validation failed",
zap.String("user_id", userID.String()),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update password
if err := passwordService.UpdatePassword(userID, req.NewPassword); err != nil {
logger.Error("Failed to update password",
zap.String("user_id", userID.String()),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update password"})
return
}
// Mark token as used
if err := passwordResetService.MarkTokenAsUsed(req.Token); err != nil {
// Log but don't fail - password is already updated
logger.Warn("Failed to mark token as used",
zap.String("user_id", userID.String()),
zap.String("token", req.Token[:min(len(req.Token), 8)]+"..."),
zap.Error(err),
)
}
// T0200: Invalidate all user sessions via AuthService
// This updates token_version and revokes all sessions
if authService != nil {
err := authService.InvalidateAllUserSessions(c.Request.Context(), userID, sessionService)
if err != nil {
// Log but don't fail - password is already updated
logger.Warn("Failed to invalidate user sessions",
zap.String("user_id", userID.String()),
zap.Error(err),
)
} else {
logger.Info("User sessions invalidated after password reset",
zap.String("user_id", userID.String()),
)
}
}
logger.Info("Password reset completed successfully",
zap.String("user_id", userID.String()),
)
RespondSuccess(c, http.StatusOK, gin.H{"message": "Password reset successfully"})
}
}
// min returns the minimum of two integers (helper function)
func min(a, b int) int {
if a < b {
return a
}
return b
}