package handlers import ( "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "github.com/RyanCopley/skybridge/user/internal/domain" "github.com/RyanCopley/skybridge/user/internal/services" ) // AuthHandler handles HTTP requests for authentication operations type AuthHandler struct { authService services.AuthService userService services.UserService logger *zap.Logger } // NewAuthHandler creates a new authentication handler func NewAuthHandler(authService services.AuthService, userService services.UserService, logger *zap.Logger) *AuthHandler { return &AuthHandler{ authService: authService, userService: userService, logger: logger, } } // Register handles POST /auth/register func (h *AuthHandler) Register(c *gin.Context) { var req domain.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid registration request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } response, err := h.authService.Register(c.Request.Context(), &req, getIPAddress(c), c.GetHeader("User-Agent")) if err != nil { h.logger.Error("Registration failed", zap.String("email", req.Email), zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Registration failed", "details": err.Error(), }) return } c.JSON(http.StatusCreated, response) } // Login handles POST /auth/login func (h *AuthHandler) Login(c *gin.Context) { var req domain.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid login request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } response, err := h.authService.Login(c.Request.Context(), &req, getIPAddress(c), c.GetHeader("User-Agent")) if err != nil { h.logger.Error("Login failed", zap.String("email", req.Email), zap.Error(err)) c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication failed", "details": err.Error(), }) return } c.JSON(http.StatusOK, response) } // Logout handles POST /auth/logout func (h *AuthHandler) Logout(c *gin.Context) { token := getTokenFromHeader(c) if token == "" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Authorization header required", }) return } err := h.authService.Logout(c.Request.Context(), token) if err != nil { h.logger.Error("Logout failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Logout failed", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Successfully logged out", }) } // RefreshToken handles POST /auth/refresh func (h *AuthHandler) RefreshToken(c *gin.Context) { token := getTokenFromHeader(c) if token == "" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Authorization header required", }) return } response, err := h.authService.RefreshToken(c.Request.Context(), token) if err != nil { h.logger.Error("Token refresh failed", zap.Error(err)) c.JSON(http.StatusUnauthorized, gin.H{ "error": "Token refresh failed", "details": err.Error(), }) return } c.JSON(http.StatusOK, response) } // ForgotPassword handles POST /auth/forgot-password func (h *AuthHandler) ForgotPassword(c *gin.Context) { var req domain.ForgotPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid forgot password request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } err := h.authService.ForgotPassword(c.Request.Context(), &req, getIPAddress(c), c.GetHeader("User-Agent")) if err != nil { h.logger.Error("Forgot password failed", zap.String("email", req.Email), zap.Error(err)) // Don't reveal whether email exists for security c.JSON(http.StatusOK, gin.H{ "message": "If the email exists, password reset instructions have been sent", }) return } c.JSON(http.StatusOK, gin.H{ "message": "Password reset instructions have been sent to your email", }) } // ResetPassword handles POST /auth/reset-password func (h *AuthHandler) ResetPassword(c *gin.Context) { var req domain.ResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid reset password request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } err := h.authService.ResetPassword(c.Request.Context(), &req, getIPAddress(c), c.GetHeader("User-Agent")) if err != nil { h.logger.Error("Password reset failed", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Password reset failed", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Password has been successfully reset", }) } // ChangePassword handles POST /auth/change-password func (h *AuthHandler) ChangePassword(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } var req domain.ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid change password request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } actorID := getActorFromContext(c) err := h.authService.ChangePassword(c.Request.Context(), userID, &req, actorID) if err != nil { h.logger.Error("Change password failed", zap.String("user_id", userID.String()), zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Change password failed", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Password has been successfully changed", }) } // VerifyEmail handles POST /auth/verify-email func (h *AuthHandler) VerifyEmail(c *gin.Context) { var req domain.VerifyEmailRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid email verification request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } err := h.authService.VerifyEmail(c.Request.Context(), &req, getIPAddress(c), c.GetHeader("User-Agent")) if err != nil { h.logger.Error("Email verification failed", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Email verification failed", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Email has been successfully verified", }) } // ResendVerification handles POST /auth/resend-verification func (h *AuthHandler) ResendVerification(c *gin.Context) { var req domain.ResendVerificationRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid resend verification request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } err := h.authService.ResendVerification(c.Request.Context(), &req, getIPAddress(c), c.GetHeader("User-Agent")) if err != nil { h.logger.Error("Resend verification failed", zap.String("email", req.Email), zap.Error(err)) // Don't reveal whether email exists c.JSON(http.StatusOK, gin.H{ "message": "If the email exists and is not verified, verification instructions have been sent", }) return } c.JSON(http.StatusOK, gin.H{ "message": "Verification email has been sent", }) } // Two-Factor Authentication endpoints // SetupTwoFactor handles GET /auth/2fa/setup func (h *AuthHandler) SetupTwoFactor(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } response, err := h.authService.SetupTwoFactor(c.Request.Context(), userID) if err != nil { h.logger.Error("Setup 2FA failed", zap.String("user_id", userID.String()), zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to setup two-factor authentication", "details": err.Error(), }) return } c.JSON(http.StatusOK, response) } // EnableTwoFactor handles POST /auth/2fa/enable func (h *AuthHandler) EnableTwoFactor(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } var req domain.EnableTwoFactorRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid enable 2FA request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } actorID := getActorFromContext(c) err := h.authService.EnableTwoFactor(c.Request.Context(), userID, &req, actorID) if err != nil { h.logger.Error("Enable 2FA failed", zap.String("user_id", userID.String()), zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Failed to enable two-factor authentication", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Two-factor authentication has been enabled", }) } // DisableTwoFactor handles POST /auth/2fa/disable func (h *AuthHandler) DisableTwoFactor(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } var req domain.DisableTwoFactorRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid disable 2FA request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } actorID := getActorFromContext(c) err := h.authService.DisableTwoFactor(c.Request.Context(), userID, &req, actorID) if err != nil { h.logger.Error("Disable 2FA failed", zap.String("user_id", userID.String()), zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Failed to disable two-factor authentication", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Two-factor authentication has been disabled", }) } // ValidateTwoFactor handles POST /auth/2fa/validate func (h *AuthHandler) ValidateTwoFactor(c *gin.Context) { var req domain.ValidateTwoFactorRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid 2FA validation request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } response, err := h.authService.ValidateTwoFactor(c.Request.Context(), &req, getIPAddress(c), c.GetHeader("User-Agent")) if err != nil { h.logger.Error("2FA validation failed", zap.Error(err)) c.JSON(http.StatusUnauthorized, gin.H{ "error": "Two-factor authentication validation failed", "details": err.Error(), }) return } c.JSON(http.StatusOK, response) } // RegenerateTwoFactorBackupCodes handles POST /auth/2fa/regenerate-codes func (h *AuthHandler) RegenerateTwoFactorBackupCodes(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } actorID := getActorFromContext(c) codes, err := h.authService.RegenerateTwoFactorBackupCodes(c.Request.Context(), userID, actorID) if err != nil { h.logger.Error("Regenerate backup codes failed", zap.String("user_id", userID.String()), zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to regenerate backup codes", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "backup_codes": codes, }) } // Session Management endpoints // GetSessions handles GET /auth/sessions func (h *AuthHandler) GetSessions(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } response, err := h.authService.GetUserSessions(c.Request.Context(), userID) if err != nil { h.logger.Error("Get sessions failed", zap.String("user_id", userID.String()), zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get sessions", "details": err.Error(), }) return } c.JSON(http.StatusOK, response) } // RevokeSession handles DELETE /auth/sessions/:sessionId func (h *AuthHandler) RevokeSession(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } sessionIDParam := c.Param("sessionId") sessionID, err := uuid.Parse(sessionIDParam) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid session ID", }) return } actorID := getActorFromContext(c) err = h.authService.RevokeSession(c.Request.Context(), userID, sessionID, actorID) if err != nil { h.logger.Error("Revoke session failed", zap.String("user_id", userID.String()), zap.String("session_id", sessionID.String()), zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to revoke session", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Session has been revoked", }) } // RevokeAllSessions handles DELETE /auth/sessions func (h *AuthHandler) RevokeAllSessions(c *gin.Context) { userID := getUserIDFromContext(c) if userID == uuid.Nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", }) return } actorID := getActorFromContext(c) err := h.authService.RevokeAllSessions(c.Request.Context(), userID, actorID) if err != nil { h.logger.Error("Revoke all sessions failed", zap.String("user_id", userID.String()), zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to revoke all sessions", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "All sessions have been revoked", }) } // Helper functions func getTokenFromHeader(c *gin.Context) string { authHeader := c.GetHeader("Authorization") if authHeader == "" { return "" } // Remove "Bearer " prefix if len(authHeader) > 7 && authHeader[:7] == "Bearer " { return authHeader[7:] } return authHeader } func getIPAddress(c *gin.Context) string { // Check for forwarded IP addresses if forwarded := c.GetHeader("X-Forwarded-For"); forwarded != "" { return forwarded } if realIP := c.GetHeader("X-Real-IP"); realIP != "" { return realIP } return c.ClientIP() } func getUserIDFromContext(c *gin.Context) uuid.UUID { if userID, exists := c.Get("user_id"); exists { if id, ok := userID.(uuid.UUID); ok { return id } if idStr, ok := userID.(string); ok { if id, err := uuid.Parse(idStr); err == nil { return id } } } return uuid.Nil }