first commit
This commit is contained in:
220
app/services/auth_service.py
Normal file
220
app/services/auth_service.py
Normal file
@ -0,0 +1,220 @@
|
||||
"""
|
||||
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)}'}
|
||||
Reference in New Issue
Block a user