221 lines
7.6 KiB
Python
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)}'}
|