""" Authentication Service - Business Logic for User Authentication Handles login, registration, password changes, and admin authentication """ from typing import Optional, Dict, Any from app.core.database import db from models import User, AuditLog class AuthService: """ Authentication service - contains ALL business logic for authentication. Following Python Quick Start Guide: - Service layer contains business rules - No database queries (those go in repository layer - future refactor) - No HTTP/request handling (that stays in endpoints) """ def __init__(self, db_session=None): """Initialize auth service with database session.""" self.db = db_session or db.session def register_user( self, username: str, email: str, name: str, password: str, password_confirm: str, preferred_username: Optional[str] = None ) -> Dict[str, Any]: """ Register a new user - complete workflow. Business Rules: 1. All fields are required 2. Passwords must match 3. Password must be at least 8 characters 4. Username must be unique 5. Email must be unique 6. User starts as active non-admin Args: username: Desired username email: User's email address name: User's full name password: User's password password_confirm: Password confirmation preferred_username: Optional preferred username (defaults to username) Returns: Dict with 'success' (bool) and 'message' or 'error' """ # Business Rule 1: Validate all fields are provided if not all([username, email, name, password, password_confirm]): return {'success': False, 'error': 'Alle Felder sind erforderlich'} # Business Rule 2: Passwords must match if password != password_confirm: return {'success': False, 'error': 'Passwörter stimmen nicht überein'} # Business Rule 3: Password minimum length if len(password) < 8: return {'success': False, 'error': 'Passwort muss mindestens 8 Zeichen lang sein'} # Business Rule 4: Check username uniqueness existing_user = User.query.filter_by(username=username).first() if existing_user: return {'success': False, 'error': 'Username bereits vergeben'} # Business Rule 5: Check email uniqueness existing_email = User.query.filter_by(email=email).first() if existing_email: return {'success': False, 'error': 'Email bereits registriert'} # Create new user (Business Rule 6: Active non-admin by default) user = User( username=username, email=email, name=name, preferred_username=preferred_username or username, is_active=True, is_admin=False ) user.set_password(password) try: self.db.add(user) self.db.commit() return { 'success': True, 'message': 'Registrierung erfolgreich! Du kannst dich jetzt einloggen.', 'user_id': user.id } except Exception as e: self.db.rollback() return {'success': False, 'error': f'Registrierung fehlgeschlagen: {str(e)}'} def authenticate_user( self, username: str, password: str, ip_address: Optional[str] = None, user_agent: Optional[str] = None ) -> Dict[str, Any]: """ Authenticate a user with username and password. Business Rules: 1. Username and password are required 2. User must exist 3. Password must be correct 4. User must be active 5. Log all authentication attempts (success and failure) Args: username: User's username password: User's password ip_address: Client IP address for audit logging user_agent: Client User-Agent for audit logging Returns: Dict with 'success' (bool), 'user' (if successful), or 'error' """ # Business Rule 1: Both fields required if not username or not password: return {'success': False, 'error': 'Username und Password sind erforderlich'} # Business Rule 2: User must exist user = User.query.filter_by(username=username).first() if not user or not user.check_password(password): # Business Rule 5: Log failed login attempt AuditLog.log( action='login_failed', username=username, ip_address=ip_address, user_agent=user_agent, details={'reason': 'invalid_credentials'} ) return {'success': False, 'error': 'Ungültige Credentials'} # Business Rule 4: User must be active if not user.is_active: # Business Rule 5: Log login attempt on inactive account AuditLog.log( action='login_failed', username=username, user_id=user.id, ip_address=ip_address, user_agent=user_agent, details={'reason': 'account_inactive'} ) return {'success': False, 'error': 'Account ist deaktiviert'} # Business Rule 5: Log successful login AuditLog.log( action='login_success', username=user.username, user_id=user.id, ip_address=ip_address, user_agent=user_agent ) return {'success': True, 'user': user} def change_password( self, username: str, current_password: str, new_password: str, new_password_confirm: str ) -> Dict[str, Any]: """ Change user password. Business Rules: 1. All fields are required 2. New passwords must match 3. New password must be at least 8 characters 4. User must exist and be active 5. Current password must be correct Args: username: User's username current_password: Current password for verification new_password: New password new_password_confirm: New password confirmation Returns: Dict with 'success' (bool) and 'message' or 'error' """ # Business Rule 1: All fields required if not all([username, current_password, new_password, new_password_confirm]): return {'success': False, 'error': 'Alle Felder sind erforderlich'} # Business Rule 2: New passwords must match if new_password != new_password_confirm: return {'success': False, 'error': 'Neue Passwörter stimmen nicht überein'} # Business Rule 3: Minimum length if len(new_password) < 8: return {'success': False, 'error': 'Neues Passwort muss mindestens 8 Zeichen lang sein'} # Business Rule 4 & 5: User exists, is active, and current password is correct user = User.query.filter_by(username=username, is_active=True).first() if not user or not user.check_password(current_password): return {'success': False, 'error': 'Ungültiger Username oder Passwort'} # Update password user.set_password(new_password) try: self.db.commit() return {'success': True, 'message': 'Passwort erfolgreich geändert!'} except Exception as e: self.db.rollback() return {'success': False, 'error': f'Passwort-Änderung fehlgeschlagen: {str(e)}'}