[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:
@@ -383,186 +383,3 @@ func (s *AuthService) VerifyEmail(ctx context.Context, token string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePasswordResetToken creates a password reset token
|
||||
func (s *AuthService) CreatePasswordResetToken(ctx context.Context, email, ipAddress string) (string, *uuid.UUID, error) {
|
||||
var userID uuid.UUID
|
||||
err := s.db.QueryRow(ctx, "SELECT id FROM users WHERE email = $1", email).Scan(&userID)
|
||||
if err != nil {
|
||||
// Don't reveal if user exists
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
token, err := s.GenerateSecureToken(32)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(ctx, `
|
||||
INSERT INTO password_reset_tokens (user_id, token, expires_at, ip_address, created_at)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
`, userID, token, time.Now().Add(time.Hour), ipAddress)
|
||||
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create reset token: %w", err)
|
||||
}
|
||||
|
||||
return token, &userID, nil
|
||||
}
|
||||
|
||||
// ResetPassword resets a user's password using a reset token
|
||||
func (s *AuthService) ResetPassword(ctx context.Context, token, newPassword string) error {
|
||||
var tokenID uuid.UUID
|
||||
var userID uuid.UUID
|
||||
var expiresAt time.Time
|
||||
var usedAt *time.Time
|
||||
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT id, user_id, expires_at, used_at FROM password_reset_tokens
|
||||
WHERE token = $1
|
||||
`, token).Scan(&tokenID, &userID, &expiresAt, &usedAt)
|
||||
|
||||
if err != nil {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
if usedAt != nil || expiresAt.Before(time.Now()) {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
passwordHash, err := s.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Mark token as used
|
||||
_, err = s.db.Exec(ctx, `UPDATE password_reset_tokens SET used_at = NOW() WHERE id = $1`, tokenID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update token: %w", err)
|
||||
}
|
||||
|
||||
// Update password
|
||||
_, err = s.db.Exec(ctx, `
|
||||
UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2
|
||||
`, passwordHash, userID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
// Revoke all sessions for security
|
||||
_, err = s.db.Exec(ctx, `UPDATE user_sessions SET revoked_at = NOW() WHERE user_id = $1 AND revoked_at IS NULL`, userID)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to revoke sessions: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangePassword changes a user's password (requires current password)
|
||||
func (s *AuthService) ChangePassword(ctx context.Context, userID uuid.UUID, currentPassword, newPassword string) error {
|
||||
var passwordHash *string
|
||||
err := s.db.QueryRow(ctx, "SELECT password_hash FROM users WHERE id = $1", userID).Scan(&passwordHash)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
if passwordHash == nil || !s.VerifyPassword(currentPassword, *passwordHash) {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
newPasswordHash, err := s.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(ctx, `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, newPasswordHash, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserByID retrieves a user by ID
|
||||
func (s *AuthService) GetUserByID(ctx context.Context, userID uuid.UUID) (*models.User, error) {
|
||||
var user models.User
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT id, email, name, role, email_verified, email_verified_at, account_status,
|
||||
last_login_at, created_at, updated_at
|
||||
FROM users WHERE id = $1
|
||||
`, userID).Scan(
|
||||
&user.ID, &user.Email, &user.Name, &user.Role, &user.EmailVerified, &user.EmailVerifiedAt,
|
||||
&user.AccountStatus, &user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateProfile updates a user's profile
|
||||
func (s *AuthService) UpdateProfile(ctx context.Context, userID uuid.UUID, req *models.UpdateProfileRequest) (*models.User, error) {
|
||||
_, err := s.db.Exec(ctx, `UPDATE users SET name = $1, updated_at = NOW() WHERE id = $2`, req.Name, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update profile: %w", err)
|
||||
}
|
||||
|
||||
return s.GetUserByID(ctx, userID)
|
||||
}
|
||||
|
||||
// GetActiveSessions retrieves all active sessions for a user
|
||||
func (s *AuthService) GetActiveSessions(ctx context.Context, userID uuid.UUID) ([]models.UserSession, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, user_id, device_info, ip_address, user_agent, expires_at, created_at, last_activity_at
|
||||
FROM user_sessions
|
||||
WHERE user_id = $1 AND revoked_at IS NULL AND expires_at > NOW()
|
||||
ORDER BY last_activity_at DESC
|
||||
`, userID)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sessions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sessions []models.UserSession
|
||||
for rows.Next() {
|
||||
var session models.UserSession
|
||||
err := rows.Scan(
|
||||
&session.ID, &session.UserID, &session.DeviceInfo, &session.IPAddress,
|
||||
&session.UserAgent, &session.ExpiresAt, &session.CreatedAt, &session.LastActivityAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan session: %w", err)
|
||||
}
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// RevokeSession revokes a specific session
|
||||
func (s *AuthService) RevokeSession(ctx context.Context, userID, sessionID uuid.UUID) error {
|
||||
result, err := s.db.Exec(ctx, `
|
||||
UPDATE user_sessions SET revoked_at = NOW() WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
|
||||
`, sessionID, userID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke session: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return errors.New("session not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout revokes a session by refresh token
|
||||
func (s *AuthService) Logout(ctx context.Context, refreshToken string) error {
|
||||
tokenHash := s.HashToken(refreshToken)
|
||||
_, err := s.db.Exec(ctx, `UPDATE user_sessions SET revoked_at = NOW() WHERE token_hash = $1`, tokenHash)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user