""" Custom Decorators ================= Reusable decorators for access control and validation. Role-based decorators (new system): - @role_required(SystemRole.OFFICE_MANAGER) - Minimum role check - @company_permission('edit') - Company-specific permissions - @member_required - Shortcut for role >= MEMBER Legacy decorators (backward compatibility): - @admin_required - Alias for @role_required(SystemRole.ADMIN) - @company_owner_or_admin - Uses new can_edit_company() method """ from functools import wraps from flask import abort, flash, redirect, url_for, request from flask_login import current_user def is_audit_owner(): """True for SUPERADMIN or company MANAGER+ — can view audits. SUPERADMIN sees all audits. MANAGER sees only own company (enforced in routes via can_edit_company). """ if not current_user.is_authenticated: return False from database import SystemRole if current_user.has_role(SystemRole.SUPERADMIN): return True # MANAGER of any company can access audit dashboards (route-level check restricts to own company) return current_user.can_edit_company() # Import role enums (lazy import to avoid circular dependencies) def _get_system_role(): from database import SystemRole return SystemRole # ============================================================ # NEW ROLE-BASED DECORATORS # ============================================================ def role_required(min_role): """ Decorator that requires user to have at least the specified role. Args: min_role: Minimum SystemRole required (e.g., SystemRole.OFFICE_MANAGER) Usage: @bp.route('/admin/companies') @login_required @role_required(SystemRole.OFFICE_MANAGER) def admin_companies(): ... Note: Always use @login_required BEFORE @role_required """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) if not current_user.has_role(min_role): SystemRole = _get_system_role() role_names = { SystemRole.MEMBER: 'członka Izby', SystemRole.EMPLOYEE: 'pracownika firmy', SystemRole.MANAGER: 'kadry zarządzającej', SystemRole.OFFICE_MANAGER: 'kierownika biura', SystemRole.ADMIN: 'administratora', } role_name = role_names.get(min_role, 'wyższych uprawnień') flash(f'Ta strona wymaga uprawnień {role_name}.', 'error') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function return decorator def member_required(f): """ Decorator that requires user to be at least a MEMBER. Shortcut for @role_required(SystemRole.MEMBER). Usage: @bp.route('/forum') @login_required @member_required def forum_index(): ... """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) SystemRole = _get_system_role() if not current_user.has_role(SystemRole.MEMBER): flash('Ta strona jest dostępna tylko dla członków Izby NORDA.', 'warning') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function def company_permission(permission_type='edit'): """ Decorator that checks user's permission for a company. Args: permission_type: 'view', 'edit', or 'manage' The company_id is extracted from: 1. URL parameter 'company_id' 2. Query parameter 'company_id' 3. User's own company_id Usage: @bp.route('/company//edit') @login_required @company_permission('edit') def edit_company(company_id): ... @bp.route('/company//users') @login_required @company_permission('manage') def manage_company_users(company_id): ... """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) # Get company_id from various sources company_id = ( kwargs.get('company_id') or request.args.get('company_id', type=int) or current_user.company_id ) if company_id is None: flash('Nie określono firmy.', 'error') return redirect(url_for('public.index')) # Check permission based on type has_permission = False if permission_type == 'view': # Anyone can view public companies, but for dashboard check company membership has_permission = ( current_user.can_access_admin_panel() or current_user.company_id == company_id ) elif permission_type == 'edit': has_permission = current_user.can_edit_company(company_id) elif permission_type == 'manage': has_permission = current_user.can_manage_company(company_id) else: abort(500, f"Unknown permission type: {permission_type}") if not has_permission: flash('Nie masz uprawnień do tej operacji.', 'error') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function return decorator def office_manager_required(f): """ Decorator that requires user to be at least OFFICE_MANAGER. Shortcut for @role_required(SystemRole.OFFICE_MANAGER). Usage: @bp.route('/admin/companies') @login_required @office_manager_required def admin_companies(): ... """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) SystemRole = _get_system_role() if not current_user.has_role(SystemRole.OFFICE_MANAGER): flash('Ta strona wymaga uprawnień kierownika biura.', 'error') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function def forum_access_required(f): """ Decorator that requires user to have forum access. Members and above can access the forum. Usage: @bp.route('/forum/topic/') @login_required @forum_access_required def view_topic(topic_id): ... """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) if not current_user.can_access_forum(): flash('Forum jest dostępne tylko dla członków Izby NORDA.', 'warning') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function def moderator_required(f): """ Decorator that requires forum moderator permissions. OFFICE_MANAGER and ADMIN roles have this permission. Usage: @bp.route('/forum/topic//delete') @login_required @moderator_required def delete_topic(topic_id): ... """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) if not current_user.can_moderate_forum(): flash('Ta akcja wymaga uprawnień moderatora.', 'error') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function def rada_member_required(f): """ Decorator that requires user to be a member of Rada Izby (Board Council). OFFICE_MANAGER and ADMIN roles also have access for management purposes. Usage: @bp.route('/rada/') @login_required @rada_member_required def board_index(): ... """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) SystemRole = _get_system_role() # Allow access if: is_rada_member OR has OFFICE_MANAGER/ADMIN role if not current_user.is_rada_member and not current_user.has_role(SystemRole.OFFICE_MANAGER): flash('Strefa RADA jest dostępna tylko dla członków Rady Izby.', 'warning') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function # ============================================================ # LEGACY DECORATORS (backward compatibility) # ============================================================ def admin_required(f): """ Decorator that requires user to be logged in AND be an admin. DEPRECATED: Use @role_required(SystemRole.ADMIN) instead. Usage: @bp.route('/admin/users') @login_required @admin_required def admin_users(): ... Note: Always use @login_required BEFORE @admin_required """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) # Use role system (is_admin fallback removed — role is source of truth) SystemRole = _get_system_role() if not current_user.has_role(SystemRole.ADMIN): flash('Brak uprawnień administratora.', 'error') return redirect(url_for('public.index')) return f(*args, **kwargs) return decorated_function def verified_required(f): """ Decorator that requires user to have verified email. Usage: @bp.route('/forum/new') @login_required @verified_required def new_topic(): ... """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) if not current_user.is_verified: flash('Musisz zweryfikować swój email, aby wykonać tę akcję.', 'warning') return redirect(url_for('auth.resend_verification')) return f(*args, **kwargs) return decorated_function def company_owner_or_admin(f): """ Decorator for routes that accept company_id. Allows access only if user is admin OR owns the company. DEPRECATED: Use @company_permission('edit') instead. Usage: @bp.route('/company//edit') @login_required @company_owner_or_admin def edit_company(company_id): ... """ @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for('auth.login')) company_id = kwargs.get('company_id') if company_id is None: abort(400) # Use new permission system if current_user.can_edit_company(company_id): return f(*args, **kwargs) flash('Nie masz uprawnień do tej firmy.', 'error') return redirect(url_for('public.index')) return decorated_function