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)"
+