-
This commit is contained in:
545
user/internal/handlers/auth_handler.go
Normal file
545
user/internal/handlers/auth_handler.go
Normal file
@ -0,0 +1,545 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user