[split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/consent-service/internal/middleware"
|
||||
"github.com/breakpilot/consent-service/internal/models"
|
||||
"github.com/breakpilot/consent-service/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -292,366 +291,6 @@ func (h *OAuthHandler) Introspect(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 2FA (TOTP) Endpoints
|
||||
// ========================================
|
||||
|
||||
// Setup2FA initiates 2FA setup
|
||||
// POST /auth/2fa/setup
|
||||
func (h *OAuthHandler) Setup2FA(c *gin.Context) {
|
||||
userID, err := middleware.GetUserID(c)
|
||||
if err != nil || userID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user email
|
||||
ctx := context.Background()
|
||||
user, err := h.authService.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Setup 2FA
|
||||
response, err := h.totpService.Setup2FA(ctx, userID, user.Email)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case services.ErrTOTPAlreadyEnabled:
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "2FA is already enabled for this account"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to setup 2FA"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Verify2FASetup verifies the 2FA setup with a code
|
||||
// POST /auth/2fa/verify-setup
|
||||
func (h *OAuthHandler) Verify2FASetup(c *gin.Context) {
|
||||
userID, err := middleware.GetUserID(c)
|
||||
if err != nil || userID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Verify2FARequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err = h.totpService.Verify2FASetup(ctx, userID, req.Code)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case services.ErrTOTPAlreadyEnabled:
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "2FA is already enabled"})
|
||||
case services.ErrTOTPInvalidCode:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 2FA code"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify 2FA setup"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "2FA enabled successfully"})
|
||||
}
|
||||
|
||||
// Verify2FAChallenge verifies a 2FA challenge during login
|
||||
// POST /auth/2fa/verify
|
||||
func (h *OAuthHandler) Verify2FAChallenge(c *gin.Context) {
|
||||
var req models.Verify2FAChallengeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var userID *uuid.UUID
|
||||
var err error
|
||||
|
||||
if req.RecoveryCode != "" {
|
||||
// Verify with recovery code
|
||||
userID, err = h.totpService.VerifyChallengeWithRecoveryCode(ctx, req.ChallengeID, req.RecoveryCode)
|
||||
} else {
|
||||
// Verify with TOTP code
|
||||
userID, err = h.totpService.VerifyChallenge(ctx, req.ChallengeID, req.Code)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
switch err {
|
||||
case services.ErrTOTPChallengeExpired:
|
||||
c.JSON(http.StatusGone, gin.H{"error": "2FA challenge has expired"})
|
||||
case services.ErrTOTPInvalidCode:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 2FA code"})
|
||||
case services.ErrRecoveryCodeInvalid:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid recovery code"})
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "2FA verification failed"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get user and generate tokens
|
||||
user, err := h.authService.GetUserByID(ctx, *userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate access token
|
||||
accessToken, err := h.authService.GenerateAccessToken(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate refresh token
|
||||
refreshToken, refreshTokenHash, err := h.authService.GenerateRefreshToken()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate refresh token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Store session
|
||||
ipAddress := middleware.GetClientIP(c)
|
||||
userAgent := middleware.GetUserAgent(c)
|
||||
|
||||
// We need direct DB access for this, or we need to add a method to AuthService
|
||||
// For now, we'll return the tokens and let the caller handle session storage
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"user": map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"email": user.Email,
|
||||
"name": user.Name,
|
||||
"role": user.Role,
|
||||
},
|
||||
"_session_hash": refreshTokenHash,
|
||||
"_ip": ipAddress,
|
||||
"_user_agent": userAgent,
|
||||
})
|
||||
}
|
||||
|
||||
// Disable2FA disables 2FA for the current user
|
||||
// POST /auth/2fa/disable
|
||||
func (h *OAuthHandler) Disable2FA(c *gin.Context) {
|
||||
userID, err := middleware.GetUserID(c)
|
||||
if err != nil || userID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Verify2FARequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err = h.totpService.Disable2FA(ctx, userID, req.Code)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case services.ErrTOTPNotEnabled:
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "2FA is not enabled"})
|
||||
case services.ErrTOTPInvalidCode:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 2FA code"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disable 2FA"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "2FA disabled successfully"})
|
||||
}
|
||||
|
||||
// Get2FAStatus returns the 2FA status for the current user
|
||||
// GET /auth/2fa/status
|
||||
func (h *OAuthHandler) Get2FAStatus(c *gin.Context) {
|
||||
userID, err := middleware.GetUserID(c)
|
||||
if err != nil || userID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
status, err := h.totpService.GetStatus(ctx, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get 2FA status"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// RegenerateRecoveryCodes generates new recovery codes
|
||||
// POST /auth/2fa/recovery-codes
|
||||
func (h *OAuthHandler) RegenerateRecoveryCodes(c *gin.Context) {
|
||||
userID, err := middleware.GetUserID(c)
|
||||
if err != nil || userID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Verify2FARequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
codes, err := h.totpService.RegenerateRecoveryCodes(ctx, userID, req.Code)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case services.ErrTOTPNotEnabled:
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "2FA is not enabled"})
|
||||
case services.ErrTOTPInvalidCode:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 2FA code"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to regenerate recovery codes"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"recovery_codes": codes})
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Enhanced Login with 2FA
|
||||
// ========================================
|
||||
|
||||
// LoginWith2FA handles login with optional 2FA
|
||||
// POST /auth/login
|
||||
func (h *OAuthHandler) LoginWith2FA(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ipAddress := middleware.GetClientIP(c)
|
||||
userAgent := middleware.GetUserAgent(c)
|
||||
|
||||
// Attempt login
|
||||
response, err := h.authService.Login(ctx, &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"})
|
||||
case services.ErrAccountSuspended:
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Account is suspended"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Login failed"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if 2FA is enabled
|
||||
twoFactorEnabled, _ := h.totpService.IsTwoFactorEnabled(ctx, response.User.ID)
|
||||
|
||||
if twoFactorEnabled {
|
||||
// Create 2FA challenge
|
||||
challengeID, err := h.totpService.CreateChallenge(ctx, response.User.ID, ipAddress, userAgent)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create 2FA challenge"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return 2FA required response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"requires_2fa": true,
|
||||
"challenge_id": challengeID,
|
||||
"message": "2FA verification required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// No 2FA required, return tokens
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"requires_2fa": false,
|
||||
"access_token": response.AccessToken,
|
||||
"refresh_token": response.RefreshToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": response.ExpiresIn,
|
||||
"user": map[string]interface{}{
|
||||
"id": response.User.ID,
|
||||
"email": response.User.Email,
|
||||
"name": response.User.Name,
|
||||
"role": response.User.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Registration with mandatory 2FA setup
|
||||
// ========================================
|
||||
|
||||
// RegisterWith2FA handles registration with mandatory 2FA setup
|
||||
// POST /auth/register
|
||||
func (h *OAuthHandler) RegisterWith2FA(c *gin.Context) {
|
||||
var req models.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Validate password strength
|
||||
if len(req.Password) < 8 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Password must be at least 8 characters"})
|
||||
return
|
||||
}
|
||||
|
||||
// Register user
|
||||
user, verificationToken, err := h.authService.Register(ctx, &req)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case services.ErrUserExists:
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "A user with this email already exists"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Registration failed"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Setup 2FA immediately
|
||||
twoFAResponse, err := h.totpService.Setup2FA(ctx, user.ID, user.Email)
|
||||
if err != nil {
|
||||
// Non-fatal - user can set up 2FA later, but log it
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Registration successful. Please verify your email.",
|
||||
"user_id": user.ID,
|
||||
"verification_token": verificationToken, // In production, this would be sent via email
|
||||
"two_factor_setup": nil,
|
||||
"two_factor_error": "Failed to initialize 2FA. Please set it up in your account settings.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Registration successful. Please verify your email and complete 2FA setup.",
|
||||
"user_id": user.ID,
|
||||
"verification_token": verificationToken, // In production, this would be sent via email
|
||||
"two_factor_setup": map[string]interface{}{
|
||||
"secret": twoFAResponse.Secret,
|
||||
"qr_code": twoFAResponse.QRCodeDataURL,
|
||||
"recovery_codes": twoFAResponse.RecoveryCodes,
|
||||
"setup_required": true,
|
||||
"setup_endpoint": "/auth/2fa/verify-setup",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OAuth Client Management (Admin)
|
||||
// ========================================
|
||||
|
||||
Reference in New Issue
Block a user