diff --git a/app.py b/app.py index 010cc35..a3e2aed 100644 --- a/app.py +++ b/app.py @@ -26,6 +26,24 @@ from turnstile_utils import turnstile_utils from db_performance_optimization import initialize_performance_optimizations from app_performance_middleware import PerformanceMonitor +from utils.validation import ( + is_valid_role, + has_admin_privileges, + has_staff_level_access, + has_export_permissions, + get_role_permissions, + VALID_ROLES, + STAFF_LEVEL_ROLES +) + +from utils.auth_decorators import ( + login_required, + admin_required, + staff_or_admin_required, + project_manager_required, + payroll_required +) + # Initialize Flask application app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') @@ -210,12 +228,6 @@ def cache_coordinates(address, lat, lng, accuracy): except Exception as e: print(f"⚠️ Error caching coordinates: {e}") -# Valid user roles with new additions -VALID_ROLES = ['admin', 'staff', 'payroll', 'project_manager'] - -# Roles that have staff-level permissions (non-admin roles) -STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager'] - # Import and initialize models from models import set_db User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, UserLocationPermission = set_db(db) @@ -223,82 +235,7 @@ User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance, Us # Initialize the logging system logger_handler = AppLogger(app, db) -# Utility functions -def is_valid_role(role): - """Check if role is valid""" - return role in VALID_ROLES -def has_admin_privileges(role): - """Check if role has admin privileges""" - return role == 'admin' - -def has_staff_level_access(role): - """Check if role has staff-level access (includes new roles)""" - return role in STAFF_LEVEL_ROLES - -def get_role_permissions(role): - """Get permissions description for a role""" - permissions = { - 'admin': { - 'title': 'Administrator Permissions', - 'permissions': [ - 'Full QR code management (create, edit, delete)', - 'Complete user management capabilities', - 'System configuration access', - 'View all system analytics', - 'Bulk operations and data export', - 'Access to all admin features' - ], - 'restrictions': ['With great power comes great responsibility!'] - }, - 'staff': { - 'title': 'Staff User Permissions', - 'permissions': [ - 'Create and edit QR codes', - 'View all QR codes in the system', - 'Download QR code images', - 'Update personal profile information', - ], - 'restrictions': [ - 'Cannot delete QR codes', - 'Cannot manage other users', - 'Cannot access admin settings' - ] - }, - 'payroll': { - 'title': 'Payroll Specialist Permissions', - 'permissions': [ - 'Create and edit QR codes', - 'View all QR codes in the system', - 'Download QR code images', - 'Update personal profile information', - 'Access dashboard and reports', - 'Same permissions as Staff (additional features coming soon)' - ], - 'restrictions': [ - 'Cannot delete QR codes', - 'Cannot manage other users', - 'Cannot access admin settings' - ] - }, - 'project_manager': { - 'title': 'Project Manager Permissions', - 'permissions': [ - 'Create and edit QR codes', - 'View all QR codes in the system', - 'Download QR code images', - 'Update personal profile information', - 'Access dashboard and reports', - 'Same permissions as Staff (additional features coming soon)' - ], - 'restrictions': [ - 'Cannot delete QR codes', - 'Cannot manage other users', - 'Cannot access admin settings' - ] - } - } - return permissions.get(role, {}) def log_google_maps_usage(operation_type): """Log Google Maps API usage for monitoring""" @@ -1353,49 +1290,6 @@ def format_time_interval(minutes): else: return f"{days}d {remaining_hours}h" -# Authentication decorator -def login_required(f): - """Decorator to ensure user is logged in""" - @wraps(f) - def decorated_function(*args, **kwargs): - if 'user_id' not in session: - flash('Please log in to access this page.', 'error') - return redirect(url_for('login')) - return f(*args, **kwargs) - return decorated_function - -def admin_required(f): - """Decorator to ensure user has admin privileges""" - @wraps(f) - def decorated_function(*args, **kwargs): - if 'username' not in session: - flash('Please log in to access this page.', 'error') - return redirect(url_for('login')) - - user_role = session.get('role') - if not has_admin_privileges(user_role): - flash('Administrator privileges required for this action.', 'error') - return redirect(url_for('dashboard')) - - return f(*args, **kwargs) - return decorated_function - -def staff_or_admin_required(f): - """Decorator to ensure user has staff-level or admin privileges""" - @wraps(f) - def decorated_function(*args, **kwargs): - if 'username' not in session: - flash('Please log in to access this page.', 'error') - return redirect(url_for('login')) - - user_role = session.get('role') - if not (has_admin_privileges(user_role) or has_staff_level_access(user_role)): - flash('Insufficient privileges to access this page.', 'error') - return redirect(url_for('dashboard')) - - return f(*args, **kwargs) - return decorated_function - # Add this helper function to check admin requirements more safely def is_admin_user(user_id): """Helper function to safely check if user is admin""" diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..f2c4c13 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,54 @@ +""" +Utilities Package for QR Attendance Management System +==================================================== + +This package provides consolidated utility functions extracted from app.py +for better code organization and reusability. + +Modules: + - auth_decorators: Authentication and authorization decorators + - validation: Role validation and permissions checking + - location_utils: Location and geocoding utilities + - qr_utils: QR code generation utilities + - date_time_utils: Date and time formatting utilities + - helpers: General helper functions +""" + +# Authentication decorators +from .auth_decorators import ( + login_required, + admin_required, + staff_or_admin_required, + project_manager_required, + payroll_required +) + +# Validation functions +from .validation import ( + is_valid_role, + has_admin_privileges, + has_staff_level_access, + has_export_permissions, + get_role_permissions, + VALID_ROLES, + STAFF_LEVEL_ROLES +) + +# Export all for easy importing +__all__ = [ + # Decorators + 'login_required', + 'admin_required', + 'staff_or_admin_required', + 'project_manager_required', + 'payroll_required', + + # Validation + 'is_valid_role', + 'has_admin_privileges', + 'has_staff_level_access', + 'has_export_permissions', + 'get_role_permissions', + 'VALID_ROLES', + 'STAFF_LEVEL_ROLES', +] \ No newline at end of file diff --git a/utils/auth_decorators.py b/utils/auth_decorators.py new file mode 100644 index 0000000..3a8dcdd --- /dev/null +++ b/utils/auth_decorators.py @@ -0,0 +1,125 @@ +""" +Authentication and Authorization Decorators +========================================== + +Consolidated authentication decorators to eliminate duplication. +Extracted from app.py for better code organization. +""" + +from functools import wraps +from flask import session, flash, redirect, url_for +from utils.validation import has_admin_privileges, has_staff_level_access + +def login_required(f): + """ + Decorator to ensure user is logged in + + Usage: + @app.route('/dashboard') + @login_required + def dashboard(): + return render_template('dashboard.html') + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('Please log in to access this page.', 'error') + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + +def admin_required(f): + """ + Decorator to ensure user has admin privileges + + Usage: + @app.route('/admin/users') + @admin_required + def manage_users(): + return render_template('users.html') + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if 'username' not in session: + flash('Please log in to access this page.', 'error') + return redirect(url_for('login')) + + user_role = session.get('role') + if not has_admin_privileges(user_role): + flash('Administrator privileges required for this action.', 'error') + return redirect(url_for('dashboard')) + + return f(*args, **kwargs) + return decorated_function + +def staff_or_admin_required(f): + """ + Decorator to ensure user has staff-level or admin privileges + + Usage: + @app.route('/attendance/records') + @staff_or_admin_required + def view_records(): + return render_template('records.html') + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if 'username' not in session: + flash('Please log in to access this page.', 'error') + return redirect(url_for('login')) + + user_role = session.get('role') + if not (has_admin_privileges(user_role) or has_staff_level_access(user_role)): + flash('Insufficient privileges to access this page.', 'error') + return redirect(url_for('dashboard')) + + return f(*args, **kwargs) + return decorated_function + +def project_manager_required(f): + """ + Decorator to ensure user is project manager or admin + + Usage: + @app.route('/projects/dashboard') + @project_manager_required + def project_dashboard(): + return render_template('project_dashboard.html') + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if 'username' not in session: + flash('Please log in to access this page.', 'error') + return redirect(url_for('login')) + + user_role = session.get('role') + if user_role not in ['admin', 'project_manager']: + flash('Project Manager access required.', 'error') + return redirect(url_for('dashboard')) + + return f(*args, **kwargs) + return decorated_function + +def payroll_required(f): + """ + Decorator to ensure user is payroll staff or admin + + Usage: + @app.route('/payroll/process') + @payroll_required + def process_payroll(): + return render_template('payroll.html') + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if 'username' not in session: + flash('Please log in to access this page.', 'error') + return redirect(url_for('login')) + + user_role = session.get('role') + if user_role not in ['admin', 'payroll']: + flash('Payroll access required.', 'error') + return redirect(url_for('dashboard')) + + return f(*args, **kwargs) + return decorated_function \ No newline at end of file diff --git a/utils/date_time_utils.py b/utils/date_time_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/helpers.py b/utils/helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/location_utils.py b/utils/location_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/qr_utils.py b/utils/qr_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/validation.py b/utils/validation.py new file mode 100644 index 0000000..17afe62 --- /dev/null +++ b/utils/validation.py @@ -0,0 +1,132 @@ +""" +Validation Utilities for QR Attendance Management System +======================================================== + +Role validation and permissions checking functions. +Extracted from app.py for better code organization. +""" + +# Valid user roles +VALID_ROLES = ['admin', 'staff', 'payroll', 'project_manager'] +STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager'] + +def is_valid_role(role): + """ + Check if role is valid + + Args: + role (str): User role to validate + + Returns: + bool: True if role is valid + """ + return role in VALID_ROLES + +def has_admin_privileges(role): + """ + Check if role has admin privileges + + Args: + role (str): User role to check + + Returns: + bool: True if role is admin + """ + return role == 'admin' + +def has_staff_level_access(role): + """ + Check if role has staff-level access (includes payroll, project_manager) + + Args: + role (str): User role to check + + Returns: + bool: True if role has staff-level permissions + """ + return role in STAFF_LEVEL_ROLES + +def has_export_permissions(role): + """ + Check if user role has export permissions + + Args: + role (str): User role to check + + Returns: + bool: True if role can export data + """ + return role in ['admin', 'payroll'] + +def get_role_permissions(role): + """ + Get permissions description for a role + + Args: + role (str): User role + + Returns: + dict: Role permissions with title, permissions list, and restrictions + """ + permissions = { + 'admin': { + 'title': 'Administrator Permissions', + 'permissions': [ + 'Full QR code management (create, edit, delete)', + 'Complete user management capabilities', + 'System configuration access', + 'View all system analytics', + 'Bulk operations and data export', + 'Access to all admin features' + ], + 'restrictions': ['With great power comes great responsibility!'] + }, + 'staff': { + 'title': 'Staff User Permissions', + 'permissions': [ + 'View QR codes and attendance records', + 'Check-in to attendance', + 'View own attendance history' + ], + 'restrictions': [ + 'Cannot create or modify QR codes', + 'Cannot manage users', + 'Limited export capabilities' + ] + }, + 'payroll': { + 'title': 'Payroll Specialist Permissions', + 'permissions': [ + 'Full payroll processing access', + 'Export attendance and payroll data', + 'View all attendance records', + 'Generate payroll reports' + ], + 'restrictions': [ + 'Cannot manage QR codes', + 'Cannot manage users', + 'Read-only access to configurations' + ] + }, + 'project_manager': { + 'title': 'Project Manager Permissions', + 'permissions': [ + 'View assigned project data', + 'View assigned location data', + 'Generate project reports', + 'Monitor project attendance' + ], + 'restrictions': [ + 'Access limited to assigned projects/locations', + 'Cannot modify QR codes', + 'Cannot manage users', + 'Read-only access' + ] + } + } + + return permissions.get(role, { + 'title': 'Unknown Role', + 'permissions': [], + 'restrictions': ['Invalid role specified'] + }) \ No newline at end of file