create-astram-app 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/add-frontend.js +107 -0
  2. package/dist/add-frontend.js.map +1 -0
  3. package/dist/index.js +132 -33
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -2
  6. package/template/CLAUDE.md +51 -0
  7. package/template/README.md +43 -44
  8. package/template/_gitignore +6 -0
  9. package/template/frontend/FRONTEND.md +92 -0
  10. package/template/frontend/README.md +16 -0
  11. package/template/frontend/_env.example +6 -0
  12. package/template/frontend/_gitignore +9 -0
  13. package/template/frontend/eslint.config.js +71 -0
  14. package/template/frontend/index.html +13 -0
  15. package/template/frontend/package.json +52 -0
  16. package/template/frontend/public/astram-logo-full.png +0 -0
  17. package/template/frontend/public/astram-logo-white.png +0 -0
  18. package/template/frontend/public/favicon.png +0 -0
  19. package/template/frontend/src/App.jsx +167 -0
  20. package/template/frontend/src/auth/AuthContext.jsx +338 -0
  21. package/template/frontend/src/components/ConfirmDialog.jsx +92 -0
  22. package/template/frontend/src/components/EntityFormModal.jsx +154 -0
  23. package/template/frontend/src/components/EntityList.jsx +480 -0
  24. package/template/frontend/src/components/PageHeader.jsx +54 -0
  25. package/template/frontend/src/components/access/CollapsibleGroup.jsx +34 -0
  26. package/template/frontend/src/components/ui/alert-dialog.tsx +175 -0
  27. package/template/frontend/src/components/ui/alert.tsx +83 -0
  28. package/template/frontend/src/components/ui/avatar.tsx +51 -0
  29. package/template/frontend/src/components/ui/badge.tsx +56 -0
  30. package/template/frontend/src/components/ui/button.tsx +67 -0
  31. package/template/frontend/src/components/ui/card.tsx +92 -0
  32. package/template/frontend/src/components/ui/checkbox.tsx +30 -0
  33. package/template/frontend/src/components/ui/date-field.tsx +47 -0
  34. package/template/frontend/src/components/ui/dialog.tsx +214 -0
  35. package/template/frontend/src/components/ui/dropdown-menu.tsx +255 -0
  36. package/template/frontend/src/components/ui/input-otp.tsx +81 -0
  37. package/template/frontend/src/components/ui/input.tsx +21 -0
  38. package/template/frontend/src/components/ui/label.tsx +46 -0
  39. package/template/frontend/src/components/ui/searchable-select.tsx +459 -0
  40. package/template/frontend/src/components/ui/select.tsx +199 -0
  41. package/template/frontend/src/components/ui/separator.tsx +26 -0
  42. package/template/frontend/src/components/ui/sheet.tsx +139 -0
  43. package/template/frontend/src/components/ui/skeleton.tsx +13 -0
  44. package/template/frontend/src/components/ui/sonner.tsx +23 -0
  45. package/template/frontend/src/components/ui/table.tsx +181 -0
  46. package/template/frontend/src/components/ui/tooltip.tsx +59 -0
  47. package/template/frontend/src/config/branding.js +80 -0
  48. package/template/frontend/src/hooks/useMobile.tsx +21 -0
  49. package/template/frontend/src/index.css +265 -0
  50. package/template/frontend/src/lib/utils.js +7 -0
  51. package/template/frontend/src/main.jsx +27 -0
  52. package/template/frontend/src/pages/Dashboard.jsx +127 -0
  53. package/template/frontend/src/pages/ForgotPassword.jsx +104 -0
  54. package/template/frontend/src/pages/Login.jsx +393 -0
  55. package/template/frontend/src/pages/ResetPassword.jsx +159 -0
  56. package/template/frontend/src/pages/access/AccessForm.jsx +416 -0
  57. package/template/frontend/src/pages/access/AccessPage.jsx +282 -0
  58. package/template/frontend/src/pages/access/RoleActionsMenu.jsx +42 -0
  59. package/template/frontend/src/pages/account/AccountLayout.jsx +229 -0
  60. package/template/frontend/src/pages/account/ChangePasswordPage.jsx +235 -0
  61. package/template/frontend/src/pages/account/ProfilePage.jsx +122 -0
  62. package/template/frontend/src/pages/account/ReenrollPage.jsx +98 -0
  63. package/template/frontend/src/pages/account/TwoFactorPage.jsx +833 -0
  64. package/template/frontend/src/pages/audit/AuditPage.jsx +268 -0
  65. package/template/frontend/src/pages/companies/CompaniesPage.jsx +232 -0
  66. package/template/frontend/src/pages/companies/CompanyForm.jsx +127 -0
  67. package/template/frontend/src/pages/roles/RoleForm.jsx +99 -0
  68. package/template/frontend/src/pages/roles/RolesPage.jsx +207 -0
  69. package/template/frontend/src/pages/users/AssignmentsEditor.jsx +193 -0
  70. package/template/frontend/src/pages/users/MembershipCard.jsx +303 -0
  71. package/template/frontend/src/pages/users/PostingRow.jsx +264 -0
  72. package/template/frontend/src/pages/users/UserForm.jsx +167 -0
  73. package/template/frontend/src/pages/users/UsersPage.jsx +232 -0
  74. package/template/frontend/src/platform/httpClient.js +125 -0
  75. package/template/frontend/src/platform/returnPathStore.js +43 -0
  76. package/template/frontend/src/platform/sessionStore.js +29 -0
  77. package/template/frontend/src/platform/shieldTransport.js +172 -0
  78. package/template/frontend/src/platform/shieldTransport.test.mjs +245 -0
  79. package/template/frontend/src/shell/AppShell.jsx +237 -0
  80. package/template/frontend/src/shell/Sidebar.jsx +257 -0
  81. package/template/frontend/src/shell/nav.js +90 -0
  82. package/template/frontend/src/theme/ThemeProvider.jsx +14 -0
  83. package/template/frontend/src/theme/ThemeToggle.jsx +44 -0
  84. package/template/frontend/tsconfig.json +22 -0
  85. package/template/frontend/vite.config.js +30 -0
  86. package/template/server/_env.example +45 -0
  87. package/template/server/eslint.config.js +95 -0
  88. package/template/server/package.json +49 -0
  89. package/template/server/scripts/dev.mjs +51 -0
  90. package/template/server/scripts/sync-astram-guide.mjs +63 -0
  91. package/template/_env.example +0 -33
  92. package/template/package.json +0 -37
  93. /package/template/{src → server/src}/app.module.ts +0 -0
  94. /package/template/{src → server/src}/features/_gitkeep +0 -0
  95. /package/template/{src → server/src}/health/demo.controller.ts +0 -0
  96. /package/template/{src → server/src}/health/health.controller.ts +0 -0
  97. /package/template/{src → server/src}/main.ts +0 -0
  98. /package/template/{test → server/test}/enforcement.e2e.spec.ts +0 -0
  99. /package/template/{tsconfig.build.json → server/tsconfig.build.json} +0 -0
  100. /package/template/{tsconfig.json → server/tsconfig.json} +0 -0
  101. /package/template/{vitest.config.ts → server/vitest.config.ts} +0 -0
