545 lines
15 KiB
Go
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
|
|
} |