feat(roles): add dedicated Supervisor (capoturno) role

Until now the out-of-tolerance authorization ("Autorizzazione capoturno")
required is_admin, conflating shift-leader authority with full system
administration. Introduce a combinable Supervisor role so a capoturno can
authorize overrides without admin privileges (roadmap D-0.8 / Phase 2).

Backend:
- users router: add "Supervisor" to the allowed roles (centralized as
  VALID_ROLES, deduplicating the create/update validation).
- middleware: add require_supervisor dependency (admins still bypass).

Frontend:
- measure.validate_supervisor: authorize users with the Supervisor role OR
  is_admin (was is_admin only).
- admin/users: Supervisor checkbox + orange role badge.
- profile: explicit Supervisor badge styling.
- EN translation for the new role label.

roles is a free JSON column, so no migration is required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-05 09:02:24 +00:00
parent bff577b461
commit 6ff78b2150
7 changed files with 25 additions and 8 deletions
+2
View File
@@ -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",
+1
View File
@@ -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()
+8 -6
View File
@@ -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)