feat: multi-role support — same as prod

This commit is contained in:
2026-05-25 23:10:53 -04:00
parent e7cdac7c44
commit 750cde68c9
4 changed files with 2487 additions and 31 deletions
+26 -15
View File
@@ -28,6 +28,7 @@ from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from cantaloupe_sync import router as cantaloupe_router, create_sync_tables
from exif_module import router as exif_router, create_exif_tables
# ─── Config ─────────────────────────────────────────────────────────────────
@@ -50,6 +51,24 @@ def _load_categories() -> set:
VALID_CATEGORIES = _load_categories()
VALID_STATUSES = {"active", "maintenance", "retired"}
VALID_ROLES = {"admin", "technician", "readonly"}
# Helpers for comma-separated multi-role support
def roles_contain(role_str: str, target: str) -> bool:
"""Check if a comma-separated role string includes target."""
return target in [r.strip() for r in (role_str or "").split(",")]
def validate_roles(role_str: str) -> str:
"""Accept comma-separated role combos. Normalise: strip whitespace, dedupe, sort."""
parts = [r.strip().lower() for r in role_str.split(",")]
parts = [r for r in parts if r] # drop empties
if not parts:
raise HTTPException(status_code=422, detail="At least one role is required")
invalid = [r for r in parts if r not in VALID_ROLES]
if invalid:
raise HTTPException(
status_code=422,
detail=f"Invalid role(s): {', '.join(invalid)}. Must be one of: {', '.join(sorted(VALID_ROLES))}",
)
return ",".join(sorted(set(parts)))
# ─── Database ───────────────────────────────────────────────────────────────
@@ -358,6 +377,7 @@ async def lifespan(app: FastAPI):
conn = get_db()
init_db(conn)
conn.close()
create_exif_tables()
yield
@@ -373,6 +393,7 @@ app.add_middleware(
# Mount Cantaloupe sync routes
app.include_router(cantaloupe_router)
app.include_router(exif_router)
# ─── Auth Middleware ────────────────────────────────────────────────────────
@@ -683,12 +704,7 @@ def create_user(body: UserCreate):
password = body.password
if not password:
raise HTTPException(status_code=422, detail="Password must not be empty")
role = body.role or "technician"
if role not in VALID_ROLES:
raise HTTPException(
status_code=422,
detail=f"Invalid role '{role}'. Must be one of: {', '.join(sorted(VALID_ROLES))}",
)
role = validate_roles(body.role or "technician")
conn = get_db()
password_hash = _hash_password(password)
@@ -738,13 +754,8 @@ def update_user(user_id: int, body: UserUpdate):
raise HTTPException(status_code=404, detail="User not found")
if body.role is not None:
if body.role not in VALID_ROLES:
conn.close()
raise HTTPException(
status_code=422,
detail=f"Invalid role '{body.role}'. Must be one of: {', '.join(sorted(VALID_ROLES))}",
)
conn.execute("UPDATE users SET role = ? WHERE id = ?", (body.role, user_id))
role = validate_roles(body.role)
conn.execute("UPDATE users SET role = ? WHERE id = ?", (role, user_id))
if body.password is not None:
pw = body.password.strip()
@@ -1501,7 +1512,7 @@ def get_stats():
THEN (julianday(v.checkout_time) - julianday(v.checkin_time)) * 1440
ELSE 0 END) AS total_minutes
FROM visits v JOIN users u ON v.user_id = u.id
WHERE u.role = 'technician'
WHERE u.role LIKE '%technician%'
GROUP BY v.user_id ORDER BY total_minutes DESC"""
).fetchall()
time_on_site = [
@@ -1657,7 +1668,7 @@ def reset_database(request: Request):
# Admin-only check
user = getattr(request.state, "current_user", None)
auth_skipped = os.environ.get("CANTEEN_SKIP_AUTH") == "1"
if not auth_skipped and (not user or user.get("role") != "admin"):
if not auth_skipped and (not user or not roles_contain(user.get("role", ""), "admin")):
raise HTTPException(status_code=403, detail="Only admins can reset the database")
# Confirmation header safety check
+1296
View File
File diff suppressed because it is too large Load Diff
Executable
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# 🚀 Dev instance — admin server on port 8092, points to dev DB
set -euo pipefail
cd "$(dirname "$0")"
PORT=8092
MAIN_DEV_DIR="$HOME/projects/canteen-asset-tracker-dev"
CANTEEN_DB_PATH="${MAIN_DEV_DIR}/assets.dev.db"
echo "🚀 Starting DEV admin server on http://0.0.0.0:${PORT}"
echo " DB: ${CANTEEN_DB_PATH}"
exec python -m uvicorn admin_server:app \
--host 0.0.0.0 \
--port "$PORT" \
--log-level info
+1150 -16
View File
File diff suppressed because it is too large Load Diff