first commit

This commit is contained in:
2025-11-30 00:07:24 +01:00
commit b5e642aecb
78 changed files with 15162 additions and 0 deletions

View File

@ -0,0 +1,409 @@
"""
User Service - Business Logic for User Management
Handles user CRUD operations, profile management, and user administration
"""
from typing import Optional, Dict, Any, List
from app.core.database import db
from models import User, AuditLog
from app.repositories import UserRepository
import json
class UserService:
"""
User service - contains ALL business logic for user management.
Following Python Quick Start Guide:
- Service layer contains business rules
- Uses repository layer for database operations
- No HTTP/request handling (that stays in endpoints)
"""
def __init__(self, db_session=None):
"""Initialize user service with database session."""
self.db = db_session or db.session
self.user_repo = UserRepository(db_session)
def get_user_by_id(self, user_id: int) -> Optional[User]:
"""
Get user by ID.
Business Rules:
1. User must exist
2. Return None if not found (don't expose deleted users)
Args:
user_id: User's ID
Returns:
User object or None
"""
user = User.query.get(user_id)
return user if user else None
def get_all_users(self, page: int = 1, per_page: int = 50) -> Dict[str, Any]:
"""
Get all users with pagination.
Args:
page: Page number (1-indexed)
per_page: Items per page
Returns:
Dict with 'users' list and pagination info
"""
pagination = User.query.order_by(User.id.desc()).paginate(
page=page,
per_page=per_page,
error_out=False
)
return {
'users': pagination.items,
'total': pagination.total,
'page': pagination.page,
'per_page': pagination.per_page,
'pages': pagination.pages
}
def get_user_statistics(self) -> Dict[str, int]:
"""
Get user statistics.
Returns:
Dict with counts for total, active, inactive, and admin users
"""
total_users = User.query.count()
active_users = User.query.filter_by(is_active=True).count()
inactive_users = total_users - active_users
admin_users = User.query.filter_by(is_admin=True).count()
return {
'total_users': total_users,
'active_users': active_users,
'inactive_users': inactive_users,
'admin_users': admin_users
}
def create_user(
self,
username: str,
email: str,
name: str,
password: str,
role: str = 'user',
permissions_str: str = '',
is_admin: bool = False,
is_active: bool = True,
admin_id: Optional[int] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new user (admin operation).
Business Rules:
1. All required fields must be provided
2. Username must be unique
3. Email must be unique
4. Permissions must be valid JSON
5. Default to non-admin active user
6. Log creation event if admin_id provided
Args:
username: User's username
email: User's email
name: User's full name
password: User's password
role: User's role (default: 'user')
permissions_str: JSON string of permissions
is_admin: Whether user is an admin
is_active: Whether user is active
admin_id: ID of admin creating this user (for audit log)
ip_address: IP address for audit log
user_agent: User agent for audit log
Returns:
Dict with 'success' (bool), 'user_id' (if successful), or 'error'
"""
# Business Rule 1: Required fields
if not all([username, email, name, password]):
return {'success': False, 'error': 'All fields are required'}
# Business Rule 2: Username uniqueness
if User.query.filter_by(username=username).first():
return {'success': False, 'error': 'Username already exists'}
# Business Rule 3: Email uniqueness
if User.query.filter_by(email=email).first():
return {'success': False, 'error': 'Email already exists'}
# Business Rule 4: Parse permissions (comma-separated or JSON)
try:
if permissions_str:
# Try JSON first
try:
permissions = json.loads(permissions_str)
except json.JSONDecodeError:
# Fall back to comma-separated
permissions = [p.strip() for p in permissions_str.split(',') if p.strip()]
else:
permissions = []
except Exception:
return {'success': False, 'error': 'Invalid permissions format'}
# Create user
user = User(
username=username,
email=email,
name=name,
preferred_username=username,
role=role,
permissions=json.dumps(permissions) if permissions else None,
is_admin=is_admin,
is_active=is_active
)
user.set_password(password)
try:
self.db.add(user)
self.db.commit()
# Business Rule 6: Log creation if admin_id provided
if admin_id:
admin_user = User.query.get(admin_id)
if admin_user:
AuditLog.log(
action='user_created',
username=admin_user.username,
user_id=admin_user.id,
ip_address=ip_address,
user_agent=user_agent,
details={
'created_user': username,
'created_user_id': user.id,
'role': role,
'is_admin': is_admin
}
)
return {
'success': True,
'user_id': user.id,
'message': f'User "{username}" created successfully'
}
except Exception as e:
self.db.rollback()
return {'success': False, 'error': f'Failed to create user: {str(e)}'}
def update_user(
self,
user_id: int,
username: Optional[str] = None,
email: Optional[str] = None,
name: Optional[str] = None,
role: Optional[str] = None,
permissions_str: Optional[str] = None,
is_admin: Optional[bool] = None,
is_active: Optional[bool] = None,
new_password: Optional[str] = None
) -> Dict[str, Any]:
"""
Update an existing user.
Business Rules:
1. User must exist
2. If username changes, new username must be unique
3. If email changes, new email must be unique
4. Permissions must be valid (JSON or comma-separated) if provided
5. Update password if provided
Args:
user_id: ID of user to update
username: New username (optional)
email: New email (optional)
name: New name (optional)
role: New role (optional)
permissions_str: New permissions (JSON or comma-separated) (optional)
is_admin: New admin status (optional)
is_active: New active status (optional)
new_password: New password (optional)
Returns:
Dict with 'success' (bool) and 'message' or 'error'
"""
# Business Rule 1: User must exist
user = User.query.get(user_id)
if not user:
return {'success': False, 'error': 'User not found'}
# Business Rule 2: Username uniqueness (if changing)
if username and username != user.username:
if User.query.filter_by(username=username).first():
return {'success': False, 'error': 'Username already exists'}
user.username = username
# Business Rule 3: Email uniqueness (if changing)
if email and email != user.email:
if User.query.filter_by(email=email).first():
return {'success': False, 'error': 'Email already exists'}
user.email = email
# Update other fields if provided
if name:
user.name = name
if role:
user.role = role
# Business Rule 4: Parse permissions if provided (JSON or comma-separated)
if permissions_str is not None:
try:
if permissions_str:
# Try JSON first
try:
permissions = json.loads(permissions_str)
except json.JSONDecodeError:
# Fall back to comma-separated
permissions = [p.strip() for p in permissions_str.split(',') if p.strip()]
user.set_permissions(permissions)
else:
user.permissions = None
except Exception:
return {'success': False, 'error': 'Invalid permissions format'}
if is_admin is not None:
user.is_admin = is_admin
if is_active is not None:
user.is_active = is_active
# Business Rule 5: Update password if provided
if new_password:
user.set_password(new_password)
try:
self.db.commit()
return {
'success': True,
'message': f'User "{user.username}" updated successfully'
}
except Exception as e:
self.db.rollback()
return {'success': False, 'error': f'Failed to update user: {str(e)}'}
def deactivate_user(self, user_id: int) -> Dict[str, Any]:
"""
Deactivate a user.
Business Rules:
1. User must exist
2. Set is_active to False
Args:
user_id: ID of user to deactivate
Returns:
Dict with 'success' (bool) and 'message' or 'error'
"""
user = User.query.get(user_id)
if not user:
return {'success': False, 'error': 'User not found'}
user.is_active = False
try:
self.db.commit()
return {'success': True, 'message': f'User "{user.username}" deactivated'}
except Exception as e:
self.db.rollback()
return {'success': False, 'error': f'Failed to deactivate user: {str(e)}'}
def activate_user(self, user_id: int) -> Dict[str, Any]:
"""
Activate a user.
Business Rules:
1. User must exist
2. Set is_active to True
Args:
user_id: ID of user to activate
Returns:
Dict with 'success' (bool) and 'message' or 'error'
"""
user = User.query.get(user_id)
if not user:
return {'success': False, 'error': 'User not found'}
user.is_active = True
try:
self.db.commit()
return {'success': True, 'message': f'User "{user.username}" activated'}
except Exception as e:
self.db.rollback()
return {'success': False, 'error': f'Failed to activate user: {str(e)}'}
def delete_user(
self,
user_id: int,
admin_id: Optional[int] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None
) -> Dict[str, Any]:
"""
Delete a user.
Business Rules:
1. User must exist
2. Cannot delete last admin user
3. Permanently remove from database
4. Log deletion if admin_id provided
Args:
user_id: ID of user to delete
admin_id: ID of admin deleting the user (for audit log)
ip_address: IP address for audit log
user_agent: User agent for audit log
Returns:
Dict with 'success' (bool) and 'message' or 'error'
"""
user = User.query.get(user_id)
if not user:
return {'success': False, 'error': 'User not found'}
# Business Rule 2: Check if deleting last admin
if user.is_admin:
admin_count = User.query.filter_by(is_admin=True).count()
if admin_count <= 1:
return {'success': False, 'error': 'Cannot delete last admin user'}
# Save info for logging
username = user.username
deleted_user_id = user.id
try:
self.db.delete(user)
self.db.commit()
# Business Rule 4: Log deletion if admin_id provided
if admin_id:
admin_user = User.query.get(admin_id)
if admin_user:
AuditLog.log(
action='user_deleted',
username=admin_user.username,
user_id=admin_user.id,
ip_address=ip_address,
user_agent=user_agent,
details={
'deleted_user': username,
'deleted_user_id': deleted_user_id
}
)
return {'success': True, 'message': f'User "{username}" deleted permanently'}
except Exception as e:
self.db.rollback()
return {'success': False, 'error': f'Failed to delete user: {str(e)}'}