Files
skybridge/user/internal/handlers/auth_handler.go
2025-09-01 18:26:44 -04:00

545 lines
15 KiB
Go

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
}