@@ -0,0 +1,92 @@
1
+ # __APP_NAME__ frontend
2
+
3
+ This is the Astram framework's **identity frontend** — login, session, users, roles,
4
+ companies, access, audit — scaffolded straight into your app. It is a real,
5
+ production-shaped Vite/React UI (shadcn/ui + Tailwind v4), not a placeholder.
6
+
7
+ ## What this is
8
+
9
+ The files under `frontend/` are **yours** — you own and edit them like any other
10
+ part of your app. The framework does not manage or overwrite them once they're
11
+ here.
12
+
13
+ The actual auth/session/API **logic** does NOT live in these files — it comes from
14
+ two npm packages:
15
+
16
+ - **`@astram/frontend-core`** — auth flows, session handling, HTTP client wiring.
17
+ - **`@astram/sdk`** — typed calls against the framework API (users, roles,
18
+ companies, access, audit).
19
+
20
+ That split is what makes this model work: the **view** (these files: pages,
21
+ components, routing, styling) is scaffolded once and is fully yours to change;
22
+ the **logic** (auth correctness, new API surface, bug fixes) ships as ordinary
23
+ npm package updates you pull in with `npm update`, without touching a single file
24
+ in this folder.
25
+
26
+ ## Run
27
+
28
+ ```bash
29
+ npm install
30
+ npm run dev
31
+ ```
32
+
33
+ - Opens the identity UI on **http://localhost:5173**.
34
+ - Log in at **`/login`**.
35
+ - The Vite dev server proxies **`/api`** to your backend on **`http://localhost:3000`**
36
+ (see `vite.config.js`) — the browser calls same-origin `/api/auth/login`, and the
37
+ proxy rewrites it to `/auth/login` on the server. Override the target with the
38
+ `API_URL` env var if your backend runs elsewhere.
39
+
40
+ Make sure the framework backend (`../server`) is running first — this UI has
41
+ nothing to talk to otherwise.
42
+
43
+ ## Re-skin
44
+
45
+ You don't need to touch component code to make this look like your product:
46
+
47
+ - **Name / logo** — copy `.env.example` to `.env` and set:
48
+ - `VITE_APP_NAME` — shown in the header/title instead of the default.
49
+ - `VITE_APP_LOGO` — path/URL to your logo (light theme).
50
+ - `VITE_APP_LOGO_DARK` — path/URL to your logo (dark theme).
51
+ - `VITE_APP_TAGLINE` / `VITE_APP_SUB_TAGLINE` — login-screen copy.
52
+ - **Theme color** — edit the `--primary` / `--primary-foreground` oklch pair in
53
+ `src/index.css`: the `:root` block controls light mode, the `.dark` block
54
+ controls dark mode. Every primary button, focus ring, and accent in the UI
55
+ derives from this one token pair — change it once, it propagates everywhere.
56
+
57
+ Both of these are **additive, config-only** customizations — they don't modify
58
+ any scaffolded file, so a future framework/template update never conflicts with
59
+ your branding.
60
+
61
+ ## Updates / no-clobber model
62
+
63
+ Two different things update on two different schedules, and it's important to
64
+ know which is which:
65
+
66
+ 1. **Logic (`@astram/frontend-core`, `@astram/sdk`)** updates like any other npm
67
+ dependency — `npm update` pulls in bug fixes and new API methods for free. You
68
+ never edit these packages directly (they live in `node_modules`).
69
+
70
+ 2. **View files (everything under `frontend/src/`, `frontend/index.html`, etc.)**
71
+ are yours. They were copied in ONCE, by either `create-astram-app --with-frontend`
72
+ at scaffold time or by `npx astram-add-frontend` afterwards. Nothing in the
73
+ framework reaches back into this folder to overwrite your edits — this file, a
74
+ page you've re-themed, a component you've customized, all survive indefinitely.
75
+
76
+ 3. **Re-running `npx astram-add-frontend` is safe, any time.** It's additive and
77
+ idempotent: it only writes files that are still missing (e.g. the published
78
+ template gained a new page or component since you last ran it). Any file that
79
+ already exists in your `frontend/` — including one you've heavily edited — is
80
+ left completely untouched. It will never overwrite your work.
81
+
82
+ 4. **Adopting an improved framework screen is a manual, deliberate step**, not an
83
+ automatic one — by design, so you're never surprised by a silent overwrite. If
84
+ a newer `create-astram-app` release improves a page you're using unmodified
85
+ (e.g. a better `Login.jsx`), diff your version against the current template
86
+ (published inside the `create-astram-app` npm package, under `template/frontend/`)
87
+ and cherry-pick the parts you want.
88
+
89
+ The practical effect: keep your branding in `.env` + the `--primary` token instead
90
+ of hand-editing component internals wherever you can — it keeps your view files
91
+ close to the upstream template, which makes adopting future improvements a small
92
+ diff instead of a merge conflict.
@@ -0,0 +1,16 @@
1
+ # frontend
2
+
3
+ Your UI lives here. This folder is intentionally empty — the Astram framework is a
4
+ **backend security kernel**, so it does not impose a frontend framework. Add whatever
5
+ you like (Vite + React, Next.js, SvelteKit, a mobile app's web build, …) and point it
6
+ at the API in `../server` (default `http://localhost:3000`).
7
+
8
+ The API's auth surface is ready to consume:
9
+
10
+ - `POST /auth/login` `{ identityHint, password }` → `{ session, tokens: { accessToken, refreshToken } }`
11
+ - `POST /auth/refresh` `{ refreshToken }` → new tokens
12
+ - `POST /auth/logout` `{ sessionId }` → 204
13
+ - Protected routes: send `Authorization: Bearer <accessToken>`. A `@Classified` route
14
+ is enforced by the framework — no token → 401, denied by policy → 403.
15
+
16
+ See `../server/src/features/` for the API features your UI will call.
@@ -0,0 +1,6 @@
1
+ # __APP_NAME__ frontend — copy to .env; all optional (astram defaults apply).
2
+ # VITE_APP_NAME=My App
3
+ # VITE_APP_LOGO=/my-logo.png
4
+ # VITE_APP_LOGO_DARK=/my-logo-dark.png
5
+ # VITE_APP_TAGLINE=...
6
+ # VITE_APP_SUB_TAGLINE=...
@@ -0,0 +1,9 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .DS_Store
5
+
6
+ # Local env files (real values). `.env.example` is committed as the template.
7
+ .env
8
+ .env.*
9
+ !.env.example
@@ -0,0 +1,71 @@
1
+ // Frontend ESLint flat config (ESLint 9) for an Astram consumer app.
2
+ //
3
+ // React + hooks + fast-refresh rules over the JS/JSX source. The `lint` script
4
+ // (`eslint "src/**/*.{js,jsx}"`) resolves this file. The Vite build is the
5
+ // correctness gate; this catches unused vars, hook-dependency mistakes, and
6
+ // fast-refresh foot-guns. Uses the framework's `_`-prefixed-unused convention.
7
+ import js from '@eslint/js';
8
+ import globals from 'globals';
9
+ import react from 'eslint-plugin-react';
10
+ import reactHooks from 'eslint-plugin-react-hooks';
11
+ import reactRefresh from 'eslint-plugin-react-refresh';
12
+
13
+ export default [
14
+ {
15
+ ignores: ['node_modules/**', 'dist/**', 'coverage/**', 'eslint.config.js'],
16
+ },
17
+
18
+ js.configs.recommended,
19
+
20
+ {
21
+ files: ['src/**/*.{js,jsx}'],
22
+ languageOptions: {
23
+ ecmaVersion: 2022,
24
+ sourceType: 'module',
25
+ globals: { ...globals.browser },
26
+ parserOptions: {
27
+ ecmaFeatures: { jsx: true },
28
+ },
29
+ },
30
+ settings: {
31
+ react: { version: 'detect' },
32
+ },
33
+ plugins: {
34
+ react,
35
+ 'react-hooks': reactHooks,
36
+ 'react-refresh': reactRefresh,
37
+ },
38
+ rules: {
39
+ ...react.configs.flat.recommended.rules,
40
+ ...react.configs.flat['jsx-runtime'].rules, // React 18 automatic JSX runtime — no `React` import needed.
41
+ ...reactHooks.configs.flat.recommended.rules,
42
+
43
+ // Allow `_`-prefixed intentionally-unused args/vars (shared convention).
44
+ 'no-unused-vars': [
45
+ 'error',
46
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
47
+ ],
48
+
49
+ // Fast-refresh: warn (not error) when a module exports non-components
50
+ // alongside components — common + harmless in shadcn ui files.
51
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
52
+
53
+ // Project convention: PropTypes are not used (no `prop-types` dep).
54
+ 'react/prop-types': 'off',
55
+
56
+ // react-hooks 7.x ships new, very aggressive rules that flag idiomatic
57
+ // patterns (a mount-guard `setState` in an effect, a fetch-on-mount
58
+ // `load()` effect, the lazy-ref-init `if (!ref.current) ref.current = …`
59
+ // one-time init, etc.). These were REVIEWED case-by-case (task #138): every
60
+ // current hit is a known-good idiom, not a bug — promoting them to error
61
+ // would force contorting clean code, so they stay ADVISORY (warn): visible,
62
+ // non-blocking. The genuinely load-bearing hooks rules below stay errors.
63
+ // If a future hit is an actual bug, fix that site; don't relax the warn.
64
+ 'react-hooks/set-state-in-effect': 'warn',
65
+ 'react-hooks/refs': 'warn',
66
+ // These two catch real bugs — keep them blocking.
67
+ 'react-hooks/exhaustive-deps': 'error',
68
+ 'react-hooks/rules-of-hooks': 'error',
69
+ },
70
+ },
71
+ ];
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en" suppressHydrationWarning>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <link rel="icon" type="image/png" href="/favicon.png" />
7
+ <title>__APP_NAME__</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "__APP_NAME__",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "The frontend (UI) half of an Astram consumer app — a professional web UI (login / auth / identity management) over the framework auth API. Built on shadcn/ui + Tailwind v4; shared auth/http logic comes from @astram/frontend-core and @astram/sdk. The API (../server) lives at http://localhost:3000 by default.",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "vite build",
10
+ "preview": "vite preview",
11
+ "lint": "eslint \"src/**/*.{js,jsx}\"",
12
+ "test": "node --test src/**/*.test.mjs"
13
+ },
14
+ "dependencies": {
15
+ "@astram/frontend-core": "^0.1.0",
16
+ "@astram/sdk": "^0.2.0",
17
+ "@radix-ui/react-alert-dialog": "^1.1.19",
18
+ "@radix-ui/react-avatar": "^1.1.10",
19
+ "@radix-ui/react-checkbox": "^1.1.4",
20
+ "@radix-ui/react-dialog": "^1.1.15",
21
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
22
+ "@radix-ui/react-label": "^2.1.7",
23
+ "@radix-ui/react-select": "^2.3.3",
24
+ "@radix-ui/react-separator": "^1.1.7",
25
+ "@radix-ui/react-slot": "^1.2.3",
26
+ "@radix-ui/react-tooltip": "^1.2.8",
27
+ "class-variance-authority": "^0.7.1",
28
+ "clsx": "^2.1.1",
29
+ "input-otp": "^1.4.2",
30
+ "lucide-react": "^0.460.0",
31
+ "next-themes": "^0.4.6",
32
+ "qrcode": "^1.5.4",
33
+ "react": "^18.3.1",
34
+ "react-dom": "^18.3.1",
35
+ "react-router-dom": "^6.26.0",
36
+ "sonner": "^2.0.7",
37
+ "tailwind-merge": "^3.3.1"
38
+ },
39
+ "devDependencies": {
40
+ "@eslint/js": "^9.39.5",
41
+ "@tailwindcss/vite": "^4.1.14",
42
+ "@vitejs/plugin-react": "^4.3.0",
43
+ "eslint": "^9.39.5",
44
+ "eslint-plugin-react": "^7.37.5",
45
+ "eslint-plugin-react-hooks": "^7.1.1",
46
+ "eslint-plugin-react-refresh": "^0.5.3",
47
+ "globals": "^17.7.0",
48
+ "tailwindcss": "^4.1.14",
49
+ "tw-animate-css": "^1.4.0",
50
+ "vite": "^5.4.0"
51
+ }
52
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * App — routing + auth guards.
3
+ *
4
+ * /login, /forgot-password → public auth screens (redirect to /dashboard if
5
+ * already signed in)
6
+ * /dashboard → protected (RequireAuth bounces to /login, saving the
7
+ * return path)
8
+ */
9
+
10
+ import { useEffect } from 'react';
11
+ import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
12
+ import { useAuth, useCan } from './auth/AuthContext';
13
+ import { saveReturnPath } from './platform/returnPathStore';
14
+ import Login from './pages/Login';
15
+ import ForgotPassword from './pages/ForgotPassword';
16
+ import ResetPassword from './pages/ResetPassword';
17
+ import Dashboard from './pages/Dashboard';
18
+ import AccountLayout from './pages/account/AccountLayout';
19
+ import ProfilePage from './pages/account/ProfilePage';
20
+ import ChangePasswordPage from './pages/account/ChangePasswordPage';
21
+ import TwoFactorPage from './pages/account/TwoFactorPage';
22
+ import ReenrollPage from './pages/account/ReenrollPage';
23
+ import CompaniesPage from './pages/companies/CompaniesPage';
24
+ import CompanyForm from './pages/companies/CompanyForm';
25
+ import RolesPage from './pages/roles/RolesPage';
26
+ import RoleForm from './pages/roles/RoleForm';
27
+ import UsersPage from './pages/users/UsersPage';
28
+ import UserForm from './pages/users/UserForm';
29
+ import AccessPage from './pages/access/AccessPage';
30
+ import AccessForm from './pages/access/AccessForm';
31
+ import AuditPage from './pages/audit/AuditPage';
32
+
33
+ /** The single forced-re-enroll screen path (the enforcement's frontend twin). */
34
+ const REENROLL_PATH = '/account/reenroll';
35
+
36
+ /** Only-when-signed-out (auth screens bounce authenticated users to the app). */
37
+ function PublicOnly({ children }) {
38
+ const { isAuthenticated } = useAuth();
39
+ return isAuthenticated ? <Navigate to="/dashboard" replace /> : children;
40
+ }
41
+
42
+ /**
43
+ * Only-when-signed-in (protected pages bounce to login, remembering where you were).
44
+ *
45
+ * ALSO the frontend twin of the server-side re-enroll gate: when the session is
46
+ * `mustReenroll` (recovery-code login), every protected route is pinned to the forced
47
+ * re-enroll screen — mirroring the server 403'ing every non-allowlisted route. The
48
+ * enforcement is server-side (this is UX, not the security boundary); it just spares
49
+ * the user a bounce-through-403 on each click until a device is confirmed.
50
+ *
51
+ * `gate` (Phase E slice 1, optional): a permission KEY required to render this route.
52
+ * When absent from the caller's auth-facts projection, redirect to `/dashboard` instead
53
+ * of rendering — sparing a direct-navigation bounce-through-403 the same way `mustReenroll`
54
+ * does above. UX ONLY: the server's decide() on the page's underlying @Classified route
55
+ * remains the real enforcement regardless of this check (a stale/tampered projection just
56
+ * means the user reaches the page and then gets 403'd by its data calls).
57
+ */
58
+ function RequireAuth({ children, gate }) {
59
+ const { isAuthenticated, mustReenroll } = useAuth();
60
+ // Called UNCONDITIONALLY (Rules of Hooks) — `useCan(undefined)` simply resolves to
61
+ // `false` when no gate is set, so the ungated case is handled by short-circuiting on
62
+ // `gate &&` below rather than skipping the hook call itself.
63
+ const canGate = useCan(gate);
64
+ const allowed = !gate || canGate;
65
+ const location = useLocation();
66
+ // Remember where the user was so the next login returns them here. Persisted to
67
+ // localStorage (via saveReturnPath) so it survives a full reload / browser restart —
68
+ // the in-memory `from` router state below only helps within the same page session.
69
+ // Done in an effect (a render-time side-effect would be impure); path+search so a
70
+ // deep-linked list page (e.g. ?page=2) restores exactly.
71
+ const fullPath = location.pathname + location.search;
72
+ useEffect(() => {
73
+ if (!isAuthenticated) saveReturnPath(fullPath);
74
+ }, [isAuthenticated, fullPath]);
75
+
76
+ if (!isAuthenticated) {
77
+ return <Navigate to="/login" replace state={{ from: fullPath }} />;
78
+ }
79
+ if (mustReenroll && location.pathname !== REENROLL_PATH) {
80
+ return <Navigate to={REENROLL_PATH} replace />;
81
+ }
82
+ if (gate && !allowed) {
83
+ return <Navigate to="/dashboard" replace />;
84
+ }
85
+ return children;
86
+ }
87
+
88
+ export default function App() {
89
+ return (
90
+ <Routes>
91
+ <Route path="/login" element={<PublicOnly><Login /></PublicOnly>} />
92
+ <Route path="/forgot-password" element={<PublicOnly><ForgotPassword /></PublicOnly>} />
93
+ <Route path="/reset-password" element={<PublicOnly><ResetPassword /></PublicOnly>} />
94
+ <Route path="/dashboard" element={<RequireAuth><Dashboard /></RequireAuth>} />
95
+ {/* Forced re-enroll — a standalone full-screen flow (NOT an account tab). While
96
+ the session is `mustReenroll`, RequireAuth pins every other protected route
97
+ here; this path is the one RequireAuth lets through so the screen can load. */}
98
+ <Route path="/account/reenroll" element={<RequireAuth><ReenrollPage /></RequireAuth>} />
99
+ {/* Account — a persistent layout with per-tab child routes (bookmarkable, back
100
+ works). /account redirects to the Profile tab. */}
101
+ <Route path="/account" element={<RequireAuth><AccountLayout /></RequireAuth>}>
102
+ <Route index element={<Navigate to="/account/profile" replace />} />
103
+ <Route path="profile" element={<ProfilePage />} />
104
+ <Route path="password" element={<ChangePasswordPage />} />
105
+ <Route path="2fa" element={<TwoFactorPage />} />
106
+ </Route>
107
+ {/* Identity management — each list is a LAYOUT route; add/edit are child routes
108
+ that render as a routed-modal overlay (bookmarkable, back button closes).
109
+ `gate` (Phase E slice 1) mirrors the Sidebar's nav gating — VERIFIED against
110
+ each page's actual @Classified list route (see shell/nav.js's header doc). */}
111
+ <Route
112
+ path="/companies"
113
+ element={
114
+ <RequireAuth gate="identity:company:read">
115
+ <CompaniesPage />
116
+ </RequireAuth>
117
+ }
118
+ >
119
+ <Route path="add" element={<CompanyForm />} />
120
+ <Route path="edit/:uuid" element={<CompanyForm />} />
121
+ </Route>
122
+ <Route
123
+ path="/roles"
124
+ element={
125
+ <RequireAuth gate="identity:role:read">
126
+ <RolesPage />
127
+ </RequireAuth>
128
+ }
129
+ >
130
+ <Route path="add" element={<RoleForm />} />
131
+ <Route path="edit/:uuid" element={<RoleForm />} />
132
+ </Route>
133
+ <Route
134
+ path="/users"
135
+ element={
136
+ <RequireAuth gate="identity:user:read">
137
+ <UsersPage />
138
+ </RequireAuth>
139
+ }
140
+ >
141
+ <Route path="add" element={<UserForm />} />
142
+ <Route path="edit/:uuid" element={<UserForm />} />
143
+ </Route>
144
+ <Route
145
+ path="/access"
146
+ element={
147
+ <RequireAuth gate="identity:role:read">
148
+ <AccessPage />
149
+ </RequireAuth>
150
+ }
151
+ >
152
+ <Route path="add" element={<AccessForm />} />
153
+ <Route path="edit/:uuid" element={<AccessForm />} />
154
+ </Route>
155
+ {/* Audit — read-only, no child add/edit routes (audit events are immutable). */}
156
+ <Route
157
+ path="/audit"
158
+ element={
159
+ <RequireAuth gate="identity:role:read">
160
+ <AuditPage />
161
+ </RequireAuth>
162
+ }
163
+ />
164
+ <Route path="*" element={<Navigate to="/dashboard" replace />} />
165
+ </Routes>
166
+ );
167
+ }