Files
oicd/app/services/auth_service.py
2025-11-30 00:07:24 +01:00

221 lines
7.6 KiB
Python

"""
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)}'}