"""Authorization decorators for role and permission gating. Authentication (is the caller logged in?) is handled by Flask-JWT-Extended's @jwt_required. Authorization (is the caller ALLOWED to do this?) is handled here. The two are separate concerns; a route needs both on any state-changing action. These decorators call verify_jwt_in_request() themselves, so they work whether or not a separate @jwt_required is also present. The admin role bypasses every permission check (see User.haspermission), so an admin never needs individual permissions granted. Usage: @assets_bp.route('/', methods=['DELETE']) @jwt_required() @require_permission('assets.delete') def delete_asset(asset_id): ... """ from functools import wraps from flask_jwt_extended import verify_jwt_in_request, current_user from shopdb.utils.responses import error_response, ErrorCodes def require_permission(permission_name: str): """Gate a route behind a single permission. Admin role bypasses.""" def decorator(view_func): @wraps(view_func) def wrapper(*args, **kwargs): # verify jwt first so current_user is loaded (idempotent if the # route also has @jwt_required) verify_jwt_in_request() if current_user is None: return error_response( ErrorCodes.UNAUTHORIZED, 'Authentication required', http_code=401 ) if not current_user.haspermission(permission_name): return error_response( ErrorCodes.FORBIDDEN, 'You do not have permission to perform this action', http_code=403 ) return view_func(*args, **kwargs) return wrapper return decorator def require_role(rolename: str): """Gate a route behind a single role (e.g. 'admin').""" def decorator(view_func): @wraps(view_func) def wrapper(*args, **kwargs): verify_jwt_in_request() if current_user is None: return error_response( ErrorCodes.UNAUTHORIZED, 'Authentication required', http_code=401 ) if not current_user.hasrole(rolename): return error_response( ErrorCodes.FORBIDDEN, f'{rolename.capitalize()} access required', http_code=403 ) return view_func(*args, **kwargs) return wrapper return decorator