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.
- package/dist/add-frontend.js +107 -0
- package/dist/add-frontend.js.map +1 -0
- package/dist/index.js +132 -33
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/template/CLAUDE.md +51 -0
- package/template/README.md +43 -44
- package/template/_gitignore +6 -0
- package/template/frontend/FRONTEND.md +92 -0
- package/template/frontend/README.md +16 -0
- package/template/frontend/_env.example +6 -0
- package/template/frontend/_gitignore +9 -0
- package/template/frontend/eslint.config.js +71 -0
- package/template/frontend/index.html +13 -0
- package/template/frontend/package.json +52 -0
- package/template/frontend/public/astram-logo-full.png +0 -0
- package/template/frontend/public/astram-logo-white.png +0 -0
- package/template/frontend/public/favicon.png +0 -0
- package/template/frontend/src/App.jsx +167 -0
- package/template/frontend/src/auth/AuthContext.jsx +338 -0
- package/template/frontend/src/components/ConfirmDialog.jsx +92 -0
- package/template/frontend/src/components/EntityFormModal.jsx +154 -0
- package/template/frontend/src/components/EntityList.jsx +480 -0
- package/template/frontend/src/components/PageHeader.jsx +54 -0
- package/template/frontend/src/components/access/CollapsibleGroup.jsx +34 -0
- package/template/frontend/src/components/ui/alert-dialog.tsx +175 -0
- package/template/frontend/src/components/ui/alert.tsx +83 -0
- package/template/frontend/src/components/ui/avatar.tsx +51 -0
- package/template/frontend/src/components/ui/badge.tsx +56 -0
- package/template/frontend/src/components/ui/button.tsx +67 -0
- package/template/frontend/src/components/ui/card.tsx +92 -0
- package/template/frontend/src/components/ui/checkbox.tsx +30 -0
- package/template/frontend/src/components/ui/date-field.tsx +47 -0
- package/template/frontend/src/components/ui/dialog.tsx +214 -0
- package/template/frontend/src/components/ui/dropdown-menu.tsx +255 -0
- package/template/frontend/src/components/ui/input-otp.tsx +81 -0
- package/template/frontend/src/components/ui/input.tsx +21 -0
- package/template/frontend/src/components/ui/label.tsx +46 -0
- package/template/frontend/src/components/ui/searchable-select.tsx +459 -0
- package/template/frontend/src/components/ui/select.tsx +199 -0
- package/template/frontend/src/components/ui/separator.tsx +26 -0
- package/template/frontend/src/components/ui/sheet.tsx +139 -0
- package/template/frontend/src/components/ui/skeleton.tsx +13 -0
- package/template/frontend/src/components/ui/sonner.tsx +23 -0
- package/template/frontend/src/components/ui/table.tsx +181 -0
- package/template/frontend/src/components/ui/tooltip.tsx +59 -0
- package/template/frontend/src/config/branding.js +80 -0
- package/template/frontend/src/hooks/useMobile.tsx +21 -0
- package/template/frontend/src/index.css +265 -0
- package/template/frontend/src/lib/utils.js +7 -0
- package/template/frontend/src/main.jsx +27 -0
- package/template/frontend/src/pages/Dashboard.jsx +127 -0
- package/template/frontend/src/pages/ForgotPassword.jsx +104 -0
- package/template/frontend/src/pages/Login.jsx +393 -0
- package/template/frontend/src/pages/ResetPassword.jsx +159 -0
- package/template/frontend/src/pages/access/AccessForm.jsx +416 -0
- package/template/frontend/src/pages/access/AccessPage.jsx +282 -0
- package/template/frontend/src/pages/access/RoleActionsMenu.jsx +42 -0
- package/template/frontend/src/pages/account/AccountLayout.jsx +229 -0
- package/template/frontend/src/pages/account/ChangePasswordPage.jsx +235 -0
- package/template/frontend/src/pages/account/ProfilePage.jsx +122 -0
- package/template/frontend/src/pages/account/ReenrollPage.jsx +98 -0
- package/template/frontend/src/pages/account/TwoFactorPage.jsx +833 -0
- package/template/frontend/src/pages/audit/AuditPage.jsx +268 -0
- package/template/frontend/src/pages/companies/CompaniesPage.jsx +232 -0
- package/template/frontend/src/pages/companies/CompanyForm.jsx +127 -0
- package/template/frontend/src/pages/roles/RoleForm.jsx +99 -0
- package/template/frontend/src/pages/roles/RolesPage.jsx +207 -0
- package/template/frontend/src/pages/users/AssignmentsEditor.jsx +193 -0
- package/template/frontend/src/pages/users/MembershipCard.jsx +303 -0
- package/template/frontend/src/pages/users/PostingRow.jsx +264 -0
- package/template/frontend/src/pages/users/UserForm.jsx +167 -0
- package/template/frontend/src/pages/users/UsersPage.jsx +232 -0
- package/template/frontend/src/platform/httpClient.js +125 -0
- package/template/frontend/src/platform/returnPathStore.js +43 -0
- package/template/frontend/src/platform/sessionStore.js +29 -0
- package/template/frontend/src/platform/shieldTransport.js +172 -0
- package/template/frontend/src/platform/shieldTransport.test.mjs +245 -0
- package/template/frontend/src/shell/AppShell.jsx +237 -0
- package/template/frontend/src/shell/Sidebar.jsx +257 -0
- package/template/frontend/src/shell/nav.js +90 -0
- package/template/frontend/src/theme/ThemeProvider.jsx +14 -0
- package/template/frontend/src/theme/ThemeToggle.jsx +44 -0
- package/template/frontend/tsconfig.json +22 -0
- package/template/frontend/vite.config.js +30 -0
- package/template/server/_env.example +45 -0
- package/template/server/eslint.config.js +95 -0
- package/template/server/package.json +49 -0
- package/template/server/scripts/dev.mjs +51 -0
- package/template/server/scripts/sync-astram-guide.mjs +63 -0
- package/template/_env.example +0 -33
- package/template/package.json +0 -37
- /package/template/{src → server/src}/app.module.ts +0 -0
- /package/template/{src → server/src}/features/_gitkeep +0 -0
- /package/template/{src → server/src}/health/demo.controller.ts +0 -0
- /package/template/{src → server/src}/health/health.controller.ts +0 -0
- /package/template/{src → server/src}/main.ts +0 -0
- /package/template/{test → server/test}/enforcement.e2e.spec.ts +0 -0
- /package/template/{tsconfig.build.json → server/tsconfig.build.json} +0 -0
- /package/template/{tsconfig.json → server/tsconfig.json} +0 -0
- /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,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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|