feat: add role-based user management, audit log, and session tracking
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) }
This commit is contained in:
@@ -8,20 +8,26 @@ import { useUnacknowledgedAlerts } from "@/lib/useAlerts";
|
||||
import { useAuth } from "@/components/AuthProvider";
|
||||
import { useTheme } from "@/components/ThemeProvider";
|
||||
|
||||
const links = [
|
||||
{ href: "/dashboard", label: "Dashboard" },
|
||||
{ href: "/nodes", label: "Nodes" },
|
||||
{ href: "/systems", label: "Systems" },
|
||||
{ href: "/calls", label: "Calls" },
|
||||
{ href: "/incidents", label: "Incidents" },
|
||||
{ href: "/map", label: "Map" },
|
||||
{ href: "/alerts", label: "Alerts" },
|
||||
{ href: "/trips", label: "Trips" },
|
||||
// Links visible to all authenticated roles (viewer+)
|
||||
const viewerLinks = [
|
||||
{ href: "/dashboard", label: "Dashboard" },
|
||||
{ href: "/calls", label: "Calls" },
|
||||
{ href: "/incidents", label: "Incidents" },
|
||||
{ href: "/map", label: "Map" },
|
||||
{ href: "/alerts", label: "Alerts" },
|
||||
{ href: "/trips", label: "Trips" },
|
||||
];
|
||||
|
||||
// Additional links for operators and admins
|
||||
const operatorLinks = [
|
||||
{ href: "/nodes", label: "Nodes" },
|
||||
{ href: "/systems", label: "Systems" },
|
||||
{ href: "/tokens", label: "Tokens" },
|
||||
];
|
||||
|
||||
// Admin-only links
|
||||
const adminLinks = [
|
||||
{ href: "/tokens", label: "Tokens" },
|
||||
{ href: "/admin", label: "Admin" },
|
||||
{ href: "/admin", label: "Admin" },
|
||||
];
|
||||
|
||||
function SunIcon() {
|
||||
@@ -49,7 +55,7 @@ function MoonIcon() {
|
||||
}
|
||||
|
||||
export function Nav() {
|
||||
const { user, isAdmin } = useAuth();
|
||||
const { user, isAdmin, isOperator } = useAuth();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { nodes: pending } = useUnconfiguredNodes();
|
||||
@@ -59,7 +65,11 @@ export function Nav() {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const allLinks = [...links, ...(isAdmin ? adminLinks : [])];
|
||||
const allLinks = [
|
||||
...viewerLinks,
|
||||
...(isAdmin || isOperator ? operatorLinks : []),
|
||||
...(isAdmin ? adminLinks : []),
|
||||
];
|
||||
|
||||
function navLinkClass(href: string) {
|
||||
return `text-sm font-mono transition-colors shrink-0 ${
|
||||
|
||||
Reference in New Issue
Block a user