diff --git a/src/backend/api/middleware/__init__.py b/src/backend/api/middleware/__init__.py index 2c0e2e7..3bd0930 100644 --- a/src/backend/api/middleware/__init__.py +++ b/src/backend/api/middleware/__init__.py @@ -6,6 +6,7 @@ from src.backend.api.middleware.api_key import ( require_maker, require_measurement_tec, require_metrologist, + require_supervisor, require_admin_user, ) from src.backend.api.middleware.logging import AccessLogMiddleware @@ -19,6 +20,7 @@ __all__ = [ "require_maker", "require_measurement_tec", "require_metrologist", + "require_supervisor", "require_admin_user", "AccessLogMiddleware", "RateLimitMiddleware", diff --git a/src/backend/api/middleware/api_key.py b/src/backend/api/middleware/api_key.py index d248626..3f7e5a4 100644 --- a/src/backend/api/middleware/api_key.py +++ b/src/backend/api/middleware/api_key.py @@ -68,4 +68,5 @@ def require_admin(): require_maker = require_role("Maker") require_measurement_tec = require_role("MeasurementTec") require_metrologist = require_role("Metrologist") +require_supervisor = require_role("Supervisor") require_admin_user = require_admin() diff --git a/src/backend/api/routers/users.py b/src/backend/api/routers/users.py index 1302dc2..ba9ad1b 100644 --- a/src/backend/api/routers/users.py +++ b/src/backend/api/routers/users.py @@ -11,6 +11,10 @@ from src.backend.services.auth_service import create_user, hash_password, regene router = APIRouter(prefix="/api/users", tags=["users"]) +# Combinable user roles. Supervisor (capoturno) authorizes out-of-tolerance +# overrides during measurement without needing full admin privileges. +VALID_ROLES = {"Maker", "MeasurementTec", "Metrologist", "Supervisor"} + @router.get("", response_model=list[UserResponse]) async def list_users( @@ -40,12 +44,11 @@ async def create_new_user( detail=f"Username '{data.username}' already exists", ) # Validate roles - valid_roles = {"Maker", "MeasurementTec", "Metrologist"} for role in data.roles: - if role not in valid_roles: + if role not in VALID_ROLES: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Invalid role '{role}'. Valid roles: {valid_roles}", + detail=f"Invalid role '{role}'. Valid roles: {VALID_ROLES}", ) user = await create_user( db, @@ -77,12 +80,11 @@ async def update_user( update_data = data.model_dump(exclude_unset=True) # Validate roles if provided if "roles" in update_data: - valid_roles = {"Maker", "MeasurementTec", "Metrologist"} for role in update_data["roles"]: - if role not in valid_roles: + if role not in VALID_ROLES: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Invalid role '{role}'. Valid roles: {valid_roles}", + detail=f"Invalid role '{role}'. Valid roles: {VALID_ROLES}", ) for field, value in update_data.items(): setattr(user, field, value) diff --git a/src/frontend/flask_app/blueprints/measure.py b/src/frontend/flask_app/blueprints/measure.py index 997ae79..8874c03 100644 --- a/src/frontend/flask_app/blueprints/measure.py +++ b/src/frontend/flask_app/blueprints/measure.py @@ -347,7 +347,8 @@ def validate_supervisor(): return jsonify({"error": True, "detail": "Credenziali non valide"}), 401 user = resp.get("user", {}) - if not user.get("is_admin"): + is_supervisor = "Supervisor" in (user.get("roles") or []) + if not (is_supervisor or user.get("is_admin")): return jsonify({"error": True, "detail": "Utente non autorizzato (richiesto capoturno)"}), 403 return jsonify({"authorized": True, "supervisor": user.get("display_name", username)}), 200 diff --git a/src/frontend/flask_app/templates/admin/users.html b/src/frontend/flask_app/templates/admin/users.html index 6a305c9..ede89df 100644 --- a/src/frontend/flask_app/templates/admin/users.html +++ b/src/frontend/flask_app/templates/admin/users.html @@ -78,7 +78,8 @@ :class="{ 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300': role === 'Maker', 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300': role === 'MeasurementTec', - 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300': role === 'Metrologist' + 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300': role === 'Metrologist', + 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300': role === 'Supervisor' }" x-text="role"> @@ -233,6 +234,11 @@ class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30"> Metrologist + diff --git a/src/frontend/flask_app/templates/auth/profile.html b/src/frontend/flask_app/templates/auth/profile.html index 6b9180d..9eca2aa 100644 --- a/src/frontend/flask_app/templates/auth/profile.html +++ b/src/frontend/flask_app/templates/auth/profile.html @@ -87,6 +87,8 @@ bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border border-blue-200 dark:border-blue-800 {% elif role == 'Metrologist' %} bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200 border border-purple-200 dark:border-purple-800 + {% elif role == 'Supervisor' %} + bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-200 border border-orange-200 dark:border-orange-800 {% else %} bg-slate-100 dark:bg-slate-700 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-600 {% endif %}"> diff --git a/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po b/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po index aac256d..91a7590 100644 --- a/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po +++ b/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po @@ -2185,3 +2185,6 @@ msgstr "E.g. LOT-2026-001 (optional)" msgid "Es. SN-000123 (opzionale)" msgstr "E.g. SN-000123 (optional)" +msgid "Supervisor (capoturno)" +msgstr "Supervisor (shift leader)" +