create-astram-app 0.4.0 → 0.6.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/README.md +211 -38
- package/dist/add-frontend.js.map +1 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/template/README.md +51 -20
- package/template/frontend/README.md +21 -11
- package/template/frontend/index.html +16 -0
- package/template/frontend/package.json +1 -1
- package/template/frontend/src/App.jsx +95 -20
- package/template/frontend/src/auth/AuthContext.jsx +145 -21
- package/template/frontend/src/auth/routeGate.js +31 -0
- package/template/frontend/src/auth/routeGate.test.mjs +59 -0
- package/template/frontend/src/auth/sessionFlow.js +27 -0
- package/template/frontend/src/auth/sessionFlow.test.mjs +63 -0
- package/template/frontend/src/components/EntityList.jsx +1 -7
- package/template/frontend/src/components/MultiSelectChecklist.jsx +166 -0
- package/template/frontend/src/components/PageHeader.jsx +5 -4
- package/template/frontend/src/components/ShareDialog.jsx +3 -5
- package/template/frontend/src/components/access/CollapsibleGroup.jsx +4 -1
- package/template/frontend/src/components/ui/alert-dialog.tsx +27 -50
- package/template/frontend/src/components/ui/alert.tsx +17 -23
- package/template/frontend/src/components/ui/avatar.tsx +8 -20
- package/template/frontend/src/components/ui/badge.tsx +17 -26
- package/template/frontend/src/components/ui/button.tsx +22 -24
- package/template/frontend/src/components/ui/card.tsx +19 -36
- package/template/frontend/src/components/ui/checkbox.tsx +7 -10
- package/template/frontend/src/components/ui/date-field.tsx +26 -27
- package/template/frontend/src/components/ui/dialog.tsx +24 -40
- package/template/frontend/src/components/ui/dropdown-menu.tsx +25 -54
- package/template/frontend/src/components/ui/input-otp.tsx +1 -4
- package/template/frontend/src/components/ui/label.tsx +10 -12
- package/template/frontend/src/components/ui/searchable-select.tsx +1 -3
- package/template/frontend/src/components/ui/select.tsx +31 -43
- package/template/frontend/src/components/ui/separator.tsx +6 -6
- package/template/frontend/src/components/ui/sheet.tsx +29 -38
- package/template/frontend/src/components/ui/skeleton.tsx +3 -3
- package/template/frontend/src/components/ui/sonner.tsx +7 -7
- package/template/frontend/src/components/ui/table.tsx +56 -70
- package/template/frontend/src/components/ui/tooltip.tsx +7 -11
- package/template/frontend/src/config/branding.js +1 -4
- package/template/frontend/src/hooks/useMobile.tsx +4 -6
- package/template/frontend/src/index.css +9 -4
- package/template/frontend/src/pages/Dashboard.jsx +13 -7
- package/template/frontend/src/pages/ForgotPassword.jsx +9 -2
- package/template/frontend/src/pages/Login.jsx +85 -70
- package/template/frontend/src/pages/ResetPassword.jsx +9 -2
- package/template/frontend/src/pages/access/AccessForm.jsx +49 -15
- package/template/frontend/src/pages/access/AccessPage.jsx +10 -6
- package/template/frontend/src/pages/access/RecordPoliciesPage.jsx +9 -2
- package/template/frontend/src/pages/account/AccountLayout.jsx +11 -6
- package/template/frontend/src/pages/account/ChangePasswordPage.jsx +2 -2
- package/template/frontend/src/pages/account/ReenrollPage.jsx +3 -5
- package/template/frontend/src/pages/account/TwoFactorPage.jsx +3 -8
- package/template/frontend/src/pages/companies/CompaniesPage.jsx +7 -9
- package/template/frontend/src/pages/cross-tenant/GrantForm.jsx +41 -8
- package/template/frontend/src/pages/cross-tenant/GrantsPage.jsx +30 -7
- package/template/frontend/src/pages/cross-tenant/InboxPage.jsx +12 -3
- package/template/frontend/src/pages/roles/RolesPage.jsx +5 -9
- package/template/frontend/src/pages/service-accounts/KeyRevealDialog.jsx +104 -0
- package/template/frontend/src/pages/service-accounts/ServiceAccountAccessDialog.jsx +231 -0
- package/template/frontend/src/pages/service-accounts/ServiceAccountActionsMenu.jsx +56 -0
- package/template/frontend/src/pages/service-accounts/ServiceAccountsPage.jsx +368 -0
- package/template/frontend/src/pages/service-accounts/accessModel.js +158 -0
- package/template/frontend/src/pages/service-accounts/accessModel.test.mjs +189 -0
- package/template/frontend/src/pages/users/MembershipCard.jsx +3 -1
- package/template/frontend/src/pages/users/PostingRow.jsx +8 -14
- package/template/frontend/src/pages/users/UsersPage.jsx +7 -9
- package/template/frontend/src/platform/httpClient.js +5 -0
- package/template/frontend/src/platform/sessionStore.js +55 -13
- package/template/frontend/src/platform/sessionStore.test.mjs +89 -0
- package/template/frontend/src/platform/shieldTransport.js +6 -0
- package/template/frontend/src/platform/shieldTransport.test.mjs +84 -5
- package/template/frontend/src/shell/AppShell.jsx +10 -12
- package/template/frontend/src/shell/Sidebar.jsx +15 -3
- package/template/frontend/src/shell/nav.js +13 -3
- package/template/frontend/vite.config.js +19 -1
- package/template/server/_env.example +22 -1
- package/template/server/eslint.config.mjs +194 -0
- package/template/server/package.json +11 -11
- package/template/server/scripts/dev.mjs +17 -6
- package/template/server/scripts/sync-astram-guide.mjs +45 -19
- package/template/server/src/app.module.ts +11 -5
- package/template/server/src/health/demo.controller.ts +16 -12
- package/template/server/src/main.ts +23 -1
- package/template/server/test/enforcement.e2e.spec.ts +73 -6
- package/template/server/eslint.config.js +0 -95
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as React from
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import { cn } from
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* shadcn Table primitive set, tuned to OCA's matured DataTable look
|
|
@@ -27,84 +27,79 @@ import { cn } from "@/lib/utils";
|
|
|
27
27
|
* components cannot be given refs".
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
const Table = React.forwardRef<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Table.displayName = "Table";
|
|
30
|
+
const Table = React.forwardRef<HTMLTableElement, React.ComponentPropsWithoutRef<'table'>>(
|
|
31
|
+
({ className, ...props }, ref) => (
|
|
32
|
+
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
|
33
|
+
<table
|
|
34
|
+
ref={ref}
|
|
35
|
+
data-slot="table"
|
|
36
|
+
className={cn('w-full caption-bottom border-collapse text-sm', className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
Table.displayName = 'Table';
|
|
44
43
|
|
|
45
44
|
const TableHeader = React.forwardRef<
|
|
46
45
|
HTMLTableSectionElement,
|
|
47
|
-
React.ComponentPropsWithoutRef<
|
|
46
|
+
React.ComponentPropsWithoutRef<'thead'>
|
|
48
47
|
>(({ className, ...props }, ref) => (
|
|
49
48
|
<thead
|
|
50
49
|
ref={ref}
|
|
51
50
|
data-slot="table-header"
|
|
52
|
-
className={cn(
|
|
51
|
+
className={cn('[&_tr]:border-b', className)}
|
|
53
52
|
{...props}
|
|
54
53
|
/>
|
|
55
54
|
));
|
|
56
|
-
TableHeader.displayName =
|
|
55
|
+
TableHeader.displayName = 'TableHeader';
|
|
57
56
|
|
|
58
57
|
const TableBody = React.forwardRef<
|
|
59
58
|
HTMLTableSectionElement,
|
|
60
|
-
React.ComponentPropsWithoutRef<
|
|
59
|
+
React.ComponentPropsWithoutRef<'tbody'>
|
|
61
60
|
>(({ className, ...props }, ref) => (
|
|
62
61
|
<tbody
|
|
63
62
|
ref={ref}
|
|
64
63
|
data-slot="table-body"
|
|
65
|
-
className={cn(
|
|
64
|
+
className={cn('[&_tr:last-child]:border-0', className)}
|
|
66
65
|
{...props}
|
|
67
66
|
/>
|
|
68
67
|
));
|
|
69
|
-
TableBody.displayName =
|
|
68
|
+
TableBody.displayName = 'TableBody';
|
|
70
69
|
|
|
71
70
|
const TableFooter = React.forwardRef<
|
|
72
71
|
HTMLTableSectionElement,
|
|
73
|
-
React.ComponentPropsWithoutRef<
|
|
72
|
+
React.ComponentPropsWithoutRef<'tfoot'>
|
|
74
73
|
>(({ className, ...props }, ref) => (
|
|
75
74
|
<tfoot
|
|
76
75
|
ref={ref}
|
|
77
76
|
data-slot="table-footer"
|
|
78
|
-
className={cn(
|
|
79
|
-
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
80
|
-
className
|
|
81
|
-
)}
|
|
77
|
+
className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
|
|
82
78
|
{...props}
|
|
83
79
|
/>
|
|
84
80
|
));
|
|
85
|
-
TableFooter.displayName =
|
|
81
|
+
TableFooter.displayName = 'TableFooter';
|
|
86
82
|
|
|
87
|
-
const TableRow = React.forwardRef<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
TableRow.displayName = "TableRow";
|
|
83
|
+
const TableRow = React.forwardRef<HTMLTableRowElement, React.ComponentPropsWithoutRef<'tr'>>(
|
|
84
|
+
({ className, ...props }, ref) => (
|
|
85
|
+
<tr
|
|
86
|
+
ref={ref}
|
|
87
|
+
data-slot="table-row"
|
|
88
|
+
// `group` so sticky cells can react to row hover (OCA keeps the pinned
|
|
89
|
+
// Actions cell in step with the row's hover surface).
|
|
90
|
+
className={cn(
|
|
91
|
+
'group border-b border-border transition-colors hover:bg-muted/40 data-[state=selected]:bg-muted',
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
TableRow.displayName = 'TableRow';
|
|
104
99
|
|
|
105
100
|
const TableHead = React.forwardRef<
|
|
106
101
|
HTMLTableCellElement,
|
|
107
|
-
React.ComponentPropsWithoutRef<
|
|
102
|
+
React.ComponentPropsWithoutRef<'th'> & {
|
|
108
103
|
/** Freeze this header to the right edge (the Actions column). */
|
|
109
104
|
sticky?: boolean;
|
|
110
105
|
}
|
|
@@ -112,26 +107,26 @@ const TableHead = React.forwardRef<
|
|
|
112
107
|
<th
|
|
113
108
|
ref={ref}
|
|
114
109
|
data-slot="table-head"
|
|
115
|
-
data-sticky={sticky ?
|
|
110
|
+
data-sticky={sticky ? '' : undefined}
|
|
116
111
|
className={cn(
|
|
117
112
|
// OCA thead th: mono-uppercase, tiny, wide-tracked, muted, on a subtle
|
|
118
113
|
// surface with a hairline bottom border.
|
|
119
|
-
|
|
114
|
+
'bg-muted/60 text-muted-foreground h-9 px-3.5 text-left align-middle font-mono text-[0.68rem] font-semibold tracking-[0.08em] whitespace-nowrap uppercase [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
|
120
115
|
sticky &&
|
|
121
116
|
// Pinned to the right edge: opaque header surface, sits above the
|
|
122
117
|
// other sticky-top cells, centered label, token left-border (light) +
|
|
123
118
|
// left shadow divider (dark).
|
|
124
|
-
|
|
125
|
-
className
|
|
119
|
+
'bg-muted/60 border-border sticky right-0 z-20 w-[1%] border-l text-center shadow-[-8px_0_10px_-8px_rgba(0,0,0,0.28)]',
|
|
120
|
+
className,
|
|
126
121
|
)}
|
|
127
122
|
{...props}
|
|
128
123
|
/>
|
|
129
124
|
));
|
|
130
|
-
TableHead.displayName =
|
|
125
|
+
TableHead.displayName = 'TableHead';
|
|
131
126
|
|
|
132
127
|
const TableCell = React.forwardRef<
|
|
133
128
|
HTMLTableCellElement,
|
|
134
|
-
React.ComponentPropsWithoutRef<
|
|
129
|
+
React.ComponentPropsWithoutRef<'td'> & {
|
|
135
130
|
/** Freeze this cell to the right edge (the Actions column). */
|
|
136
131
|
sticky?: boolean;
|
|
137
132
|
}
|
|
@@ -139,43 +134,34 @@ const TableCell = React.forwardRef<
|
|
|
139
134
|
<td
|
|
140
135
|
ref={ref}
|
|
141
136
|
data-slot="table-cell"
|
|
142
|
-
data-sticky={sticky ?
|
|
137
|
+
data-sticky={sticky ? '' : undefined}
|
|
143
138
|
className={cn(
|
|
144
139
|
// OCA tbody td: 11px/14px padding (px-3.5 py-2.5), hairline border,
|
|
145
140
|
// secondary ink, middle-aligned.
|
|
146
|
-
|
|
141
|
+
'text-foreground/90 px-3.5 py-2.5 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
|
147
142
|
sticky &&
|
|
148
143
|
// Pinned cell: opaque background that tracks the row hover surface
|
|
149
144
|
// (via the row's `group` + group-hover), centered actions, token
|
|
150
145
|
// left-border (light) + left shadow (dark).
|
|
151
|
-
|
|
152
|
-
className
|
|
146
|
+
'bg-background group-hover:bg-muted/40 border-border sticky right-0 z-10 w-[1%] border-l text-center shadow-[-8px_0_10px_-8px_rgba(0,0,0,0.28)]',
|
|
147
|
+
className,
|
|
153
148
|
)}
|
|
154
149
|
{...props}
|
|
155
150
|
/>
|
|
156
151
|
));
|
|
157
|
-
TableCell.displayName =
|
|
152
|
+
TableCell.displayName = 'TableCell';
|
|
158
153
|
|
|
159
154
|
const TableCaption = React.forwardRef<
|
|
160
155
|
HTMLTableCaptionElement,
|
|
161
|
-
React.ComponentPropsWithoutRef<
|
|
156
|
+
React.ComponentPropsWithoutRef<'caption'>
|
|
162
157
|
>(({ className, ...props }, ref) => (
|
|
163
158
|
<caption
|
|
164
159
|
ref={ref}
|
|
165
160
|
data-slot="table-caption"
|
|
166
|
-
className={cn(
|
|
161
|
+
className={cn('text-muted-foreground mt-4 text-sm', className)}
|
|
167
162
|
{...props}
|
|
168
163
|
/>
|
|
169
164
|
));
|
|
170
|
-
TableCaption.displayName =
|
|
165
|
+
TableCaption.displayName = 'TableCaption';
|
|
171
166
|
|
|
172
|
-
export {
|
|
173
|
-
Table,
|
|
174
|
-
TableHeader,
|
|
175
|
-
TableBody,
|
|
176
|
-
TableFooter,
|
|
177
|
-
TableHead,
|
|
178
|
-
TableRow,
|
|
179
|
-
TableCell,
|
|
180
|
-
TableCaption,
|
|
181
|
-
};
|
|
167
|
+
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import * as TooltipPrimitive from
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
3
3
|
|
|
4
|
-
import { cn } from
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
5
|
|
|
6
6
|
function TooltipProvider({
|
|
7
7
|
delayDuration = 0,
|
|
@@ -16,9 +16,7 @@ function TooltipProvider({
|
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function Tooltip({
|
|
20
|
-
...props
|
|
21
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
19
|
+
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
22
20
|
return (
|
|
23
21
|
<TooltipProvider>
|
|
24
22
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
@@ -26,9 +24,7 @@ function Tooltip({
|
|
|
26
24
|
);
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
function TooltipTrigger({
|
|
30
|
-
...props
|
|
31
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
27
|
+
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
32
28
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -44,8 +40,8 @@ function TooltipContent({
|
|
|
44
40
|
data-slot="tooltip-content"
|
|
45
41
|
sideOffset={sideOffset}
|
|
46
42
|
className={cn(
|
|
47
|
-
|
|
48
|
-
className
|
|
43
|
+
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
|
44
|
+
className,
|
|
49
45
|
)}
|
|
50
46
|
{...props}
|
|
51
47
|
>
|
|
@@ -43,10 +43,7 @@ export const branding = {
|
|
|
43
43
|
* `VITE_APP_SUB_TAGLINE`); falls back to these static strings when the env is unset.
|
|
44
44
|
*/
|
|
45
45
|
tagline: envString(import.meta.env.VITE_APP_TAGLINE, 'Welcome back'),
|
|
46
|
-
subTagline: envString(
|
|
47
|
-
import.meta.env.VITE_APP_SUB_TAGLINE,
|
|
48
|
-
'Sign in to your account',
|
|
49
|
-
),
|
|
46
|
+
subTagline: envString(import.meta.env.VITE_APP_SUB_TAGLINE, 'Sign in to your account'),
|
|
50
47
|
poweredBy: 'Astram',
|
|
51
48
|
|
|
52
49
|
/**
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import * as React from
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
const MOBILE_BREAKPOINT = 768;
|
|
4
4
|
|
|
5
5
|
export function useIsMobile() {
|
|
6
|
-
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
|
7
|
-
undefined
|
|
8
|
-
);
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
|
9
7
|
|
|
10
8
|
React.useEffect(() => {
|
|
11
9
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
12
10
|
const onChange = () => {
|
|
13
11
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
14
12
|
};
|
|
15
|
-
mql.addEventListener(
|
|
13
|
+
mql.addEventListener('change', onChange);
|
|
16
14
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
17
|
-
return () => mql.removeEventListener(
|
|
15
|
+
return () => mql.removeEventListener('change', onChange);
|
|
18
16
|
}, []);
|
|
19
17
|
|
|
20
18
|
return !!isMobile;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
/* Inter (UI) + JetBrains Mono (code/data) — the professional SaaS standard.
|
|
2
|
+
Must stay FIRST: `@import 'tailwindcss'` inlines rules, and a url @import after any
|
|
3
|
+
rule is invalid CSS (the build warns and browsers ignore it). */
|
|
4
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
1
5
|
@import 'tailwindcss';
|
|
2
6
|
@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
7
|
|
|
6
8
|
/*
|
|
7
9
|
astram3 design tokens — shadcn/ui + Tailwind v4 (CSS-var driven).
|
|
@@ -180,8 +182,11 @@
|
|
|
180
182
|
@layer components {
|
|
181
183
|
/* Subtle dot-grid for auth/content backgrounds (astram-tinted). */
|
|
182
184
|
.grid-bg {
|
|
183
|
-
background-image:
|
|
184
|
-
|
|
185
|
+
background-image: radial-gradient(
|
|
186
|
+
circle at 1px 1px,
|
|
187
|
+
color-mix(in oklch, var(--primary) 12%, transparent) 1px,
|
|
188
|
+
transparent 0
|
|
189
|
+
);
|
|
185
190
|
background-size: 22px 22px;
|
|
186
191
|
}
|
|
187
192
|
|
|
@@ -53,7 +53,8 @@ export default function Dashboard() {
|
|
|
53
53
|
<div className="mb-4">
|
|
54
54
|
<h1 className="text-xl font-bold tracking-tight">Dashboard</h1>
|
|
55
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
|
|
56
|
+
You are signed in. Below, the app calls a protected API route as you — the framework
|
|
57
|
+
enforces it.
|
|
57
58
|
</p>
|
|
58
59
|
</div>
|
|
59
60
|
|
|
@@ -67,10 +68,11 @@ export default function Dashboard() {
|
|
|
67
68
|
</CardHeader>
|
|
68
69
|
<CardContent className="space-y-3">
|
|
69
70
|
<p className="text-sm text-muted-foreground">
|
|
70
|
-
<code className="rounded bg-muted px-1 py-0.5 text-xs">GET /identity/companies</code>
|
|
71
|
-
<code className="rounded bg-muted px-1 py-0.5 text-xs">@Classified</code> route —
|
|
72
|
-
resolves your bearer token to a principal and runs
|
|
73
|
-
before the
|
|
71
|
+
<code className="rounded bg-muted px-1 py-0.5 text-xs">GET /identity/companies</code>{' '}
|
|
72
|
+
is a <code className="rounded bg-muted px-1 py-0.5 text-xs">@Classified</code> route —
|
|
73
|
+
the framework resolves your bearer token to a principal and runs{' '}
|
|
74
|
+
<code className="rounded bg-muted px-1 py-0.5 text-xs">decide()</code> before the
|
|
75
|
+
handler.
|
|
74
76
|
</p>
|
|
75
77
|
<div className="flex flex-wrap gap-2">
|
|
76
78
|
<Button variant="outline" size="sm" onClick={() => load(true)}>
|
|
@@ -104,7 +106,9 @@ export default function Dashboard() {
|
|
|
104
106
|
</CardTitle>
|
|
105
107
|
</CardHeader>
|
|
106
108
|
<CardContent>
|
|
107
|
-
{status === 'loading' &&
|
|
109
|
+
{status === 'loading' && (
|
|
110
|
+
<p className="py-6 text-center text-sm text-muted-foreground">Loading…</p>
|
|
111
|
+
)}
|
|
108
112
|
{status !== 'loading' && rows.length === 0 && (
|
|
109
113
|
<p className="py-6 text-center text-sm text-muted-foreground">
|
|
110
114
|
No companies visible (or the call was denied).
|
|
@@ -115,7 +119,9 @@ export default function Dashboard() {
|
|
|
115
119
|
{i > 0 && <Separator />}
|
|
116
120
|
<div className="flex items-center justify-between py-2.5 text-sm">
|
|
117
121
|
<span className="font-medium">{r.fields?.name ?? '(unnamed)'}</span>
|
|
118
|
-
<span className="font-mono text-xs text-muted-foreground">
|
|
122
|
+
<span className="font-mono text-xs text-muted-foreground">
|
|
123
|
+
{r.fields?.status ?? ''}
|
|
124
|
+
</span>
|
|
119
125
|
</div>
|
|
120
126
|
</div>
|
|
121
127
|
))}
|
|
@@ -47,7 +47,11 @@ export default function ForgotPassword() {
|
|
|
47
47
|
<CardContent className="p-8">
|
|
48
48
|
<div className="mb-7 flex flex-col items-center gap-4">
|
|
49
49
|
{/* Brand logo — color in light, white in dark (from branding config). */}
|
|
50
|
-
<img
|
|
50
|
+
<img
|
|
51
|
+
src={branding.logo.light}
|
|
52
|
+
alt={branding.appName}
|
|
53
|
+
className="h-9 w-auto dark:hidden"
|
|
54
|
+
/>
|
|
51
55
|
<img
|
|
52
56
|
src={branding.logo.dark}
|
|
53
57
|
alt={branding.appName}
|
|
@@ -92,7 +96,10 @@ export default function ForgotPassword() {
|
|
|
92
96
|
)}
|
|
93
97
|
|
|
94
98
|
<div className="mt-6 text-center">
|
|
95
|
-
<Link
|
|
99
|
+
<Link
|
|
100
|
+
to="/login"
|
|
101
|
+
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
|
102
|
+
>
|
|
96
103
|
<ArrowLeft className="size-4" />
|
|
97
104
|
Back to sign in
|
|
98
105
|
</Link>
|
|
@@ -171,7 +171,11 @@ export default function Login() {
|
|
|
171
171
|
<CardContent className="p-8">
|
|
172
172
|
{/* Brand logo — color in light, white in dark (from branding config). */}
|
|
173
173
|
<div className="mb-6 flex justify-center">
|
|
174
|
-
<img
|
|
174
|
+
<img
|
|
175
|
+
src={branding.logo.light}
|
|
176
|
+
alt={branding.appName}
|
|
177
|
+
className="h-9 w-auto dark:hidden"
|
|
178
|
+
/>
|
|
175
179
|
<img
|
|
176
180
|
src={branding.logo.dark}
|
|
177
181
|
alt={branding.appName}
|
|
@@ -306,85 +310,96 @@ export default function Login() {
|
|
|
306
310
|
</button>
|
|
307
311
|
</form>
|
|
308
312
|
) : (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
<Input
|
|
318
|
-
id="identityHint"
|
|
319
|
-
autoComplete="username"
|
|
320
|
-
autoFocus
|
|
321
|
-
value={identityHint}
|
|
322
|
-
onChange={(e) => setIdentityHint(e.target.value)}
|
|
323
|
-
onKeyDown={onFieldKeyDown}
|
|
324
|
-
placeholder="you@example.com"
|
|
325
|
-
className="h-11 bg-secondary/60"
|
|
326
|
-
/>
|
|
327
|
-
</div>
|
|
328
|
-
|
|
329
|
-
<div className="space-y-1.5">
|
|
330
|
-
<label
|
|
331
|
-
htmlFor="password"
|
|
332
|
-
className="block text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
|
|
333
|
-
>
|
|
334
|
-
Password
|
|
335
|
-
</label>
|
|
336
|
-
<div className="relative">
|
|
313
|
+
<form onSubmit={onSubmit} className="space-y-4" noValidate>
|
|
314
|
+
<div className="space-y-1.5">
|
|
315
|
+
<label
|
|
316
|
+
htmlFor="identityHint"
|
|
317
|
+
className="block text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
|
|
318
|
+
>
|
|
319
|
+
Email or username
|
|
320
|
+
</label>
|
|
337
321
|
<Input
|
|
338
|
-
id="
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
value={
|
|
342
|
-
onChange={(e) =>
|
|
322
|
+
id="identityHint"
|
|
323
|
+
autoComplete="username"
|
|
324
|
+
autoFocus
|
|
325
|
+
value={identityHint}
|
|
326
|
+
onChange={(e) => setIdentityHint(e.target.value)}
|
|
343
327
|
onKeyDown={onFieldKeyDown}
|
|
344
|
-
placeholder="
|
|
345
|
-
className="h-11 bg-secondary/60
|
|
328
|
+
placeholder="you@example.com"
|
|
329
|
+
className="h-11 bg-secondary/60"
|
|
346
330
|
/>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div className="space-y-1.5">
|
|
334
|
+
<label
|
|
335
|
+
htmlFor="password"
|
|
336
|
+
className="block text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
|
|
352
337
|
>
|
|
353
|
-
|
|
354
|
-
</
|
|
338
|
+
Password
|
|
339
|
+
</label>
|
|
340
|
+
<div className="relative">
|
|
341
|
+
<Input
|
|
342
|
+
id="password"
|
|
343
|
+
type={showPw ? 'text' : 'password'}
|
|
344
|
+
autoComplete="current-password"
|
|
345
|
+
value={password}
|
|
346
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
347
|
+
onKeyDown={onFieldKeyDown}
|
|
348
|
+
placeholder="••••••••"
|
|
349
|
+
className="h-11 bg-secondary/60 pr-10"
|
|
350
|
+
/>
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
onClick={() => setShowPw((s) => !s)}
|
|
354
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground hover:text-foreground"
|
|
355
|
+
aria-label={showPw ? 'Hide password' : 'Show password'}
|
|
356
|
+
>
|
|
357
|
+
{showPw ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
355
360
|
</div>
|
|
356
|
-
</div>
|
|
357
361
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
362
|
+
{/* Remember me + Forgot password, inline below the password. */}
|
|
363
|
+
<div className="flex items-center justify-between pt-0.5">
|
|
364
|
+
<label
|
|
365
|
+
htmlFor="remember"
|
|
366
|
+
className="flex items-center gap-2 text-sm text-muted-foreground"
|
|
367
|
+
>
|
|
368
|
+
<Checkbox
|
|
369
|
+
id="remember"
|
|
370
|
+
checked={remember}
|
|
371
|
+
onCheckedChange={(v) => setRemember(Boolean(v))}
|
|
372
|
+
/>
|
|
373
|
+
Remember me
|
|
374
|
+
</label>
|
|
375
|
+
<Link
|
|
376
|
+
to="/forgot-password"
|
|
377
|
+
className="text-sm font-medium text-primary hover:underline"
|
|
378
|
+
>
|
|
379
|
+
Forgot password?
|
|
380
|
+
</Link>
|
|
381
|
+
</div>
|
|
368
382
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
<Button
|
|
384
|
+
type="submit"
|
|
385
|
+
disabled={busy}
|
|
386
|
+
className="h-11 w-full bg-primary font-mono text-xs font-semibold uppercase tracking-widest text-primary-foreground hover:bg-primary/90"
|
|
387
|
+
>
|
|
388
|
+
{busy ? (
|
|
389
|
+
<Loader2 className="size-4 animate-spin" />
|
|
390
|
+
) : (
|
|
391
|
+
<>
|
|
392
|
+
<LogIn className="size-4" />
|
|
393
|
+
Log in
|
|
394
|
+
</>
|
|
395
|
+
)}
|
|
396
|
+
</Button>
|
|
397
|
+
</form>
|
|
384
398
|
)}
|
|
385
399
|
|
|
386
400
|
<p className="mt-6 text-center text-[11px] tracking-wide text-muted-foreground">
|
|
387
|
-
Powered by
|
|
401
|
+
Powered by{' '}
|
|
402
|
+
<span className="font-semibold text-foreground/70">{branding.poweredBy}</span>
|
|
388
403
|
</p>
|
|
389
404
|
</CardContent>
|
|
390
405
|
</Card>
|
|
@@ -78,7 +78,11 @@ export default function ResetPassword() {
|
|
|
78
78
|
<CardContent className="p-8">
|
|
79
79
|
<div className="mb-7 flex flex-col items-center gap-4">
|
|
80
80
|
{/* Brand logo — color in light, white in dark (from branding config). */}
|
|
81
|
-
<img
|
|
81
|
+
<img
|
|
82
|
+
src={branding.logo.light}
|
|
83
|
+
alt={branding.appName}
|
|
84
|
+
className="h-9 w-auto dark:hidden"
|
|
85
|
+
/>
|
|
82
86
|
<img
|
|
83
87
|
src={branding.logo.dark}
|
|
84
88
|
alt={branding.appName}
|
|
@@ -147,7 +151,10 @@ export default function ResetPassword() {
|
|
|
147
151
|
)}
|
|
148
152
|
|
|
149
153
|
<div className="mt-6 text-center">
|
|
150
|
-
<Link
|
|
154
|
+
<Link
|
|
155
|
+
to="/login"
|
|
156
|
+
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
|
157
|
+
>
|
|
151
158
|
<ArrowLeft className="size-4" />
|
|
152
159
|
Back to sign in
|
|
153
160
|
</Link>
|