create-astram-app 0.1.3 → 0.3.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 +122 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/template/frontend/FRONTEND.md +92 -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 +191 -0
- package/template/frontend/src/auth/AuthContext.jsx +343 -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/cross-tenant/GrantForm.jsx +337 -0
- package/template/frontend/src/pages/cross-tenant/GrantsPage.jsx +237 -0
- package/template/frontend/src/pages/cross-tenant/InboxPage.jsx +179 -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 +103 -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 +27 -15
- package/template/server/package.json +7 -4
- package/template/server/src/app.module.ts +28 -2
- package/template/server/src/health/demo.controller.ts +23 -12
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import 'tw-animate-css';
|
|
3
|
+
/* Inter (UI) + JetBrains Mono (code/data) — the professional SaaS standard. */
|
|
4
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
astram3 design tokens — shadcn/ui + Tailwind v4 (CSS-var driven).
|
|
8
|
+
|
|
9
|
+
Foundation adopted from the ffs-assessment reference (shadcn "new-york" +
|
|
10
|
+
Tailwind v4 @theme inline), re-BRANDED to astram3 (the #3366ff blue) with a
|
|
11
|
+
polished neutral ramp + a deep-navy collapsible sidebar. Light + dark are
|
|
12
|
+
first-class (OCA-quality dual theme) — the viewer's next-themes choice stamps
|
|
13
|
+
`.dark` on <html>.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
@custom-variant dark (&:is(.dark *));
|
|
17
|
+
|
|
18
|
+
@theme inline {
|
|
19
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
20
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
21
|
+
--radius-lg: var(--radius);
|
|
22
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
23
|
+
--color-background: var(--background);
|
|
24
|
+
--color-foreground: var(--foreground);
|
|
25
|
+
--color-card: var(--card);
|
|
26
|
+
--color-card-foreground: var(--card-foreground);
|
|
27
|
+
--color-popover: var(--popover);
|
|
28
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
29
|
+
--color-primary: var(--primary);
|
|
30
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
31
|
+
--color-secondary: var(--secondary);
|
|
32
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
+
--color-muted: var(--muted);
|
|
34
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
35
|
+
--color-accent: var(--accent);
|
|
36
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
37
|
+
--color-destructive: var(--destructive);
|
|
38
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
39
|
+
--color-border: var(--border);
|
|
40
|
+
--color-input: var(--input);
|
|
41
|
+
--color-ring: var(--ring);
|
|
42
|
+
--color-ok: var(--ok);
|
|
43
|
+
--color-warn: var(--warn);
|
|
44
|
+
--color-sidebar: var(--sidebar);
|
|
45
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
46
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
47
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
48
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
49
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
50
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
51
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:root {
|
|
55
|
+
--radius: 0.5rem;
|
|
56
|
+
|
|
57
|
+
/* Brand — the astram3 blue (#3366ff family) in oklch. */
|
|
58
|
+
--primary: oklch(0.55 0.216 265);
|
|
59
|
+
--primary-foreground: oklch(1 0 0);
|
|
60
|
+
|
|
61
|
+
/* Canvas + neutrals — a slight blue-bias ramp (chosen, not default grey). */
|
|
62
|
+
--background: oklch(1 0 0);
|
|
63
|
+
--foreground: oklch(0.16 0.02 260);
|
|
64
|
+
--card: oklch(1 0 0);
|
|
65
|
+
--card-foreground: oklch(0.16 0.02 260);
|
|
66
|
+
--popover: oklch(1 0 0);
|
|
67
|
+
--popover-foreground: oklch(0.16 0.02 260);
|
|
68
|
+
--secondary: oklch(0.965 0.01 260);
|
|
69
|
+
--secondary-foreground: oklch(0.28 0.04 260);
|
|
70
|
+
--muted: oklch(0.965 0.008 260);
|
|
71
|
+
--muted-foreground: oklch(0.52 0.02 260);
|
|
72
|
+
--accent: oklch(0.955 0.02 265);
|
|
73
|
+
--accent-foreground: oklch(0.3 0.08 265);
|
|
74
|
+
--destructive: oklch(0.58 0.22 25);
|
|
75
|
+
--destructive-foreground: oklch(1 0 0);
|
|
76
|
+
--ok: oklch(0.62 0.15 155);
|
|
77
|
+
--warn: oklch(0.68 0.16 65);
|
|
78
|
+
--border: oklch(0.9 0.012 260);
|
|
79
|
+
--input: oklch(0.92 0.01 260);
|
|
80
|
+
--ring: oklch(0.55 0.216 265);
|
|
81
|
+
|
|
82
|
+
/* Sidebar — LIGHT-GRAY panel in light mode (OCA-style): a soft off-white that sits
|
|
83
|
+
just below the white content so the rail reads as its own region, separated by a
|
|
84
|
+
hairline. `-foreground` is dark ink; `-accent` is the hover/nav-hover tint; the
|
|
85
|
+
border is a light hairline. Dark mode (below) keeps the deep-navy rail. */
|
|
86
|
+
--sidebar: oklch(0.975 0.004 260);
|
|
87
|
+
--sidebar-foreground: oklch(0.28 0.02 262);
|
|
88
|
+
--sidebar-primary: oklch(0.55 0.216 265);
|
|
89
|
+
--sidebar-primary-foreground: oklch(1 0 0);
|
|
90
|
+
--sidebar-accent: oklch(0.94 0.006 260);
|
|
91
|
+
--sidebar-accent-foreground: oklch(0.28 0.02 262);
|
|
92
|
+
--sidebar-border: oklch(0.9 0.012 260);
|
|
93
|
+
--sidebar-ring: oklch(0.55 0.216 265);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.dark {
|
|
97
|
+
/* A crisper, more saturated brand blue in dark — the old value read washed-out on
|
|
98
|
+
the dark card surface. Foreground is a near-white so the primary button + solid
|
|
99
|
+
avatar fills have strong contrast (not the muted dark-on-pale-blue of before). */
|
|
100
|
+
--primary: oklch(0.62 0.208 264);
|
|
101
|
+
--primary-foreground: oklch(0.99 0 0);
|
|
102
|
+
|
|
103
|
+
/* Canvas — a deep near-black navy so LIFTED cards separate clearly from it. */
|
|
104
|
+
--background: oklch(0.145 0.018 265);
|
|
105
|
+
--foreground: oklch(0.955 0.006 250);
|
|
106
|
+
|
|
107
|
+
/* Cards — a distinctly lighter panel than the canvas (≈+0.06 L) with a visible
|
|
108
|
+
top-light border, so surfaces read as raised, not flat-on-flat. */
|
|
109
|
+
--card: oklch(0.205 0.02 264);
|
|
110
|
+
--card-foreground: oklch(0.955 0.006 250);
|
|
111
|
+
--popover: oklch(0.215 0.022 264);
|
|
112
|
+
--popover-foreground: oklch(0.955 0.006 250);
|
|
113
|
+
|
|
114
|
+
--secondary: oklch(0.26 0.022 263);
|
|
115
|
+
--secondary-foreground: oklch(0.9 0.008 250);
|
|
116
|
+
--muted: oklch(0.24 0.02 263);
|
|
117
|
+
/* Brighter muted text — the old value was too dim for secondary labels. */
|
|
118
|
+
--muted-foreground: oklch(0.72 0.018 258);
|
|
119
|
+
--accent: oklch(0.3 0.035 263);
|
|
120
|
+
--accent-foreground: oklch(0.96 0.006 250);
|
|
121
|
+
--destructive: oklch(0.7 0.19 22);
|
|
122
|
+
--destructive-foreground: oklch(0.98 0 0);
|
|
123
|
+
--ok: oklch(0.72 0.15 155);
|
|
124
|
+
--warn: oklch(0.78 0.15 70);
|
|
125
|
+
/* Crisper hairlines — a touch more visible than 10% so panels have a clean edge. */
|
|
126
|
+
--border: oklch(1 0 0 / 12%);
|
|
127
|
+
--input: oklch(1 0 0 / 16%);
|
|
128
|
+
--ring: oklch(0.62 0.18 262);
|
|
129
|
+
|
|
130
|
+
/* Sidebar — deep navy in dark mode (light mode uses the light-gray panel from
|
|
131
|
+
:root above). Sits a touch DARKER than the dark-mode canvas so the rail still
|
|
132
|
+
reads as its own region; the interactive tokens are tuned for the dark surface. */
|
|
133
|
+
--sidebar: oklch(0.135 0.03 265);
|
|
134
|
+
--sidebar-foreground: oklch(0.9 0.012 250);
|
|
135
|
+
--sidebar-primary: oklch(0.66 0.18 262);
|
|
136
|
+
--sidebar-primary-foreground: oklch(1 0 0);
|
|
137
|
+
--sidebar-accent: oklch(0.27 0.05 263);
|
|
138
|
+
--sidebar-accent-foreground: oklch(0.98 0 0);
|
|
139
|
+
--sidebar-border: oklch(1 0 0 / 8%);
|
|
140
|
+
--sidebar-ring: oklch(0.62 0.18 262);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@theme {
|
|
144
|
+
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
145
|
+
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@layer base {
|
|
149
|
+
/* Base type scale: 13.5px root so the whole UI reads denser + more professional. */
|
|
150
|
+
html {
|
|
151
|
+
font-size: 13.5px;
|
|
152
|
+
}
|
|
153
|
+
* {
|
|
154
|
+
@apply border-border outline-ring/50;
|
|
155
|
+
}
|
|
156
|
+
body {
|
|
157
|
+
@apply bg-background text-foreground font-sans antialiased;
|
|
158
|
+
letter-spacing: -0.006em;
|
|
159
|
+
}
|
|
160
|
+
code,
|
|
161
|
+
pre,
|
|
162
|
+
.font-mono {
|
|
163
|
+
font-family: var(--font-mono);
|
|
164
|
+
}
|
|
165
|
+
/* Tailwind v4 dropped the default button cursor — restore pointer on interactive
|
|
166
|
+
elements (matches the ffs reference). Disabled controls keep not-allowed. */
|
|
167
|
+
button:not(:disabled),
|
|
168
|
+
a,
|
|
169
|
+
[role='button']:not([aria-disabled='true']),
|
|
170
|
+
label[for],
|
|
171
|
+
summary {
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
}
|
|
174
|
+
button:disabled,
|
|
175
|
+
[aria-disabled='true'] {
|
|
176
|
+
cursor: not-allowed;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@layer components {
|
|
181
|
+
/* Subtle dot-grid for auth/content backgrounds (astram-tinted). */
|
|
182
|
+
.grid-bg {
|
|
183
|
+
background-image:
|
|
184
|
+
radial-gradient(circle at 1px 1px, color-mix(in oklch, var(--primary) 12%, transparent) 1px, transparent 0);
|
|
185
|
+
background-size: 22px 22px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Content dot-grid (lighter, for the app content area) — the ffs "engineering" feel. */
|
|
189
|
+
.engineering-grid {
|
|
190
|
+
background-image: radial-gradient(
|
|
191
|
+
circle,
|
|
192
|
+
color-mix(in oklch, var(--foreground) 8%, transparent) 1px,
|
|
193
|
+
transparent 1px
|
|
194
|
+
);
|
|
195
|
+
background-size: 28px 28px;
|
|
196
|
+
}
|
|
197
|
+
/* In dark mode the light foreground makes the grid too loud — dial it right down
|
|
198
|
+
so it's a whisper of texture behind the cards, not a visible pattern. */
|
|
199
|
+
.dark .engineering-grid {
|
|
200
|
+
background-image: radial-gradient(
|
|
201
|
+
circle,
|
|
202
|
+
color-mix(in oklch, var(--foreground) 4%, transparent) 1px,
|
|
203
|
+
transparent 1px
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Deep-navy sidebar surface. A right-edge hairline separates it from the content
|
|
208
|
+
(crucial in dark mode, where the rail and canvas are close in value). */
|
|
209
|
+
.sidebar-panel {
|
|
210
|
+
background: var(--sidebar);
|
|
211
|
+
color: var(--sidebar-foreground);
|
|
212
|
+
border-right: 1px solid var(--sidebar-border);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* In dark mode, `shadow-sm` all but vanishes on a dark card — give shadcn cards a
|
|
216
|
+
deeper ambient shadow + a subtle top-light inset so they read as raised panels
|
|
217
|
+
(professional dark-SaaS depth). Scoped to elements using the `bg-card` token. */
|
|
218
|
+
.dark [data-slot='card'] {
|
|
219
|
+
box-shadow:
|
|
220
|
+
0 1px 0 0 oklch(1 0 0 / 6%) inset,
|
|
221
|
+
0 1px 2px oklch(0 0 0 / 40%),
|
|
222
|
+
0 8px 24px -12px oklch(0 0 0 / 55%);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Primary buttons on a dark card can look flat + washed. Give them a crisp finish:
|
|
226
|
+
a subtle top-light inset + a soft brand-tinted drop shadow so they read as a
|
|
227
|
+
raised, saturated control (professional dark-SaaS button depth). Scoped to the
|
|
228
|
+
shadcn primary variant (bg-primary) inside dark mode. */
|
|
229
|
+
.dark button[data-slot='button'].bg-primary,
|
|
230
|
+
.dark [data-slot='button'].bg-primary {
|
|
231
|
+
box-shadow:
|
|
232
|
+
0 1px 0 0 oklch(1 0 0 / 18%) inset,
|
|
233
|
+
0 1px 2px oklch(0 0 0 / 45%),
|
|
234
|
+
0 4px 14px -6px color-mix(in oklch, var(--primary) 55%, transparent);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Glow ring around the brand tile / accents. */
|
|
238
|
+
.accent-glow {
|
|
239
|
+
box-shadow:
|
|
240
|
+
0 0 0 1px color-mix(in oklch, var(--primary) 35%, transparent),
|
|
241
|
+
0 0 16px color-mix(in oklch, var(--primary) 20%, transparent);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Blurred scrim for the mobile drawer. A fixed dark tint (NOT the --sidebar token,
|
|
245
|
+
which is now light in light mode — a light scrim wouldn't dim the content). */
|
|
246
|
+
.modal-overlay {
|
|
247
|
+
backdrop-filter: blur(6px);
|
|
248
|
+
background: oklch(0.15 0.02 265 / 55%);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* SearchableSelect-in-a-modal pointer-events fix (load-bearing).
|
|
252
|
+
The SearchableSelect panel is portalled to <body>. When it opens inside a Radix
|
|
253
|
+
Dialog (the routed Add/Edit modal), react-popper's OUTER positioning wrapper
|
|
254
|
+
`[data-radix-popper-content-wrapper]` becomes a <body> sibling of the Dialog content,
|
|
255
|
+
so Radix Dialog's RemoveScroll sets `pointer-events: none` on it. That wrapper is
|
|
256
|
+
injected by react-popper and can't take a className, so re-enabling pointer events on
|
|
257
|
+
the inner Content alone doesn't help — hit-testing is swallowed by the dead wrapper
|
|
258
|
+
above it (search box, options, and wheel-scroll all go inert).
|
|
259
|
+
This re-enables pointer events on ONLY that wrapper, scoped via `:has()` to the one
|
|
260
|
+
wrapping our panel (tagged `data-searchable-select`), so no other Radix popper is
|
|
261
|
+
touched. Result: the in-panel search is clickable/typeable and the option list scrolls. */
|
|
262
|
+
[data-radix-popper-content-wrapper]:has(> [data-searchable-select]) {
|
|
263
|
+
pointer-events: auto;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import { ThemeProvider } from '@/theme/ThemeProvider';
|
|
5
|
+
import { AuthProvider } from '@/auth/AuthContext';
|
|
6
|
+
import { Toaster } from '@/components/ui/sonner';
|
|
7
|
+
import App from '@/App';
|
|
8
|
+
import { applyBrandColors } from '@/config/branding';
|
|
9
|
+
import '@/index.css';
|
|
10
|
+
|
|
11
|
+
// Apply the branding color overrides (logo/name/tagline are read in components).
|
|
12
|
+
// Making this a runtime call keeps the door open for per-tenant branding loaded
|
|
13
|
+
// from the server later — swap the config, re-apply, no component changes.
|
|
14
|
+
applyBrandColors();
|
|
15
|
+
|
|
16
|
+
createRoot(document.getElementById('root')).render(
|
|
17
|
+
<StrictMode>
|
|
18
|
+
<ThemeProvider>
|
|
19
|
+
<BrowserRouter>
|
|
20
|
+
<AuthProvider>
|
|
21
|
+
<App />
|
|
22
|
+
<Toaster richColors position="top-right" />
|
|
23
|
+
</AuthProvider>
|
|
24
|
+
</BrowserRouter>
|
|
25
|
+
</ThemeProvider>
|
|
26
|
+
</StrictMode>,
|
|
27
|
+
);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard — the post-login shell + the ACT-AS-USER proof. Inside AppShell (the
|
|
3
|
+
* collapsible sidebar + header). Calls the protected GET /identity/companies WITH
|
|
4
|
+
* the bearer token (the framework resolves it → Principal → decide() → the handler
|
|
5
|
+
* acts as you), and lets you retry WITHOUT a token to see the 401 wall.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
+
import { Building2, RefreshCw, ShieldAlert, ShieldCheck } from 'lucide-react';
|
|
9
|
+
import { useAuth } from '@/auth/AuthContext';
|
|
10
|
+
import AppShell from '@/shell/AppShell';
|
|
11
|
+
import { Button } from '@/components/ui/button';
|
|
12
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
13
|
+
import { Badge } from '@/components/ui/badge';
|
|
14
|
+
import { Separator } from '@/components/ui/separator';
|
|
15
|
+
|
|
16
|
+
export default function Dashboard() {
|
|
17
|
+
const { tokens, identity } = useAuth();
|
|
18
|
+
const [rows, setRows] = useState([]);
|
|
19
|
+
const [status, setStatus] = useState('idle'); // idle | loading | ok | error
|
|
20
|
+
const [message, setMessage] = useState('');
|
|
21
|
+
|
|
22
|
+
const load = useCallback(
|
|
23
|
+
async (withToken = true) => {
|
|
24
|
+
setStatus('loading');
|
|
25
|
+
setMessage('');
|
|
26
|
+
const r = await identity.listCompanies(withToken ? tokens?.accessToken : undefined);
|
|
27
|
+
if (r.ok) {
|
|
28
|
+
setRows(r.rows);
|
|
29
|
+
setStatus('ok');
|
|
30
|
+
setMessage(`200 OK — acted as you; ${r.total} companies visible in your tenant.`);
|
|
31
|
+
} else {
|
|
32
|
+
setRows([]);
|
|
33
|
+
setStatus('error');
|
|
34
|
+
setMessage(
|
|
35
|
+
r.status === 401
|
|
36
|
+
? '401 Unauthorized — the framework denied the request (no/invalid token). The wall working.'
|
|
37
|
+
: r.status === 403
|
|
38
|
+
? '403 Forbidden — authenticated, but your policy denied this action.'
|
|
39
|
+
: `Request failed (${r.status || 'no response'}).`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[identity, tokens],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
load(true);
|
|
48
|
+
}, [load]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<AppShell>
|
|
52
|
+
<div className="p-4">
|
|
53
|
+
<div className="mb-4">
|
|
54
|
+
<h1 className="text-xl font-bold tracking-tight">Dashboard</h1>
|
|
55
|
+
<p className="mt-0.5 text-sm text-muted-foreground">
|
|
56
|
+
You are signed in. Below, the app calls a protected API route as you — the framework enforces it.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<Card className="mb-4">
|
|
61
|
+
<CardHeader className="flex-row items-center justify-between space-y-0">
|
|
62
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
63
|
+
<ShieldCheck className="size-4 text-[var(--ok)]" />
|
|
64
|
+
Act as me
|
|
65
|
+
</CardTitle>
|
|
66
|
+
<Badge variant="secondary">authenticated</Badge>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent className="space-y-3">
|
|
69
|
+
<p className="text-sm text-muted-foreground">
|
|
70
|
+
<code className="rounded bg-muted px-1 py-0.5 text-xs">GET /identity/companies</code> is a{' '}
|
|
71
|
+
<code className="rounded bg-muted px-1 py-0.5 text-xs">@Classified</code> route — the framework
|
|
72
|
+
resolves your bearer token to a principal and runs <code className="rounded bg-muted px-1 py-0.5 text-xs">decide()</code>{' '}
|
|
73
|
+
before the handler.
|
|
74
|
+
</p>
|
|
75
|
+
<div className="flex flex-wrap gap-2">
|
|
76
|
+
<Button variant="outline" size="sm" onClick={() => load(true)}>
|
|
77
|
+
<RefreshCw className="size-4" />
|
|
78
|
+
Call with my token
|
|
79
|
+
</Button>
|
|
80
|
+
<Button variant="outline" size="sm" onClick={() => load(false)}>
|
|
81
|
+
<ShieldAlert className="size-4" />
|
|
82
|
+
Call WITHOUT a token (expect 401)
|
|
83
|
+
</Button>
|
|
84
|
+
</div>
|
|
85
|
+
{message && (
|
|
86
|
+
<div
|
|
87
|
+
className={
|
|
88
|
+
status === 'error'
|
|
89
|
+
? 'rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive'
|
|
90
|
+
: 'rounded-lg border border-primary/30 bg-primary/10 px-3 py-2 text-sm text-primary'
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
{message}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</CardContent>
|
|
97
|
+
</Card>
|
|
98
|
+
|
|
99
|
+
<Card>
|
|
100
|
+
<CardHeader>
|
|
101
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
102
|
+
<Building2 className="size-4" />
|
|
103
|
+
Companies in your tenant
|
|
104
|
+
</CardTitle>
|
|
105
|
+
</CardHeader>
|
|
106
|
+
<CardContent>
|
|
107
|
+
{status === 'loading' && <p className="py-6 text-center text-sm text-muted-foreground">Loading…</p>}
|
|
108
|
+
{status !== 'loading' && rows.length === 0 && (
|
|
109
|
+
<p className="py-6 text-center text-sm text-muted-foreground">
|
|
110
|
+
No companies visible (or the call was denied).
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
113
|
+
{rows.map((r, i) => (
|
|
114
|
+
<div key={typeof r.id === 'string' ? r.id : i}>
|
|
115
|
+
{i > 0 && <Separator />}
|
|
116
|
+
<div className="flex items-center justify-between py-2.5 text-sm">
|
|
117
|
+
<span className="font-medium">{r.fields?.name ?? '(unnamed)'}</span>
|
|
118
|
+
<span className="font-mono text-xs text-muted-foreground">{r.fields?.status ?? ''}</span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</CardContent>
|
|
123
|
+
</Card>
|
|
124
|
+
</div>
|
|
125
|
+
</AppShell>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ForgotPassword — reset-request screen (shadcn/ui). Submits the hint to the
|
|
3
|
+
* framework's `POST /auth/forgot-password` via the shared `authService.requestReset`
|
|
4
|
+
* (through AuthContext). ANTI-ENUMERATION: the server always returns the same 204 and
|
|
5
|
+
* this page ALWAYS shows the SAME neutral confirmation — it never reveals whether an
|
|
6
|
+
* account matched.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { Link } from 'react-router-dom';
|
|
10
|
+
import { ArrowLeft, Loader2, MailCheck } from 'lucide-react';
|
|
11
|
+
import { useAuth } from '@/auth/AuthContext';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { Input } from '@/components/ui/input';
|
|
14
|
+
import { Label } from '@/components/ui/label';
|
|
15
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
16
|
+
import ThemeToggle from '@/theme/ThemeToggle';
|
|
17
|
+
import { branding } from '@/config/branding';
|
|
18
|
+
|
|
19
|
+
const NEUTRAL_CONFIRMATION =
|
|
20
|
+
'If an account matches, a reset link has been sent. Check your inbox and follow the ' +
|
|
21
|
+
'link to choose a new password.';
|
|
22
|
+
|
|
23
|
+
export default function ForgotPassword() {
|
|
24
|
+
const { auth } = useAuth();
|
|
25
|
+
const [identityHint, setIdentityHint] = useState('');
|
|
26
|
+
const [sent, setSent] = useState(false);
|
|
27
|
+
const [busy, setBusy] = useState(false);
|
|
28
|
+
|
|
29
|
+
const onSubmit = async (e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
if (busy || !identityHint.trim()) return;
|
|
32
|
+
setBusy(true);
|
|
33
|
+
// requestReset ALWAYS resolves ok (mirrors the server's anti-enumeration); we
|
|
34
|
+
// show the SAME neutral confirmation whether or not an account matched.
|
|
35
|
+
await auth.requestReset(identityHint.trim());
|
|
36
|
+
setBusy(false);
|
|
37
|
+
setSent(true);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="grid-bg relative flex min-h-screen items-center justify-center bg-background p-6">
|
|
42
|
+
<div className="absolute right-4 top-4">
|
|
43
|
+
<ThemeToggle compact />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<Card className="w-full max-w-md shadow-xl">
|
|
47
|
+
<CardContent className="p-8">
|
|
48
|
+
<div className="mb-7 flex flex-col items-center gap-4">
|
|
49
|
+
{/* Brand logo — color in light, white in dark (from branding config). */}
|
|
50
|
+
<img src={branding.logo.light} alt={branding.appName} className="h-9 w-auto dark:hidden" />
|
|
51
|
+
<img
|
|
52
|
+
src={branding.logo.dark}
|
|
53
|
+
alt={branding.appName}
|
|
54
|
+
className="hidden h-9 w-auto dark:block"
|
|
55
|
+
/>
|
|
56
|
+
<div className="text-center">
|
|
57
|
+
<h1 className="text-xl font-bold tracking-tight">Reset your password</h1>
|
|
58
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
59
|
+
Enter your username or email and we'll send reset instructions.
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{sent ? (
|
|
65
|
+
<div className="flex items-start gap-2 rounded-lg border border-primary/30 bg-primary/10 px-3 py-3 text-sm text-primary">
|
|
66
|
+
<MailCheck className="mt-0.5 size-4 shrink-0" />
|
|
67
|
+
<span>{NEUTRAL_CONFIRMATION}</span>
|
|
68
|
+
</div>
|
|
69
|
+
) : (
|
|
70
|
+
<form onSubmit={onSubmit} className="space-y-4" noValidate>
|
|
71
|
+
<div className="space-y-1.5">
|
|
72
|
+
<Label htmlFor="fp-hint">Username or email</Label>
|
|
73
|
+
<Input
|
|
74
|
+
id="fp-hint"
|
|
75
|
+
autoFocus
|
|
76
|
+
value={identityHint}
|
|
77
|
+
onChange={(e) => setIdentityHint(e.target.value)}
|
|
78
|
+
placeholder="you@example.com"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<Button type="submit" disabled={busy} className="w-full">
|
|
82
|
+
{busy ? (
|
|
83
|
+
<Loader2 className="size-4 animate-spin" />
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<MailCheck className="size-4" />
|
|
87
|
+
Send reset link
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
</Button>
|
|
91
|
+
</form>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<div className="mt-6 text-center">
|
|
95
|
+
<Link to="/login" className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline">
|
|
96
|
+
<ArrowLeft className="size-4" />
|
|
97
|
+
Back to sign in
|
|
98
|
+
</Link>
|
|
99
|
+
</div>
|
|
100
|
+
</CardContent>
|
|
101
|
+
</Card>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|