package handlers import ( "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/breakpilot/consent-service/internal/models" "github.com/breakpilot/consent-service/internal/services" ) // AuthHandler handles authentication endpoints type AuthHandler struct { authService *services.AuthService emailService *services.EmailService } // NewAuthHandler creates a new AuthHandler func NewAuthHandler(authService *services.AuthService, emailService *services.EmailService) *AuthHandler { return &AuthHandler{ authService: authService, emailService: emailService, } } // Register handles user registration // @Summary Register a new user // @Tags auth // @Accept json // @Produce json // @Param request body models.RegisterRequest true "Registration data" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /auth/register [post] func (h *AuthHandler) Register(c *gin.Context) { var req models.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()}) return } user, verificationToken, err := h.authService.Register(c.Request.Context(), &req) if err != nil { if err == services.ErrUserExists { c.JSON(http.StatusConflict, gin.H{"error": "User with this email already exists"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to register user"}) return } // Send verification email (async, don't block response) go func() { var name string if user.Name != nil { name = *user.Name } if err := h.emailService.SendVerificationEmail(user.Email, name, verificationToken); err != nil { // Log error but don't fail registration println("Failed to send verification email:", err.Error()) } }() c.JSON(http.StatusCreated, gin.H{ "message": "Registration successful. Please check your email to verify your account.", "user": gin.H{ "id": user.ID, "email": user.Email, "name": user.Name, }, }) } // Login handles user login // @Summary Login user // @Tags auth // @Accept json // @Produce json // @Param request body models.LoginRequest true "Login credentials" // @Success 200 {object} models.LoginResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /auth/login [post] func (h *AuthHandler) Login(c *gin.Context) { var req models.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()}) return } ipAddress := c.ClientIP() userAgent := c.Request.UserAgent() response, err := h.authService.Login(c.Request.Context(), &req, ipAddress, userAgent) if err != nil { switch err { case services.ErrInvalidCredentials: c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"}) case services.ErrAccountLocked: c.JSON(http.StatusForbidden, gin.H{"error": "Account is temporarily locked. Please try again later."}) case services.ErrAccountSuspended: c.JSON(http.StatusForbidden, gin.H{ "error": "Account is suspended", "reason": "consent_required", "redirect": "/consent/pending", }) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "Login failed"}) } return } c.JSON(http.StatusOK, response) } // Logout handles user logout // @Summary Logout user // @Tags auth // @Accept json // @Produce json // @Param Authorization header string true "Bearer token" // @Success 200 {object} map[string]string // @Router /auth/logout [post] func (h *AuthHandler) Logout(c *gin.Context) { var req struct { RefreshToken string `json:"refresh_token"` } if err := c.ShouldBindJSON(&req); err == nil && req.RefreshToken != "" { _ = h.authService.Logout(c.Request.Context(), req.RefreshToken) } c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"}) } // RefreshToken refreshes the access token // @Summary Refresh access token // @Tags auth // @Accept json // @Produce json // @Param request body models.RefreshTokenRequest true "Refresh token" // @Success 200 {object} models.LoginResponse // @Failure 401 {object} map[string]string // @Router /auth/refresh [post] func (h *AuthHandler) RefreshToken(c *gin.Context) { var req models.RefreshTokenRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } response, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken) if err != nil { if err == services.ErrAccountSuspended { c.JSON(http.StatusForbidden, gin.H{ "error": "Account is suspended", "reason": "consent_required", "redirect": "/consent/pending", }) return } c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired refresh token"}) return } c.JSON(http.StatusOK, response) } // VerifyEmail verifies user email // @Summary Verify email address // @Tags auth // @Accept json // @Produce json // @Param request body models.VerifyEmailRequest true "Verification token" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Router /auth/verify-email [post] func (h *AuthHandler) VerifyEmail(c *gin.Context) { var req models.VerifyEmailRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } if err := h.authService.VerifyEmail(c.Request.Context(), req.Token); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired verification token"}) return } c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully. You can now log in."}) } // ResendVerification resends verification email // @Summary Resend verification email // @Tags auth // @Accept json // @Produce json // @Param request body map[string]string true "Email" // @Success 200 {object} map[string]string // @Router /auth/resend-verification [post] func (h *AuthHandler) ResendVerification(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } // Always return success to prevent email enumeration c.JSON(http.StatusOK, gin.H{"message": "If an account exists with this email, a verification email has been sent."}) } // ForgotPassword initiates password reset // @Summary Request password reset // @Tags auth // @Accept json // @Produce json // @Param request body models.ForgotPasswordRequest true "Email" // @Success 200 {object} map[string]string // @Router /auth/forgot-password [post] func (h *AuthHandler) ForgotPassword(c *gin.Context) { var req models.ForgotPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } token, userID, err := h.authService.CreatePasswordResetToken(c.Request.Context(), req.Email, c.ClientIP()) if err == nil && userID != nil { // Send email asynchronously go func() { _ = h.emailService.SendPasswordResetEmail(req.Email, "", token) }() } // Always return success to prevent email enumeration c.JSON(http.StatusOK, gin.H{"message": "If an account exists with this email, a password reset link has been sent."}) } // ResetPassword resets password with token // @Summary Reset password // @Tags auth // @Accept json // @Produce json // @Param request body models.ResetPasswordRequest true "Reset token and new password" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Router /auth/reset-password [post] func (h *AuthHandler) ResetPassword(c *gin.Context) { var req models.ResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()}) return } if err := h.authService.ResetPassword(c.Request.Context(), req.Token, req.NewPassword); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired reset token"}) return } c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully. You can now log in with your new password."}) } // GetProfile returns the current user's profile // @Summary Get user profile // @Tags profile // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} models.User // @Failure 401 {object} map[string]string // @Router /profile [get] func (h *AuthHandler) GetProfile(c *gin.Context) { userIDStr, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userID, err := uuid.Parse(userIDStr.(string)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID"}) return } user, err := h.authService.GetUserByID(c.Request.Context(), userID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } c.JSON(http.StatusOK, user) } // UpdateProfile updates the current user's profile // @Summary Update user profile // @Tags profile // @Accept json // @Produce json // @Security BearerAuth // @Param request body models.UpdateProfileRequest true "Profile data" // @Success 200 {object} models.User // @Failure 400 {object} map[string]string // @Router /profile [put] func (h *AuthHandler) UpdateProfile(c *gin.Context) { userIDStr, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userID, err := uuid.Parse(userIDStr.(string)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID"}) return } var req models.UpdateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } user, err := h.authService.UpdateProfile(c.Request.Context(), userID, &req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) return } c.JSON(http.StatusOK, user) } // ChangePassword changes the current user's password // @Summary Change password // @Tags profile // @Accept json // @Produce json // @Security BearerAuth // @Param request body models.ChangePasswordRequest true "Password data" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Router /profile/password [put] func (h *AuthHandler) ChangePassword(c *gin.Context) { userIDStr, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userID, err := uuid.Parse(userIDStr.(string)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID"}) return } var req models.ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()}) return } if err := h.authService.ChangePassword(c.Request.Context(), userID, req.CurrentPassword, req.NewPassword); err != nil { if err == services.ErrInvalidCredentials { c.JSON(http.StatusBadRequest, gin.H{"error": "Current password is incorrect"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to change password"}) return } c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"}) } // GetActiveSessions returns all active sessions for the current user // @Summary Get active sessions // @Tags profile // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {array} models.UserSession // @Router /profile/sessions [get] func (h *AuthHandler) GetActiveSessions(c *gin.Context) { userIDStr, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userID, err := uuid.Parse(userIDStr.(string)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID"}) return } sessions, err := h.authService.GetActiveSessions(c.Request.Context(), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get sessions"}) return } c.JSON(http.StatusOK, gin.H{"sessions": sessions}) } // RevokeSession revokes a specific session // @Summary Revoke session // @Tags profile // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Session ID" // @Success 200 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /profile/sessions/{id} [delete] func (h *AuthHandler) RevokeSession(c *gin.Context) { userIDStr, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userID, err := uuid.Parse(userIDStr.(string)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID"}) return } sessionID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid session ID"}) return } if err := h.authService.RevokeSession(c.Request.Context(), userID, sessionID); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "Session revoked successfully"}) }