1f17b6c0d2
Introduces a full user management system with three roles (admin, operator,
viewer), an audit log, and per-session login history.
Backend:
- app/internal/audit.py: write_audit() helper → audit_log Firestore collection
- app/internal/auth.py: get_role() helper; require_admin_token accepts both
legacy admin:true claim and new role:"admin" claim for backward compat
- app/routers/users.py: CRUD under /admin/users — list, create (returns
one-time invite link), get (with sessions), patch role/nodes/name,
disable, enable, delete; operator role requires ≥1 owned node
- app/routers/links.py: POST /auth/session records sign-in events to
user_sessions Firestore collection
- app/routers/admin.py: GET /admin/audit paginated endpoint
- app/main.py: register users router
Frontend:
- AuthProvider: exposes role, isAdmin, isOperator, ownedNodeIds from claims
- Nav: role-gated links — viewers get dashboard/calls/incidents/map/alerts/
trips; operators add nodes/systems/tokens; admins add admin
- admin/page.tsx: new Users tab (list table, create modal, inline edit panel
with role/nodes editor, disable/enable/delete, login history) and Audit
Log tab (paginated, color-coded actions)
- login/page.tsx: calls recordSession() on email and Google sign-in
- nodes, systems, tokens pages: role guards redirect viewers to dashboard
- profile/page.tsx: shows accurate role badge and label
- lib/types.ts: UserRole, UserRecord, UserSession, AuditEntry types
- lib/c2api.ts: user management methods + recordSession
Firestore collections added: user_profiles, audit_log, user_sessions
Firebase custom claims schema: { role, owned_node_ids, admin (legacy) }
27 lines
780 B
Python
27 lines
780 B
Python
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
from uuid import uuid4
|
|
from app.internal import firestore as fstore
|
|
|
|
|
|
async def write_audit(
|
|
actor_uid: str,
|
|
actor_email: str,
|
|
action: str,
|
|
target_uid: Optional[str] = None,
|
|
target_email: Optional[str] = None,
|
|
details: Optional[dict] = None,
|
|
) -> None:
|
|
"""Write an entry to the audit_log collection."""
|
|
doc_id = str(uuid4())
|
|
await fstore.doc_set("audit_log", doc_id, {
|
|
"log_id": doc_id,
|
|
"action": action,
|
|
"actor_uid": actor_uid,
|
|
"actor_email": actor_email,
|
|
"target_uid": target_uid,
|
|
"target_email": target_email,
|
|
"details": details or {},
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
}, merge=False)
|