@better-forge/ui 0.1.0 → 0.1.1

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 (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +13 -13
  3. package/icons.json +170 -170
  4. package/package.json +71 -71
  5. package/src/api/client.ts +139 -139
  6. package/src/components/ConfirmIdentityModal.tsx +82 -82
  7. package/src/components/ConfirmModal.tsx +83 -83
  8. package/src/components/CopyButton.tsx +71 -71
  9. package/src/components/CredentialSection.tsx +270 -270
  10. package/src/components/ErrorBoundary.tsx +47 -47
  11. package/src/components/ModalShell.tsx +160 -160
  12. package/src/components/ResetTwoFactorModal.tsx +74 -74
  13. package/src/components/SidePanel.tsx +96 -96
  14. package/src/components/SingleSignOnModal.tsx +161 -161
  15. package/src/components/SsoConfirmation.tsx +96 -96
  16. package/src/components/VaultTypePicker.tsx +131 -131
  17. package/src/components/WhatsNew.tsx +84 -84
  18. package/src/components/formParts.tsx +59 -59
  19. package/src/components/ui/Alert.tsx +45 -45
  20. package/src/components/ui/DataTable.tsx +225 -225
  21. package/src/components/ui/Field.tsx +63 -63
  22. package/src/components/ui/PageHeader.tsx +36 -36
  23. package/src/components/ui/Pagination.tsx +37 -37
  24. package/src/components/ui/QuickStartPanel.tsx +57 -57
  25. package/src/components/ui/StateViews.tsx +135 -135
  26. package/src/components/ui/Switch.tsx +65 -65
  27. package/src/components/ui/Toast.tsx +104 -104
  28. package/src/components/ui/index.ts +9 -9
  29. package/src/config.ts +46 -46
  30. package/src/hooks/useDarkMode.ts +38 -38
  31. package/src/hooks/useDebouncedValue.ts +22 -22
  32. package/src/index.ts +42 -42
  33. package/src/lib/changelog.ts +93 -93
  34. package/src/lib/dateFormat.ts +90 -90
  35. package/src/navigation.ts +6 -6
  36. package/src/pages/ApiTokens.tsx +361 -361
  37. package/src/pages/AuditLog.tsx +388 -388
  38. package/src/pages/Backups.tsx +170 -170
  39. package/src/pages/ChangePassword.tsx +84 -84
  40. package/src/pages/Login.tsx +275 -275
  41. package/src/pages/Settings.tsx +264 -264
  42. package/src/pages/Setup.tsx +438 -438
  43. package/src/pages/SsoConfirm.tsx +26 -26
  44. package/src/pages/SsoTestResult.tsx +104 -104
  45. package/src/pages/Users.tsx +426 -426
  46. package/src/pages/Vault.tsx +1036 -1036
  47. package/src/shell/AdminShell.tsx +256 -256
  48. package/src/shell/guards.tsx +88 -88
  49. package/styles/base.css +521 -521
  50. package/styles/icons.css +4 -4
  51. package/tailwind-preset.js +79 -79
@@ -1,26 +1,26 @@
1
- import { useEffect } from 'react'
2
- import { useSearchParams } from 'react-router-dom'
3
- import { ssoConfirmChannel } from '../components/SsoConfirmation'
4
-
5
- /** Where the SSO confirmation popup lands: tells the window that opened it how it went, then closes. */
6
- export default function SsoConfirmPage() {
7
- const [params] = useSearchParams()
8
- const ok = params.get('status') === 'ok'
9
-
10
- useEffect(() => {
11
- const channel = new BroadcastChannel(ssoConfirmChannel())
12
- channel.postMessage({ ok })
13
- channel.close()
14
- if (ok) window.close()
15
- }, [ok])
16
-
17
- return (
18
- <div className="min-h-screen flex items-center justify-center px-6" style={{ background: 'var(--m3-surface)', color: 'var(--m3-on-surface)' }}>
19
- <p className="text-sm text-center max-w-sm">
20
- {ok
21
- ? 'Confirmed. You can close this window.'
22
- : 'The confirmation did not succeed. Close this window and try again.'}
23
- </p>
24
- </div>
25
- )
26
- }
1
+ import { useEffect } from 'react'
2
+ import { useSearchParams } from 'react-router-dom'
3
+ import { ssoConfirmChannel } from '../components/SsoConfirmation'
4
+
5
+ /** Where the SSO confirmation popup lands: tells the window that opened it how it went, then closes. */
6
+ export default function SsoConfirmPage() {
7
+ const [params] = useSearchParams()
8
+ const ok = params.get('status') === 'ok'
9
+
10
+ useEffect(() => {
11
+ const channel = new BroadcastChannel(ssoConfirmChannel())
12
+ channel.postMessage({ ok })
13
+ channel.close()
14
+ if (ok) window.close()
15
+ }, [ok])
16
+
17
+ return (
18
+ <div className="min-h-screen flex items-center justify-center px-6" style={{ background: 'var(--m3-surface)', color: 'var(--m3-on-surface)' }}>
19
+ <p className="text-sm text-center max-w-sm">
20
+ {ok
21
+ ? 'Confirmed. You can close this window.'
22
+ : 'The confirmation did not succeed. Close this window and try again.'}
23
+ </p>
24
+ </div>
25
+ )
26
+ }
@@ -1,104 +1,104 @@
1
- import { useQuery } from '@tanstack/react-query'
2
- import { useSearchParams } from 'react-router-dom'
3
- import { api } from '../api/client'
4
- import { uiConfig } from '../config'
5
- import { Alert, ErrorState, LoadingState, PageContainer, PageHeader, DataTable, DataTh } from '../components/ui'
6
- import { formatDateTime } from '../lib/dateFormat'
7
-
8
- interface TestResult {
9
- issuer: string
10
- testedAt: number
11
- claims: Record<string, unknown> | null
12
- outcome: 'sign_in' | 'link' | 'provision' | 'deny'
13
- user: string | null
14
- denial: { code: string; reason: string; email?: string } | null
15
- }
16
-
17
- // Claims that decide the outcome, shown first.
18
- const KEY_CLAIMS = ['sub', 'email', 'email_verified', 'xms_edov', 'preferred_username', 'name']
19
-
20
- function formatClaim(value: unknown) {
21
- return typeof value === 'string' ? value : JSON.stringify(value)
22
- }
23
-
24
- function Outcome({ result }: { result: TestResult }) {
25
- if (result.outcome === 'sign_in') {
26
- return <Alert tone="success" title="Sign-in would succeed">Signs in as <strong>{result.user}</strong>, matched by their linked identity-provider account.</Alert>
27
- }
28
- if (result.outcome === 'link') {
29
- return <Alert tone="success" title="Sign-in would succeed">Signs in as <strong>{result.user}</strong>, matched by email. The first real sign-in links this identity-provider account to the user.</Alert>
30
- }
31
- if (result.outcome === 'provision') {
32
- return <Alert tone="success" title="Sign-in would succeed">{newAccountNote(result.user)}</Alert>
33
- }
34
- return (
35
- <Alert tone="error" title={`Sign-in would be refused (${result.denial?.code})`}>
36
- {result.denial?.reason}
37
- </Alert>
38
- )
39
- }
40
-
41
- export default function SsoTestResultPage() {
42
- const [params] = useSearchParams()
43
- const id = params.get('result') ?? ''
44
- const { data, isLoading, error } = useQuery<TestResult>({
45
- queryKey: ['sso-test', id],
46
- queryFn: () => api.get(`/admin/oidc/test-sign-in/${encodeURIComponent(id)}`),
47
- enabled: !!id,
48
- retry: false,
49
- })
50
-
51
- const claims = data?.claims ? Object.entries(data.claims) : []
52
- const ordered = [
53
- ...KEY_CLAIMS.flatMap((key) => claims.filter(([name]) => name === key)),
54
- ...claims.filter(([name]) => !KEY_CLAIMS.includes(name)).sort(([a], [b]) => a.localeCompare(b)),
55
- ]
56
-
57
- return (
58
- <PageContainer className="max-w-3xl">
59
- <PageHeader
60
- title="Single sign-on test"
61
- subtitle="Nobody was signed in and nothing was saved. Close this tab when you are done."
62
- />
63
- {!id ? <ErrorState message="No test result to show. Start a test sign-in from Users → Single sign-on." />
64
- : isLoading ? <LoadingState label="Loading the test result…" />
65
- : error || !data ? <ErrorState message={error instanceof Error ? error.message : 'Could not load the test result.'} />
66
- : (
67
- <>
68
- <Outcome result={data} />
69
- <p className="text-sm" style={{ color: 'var(--m3-secondary)' }}>
70
- Issuer <code>{data.issuer}</code> · tested {formatDateTime(data.testedAt)}
71
- </p>
72
- {ordered.length > 0 && (
73
- <div className="rounded-2xl overflow-x-auto" style={{ background: 'var(--m3-surface-container-low)', border: '1px solid var(--m3-outline-variant)' }}>
74
- <DataTable className="w-full text-sm">
75
- <caption className="sr-only">ID token claims</caption>
76
- <thead>
77
- <tr style={{ borderBottom: '1px solid var(--m3-outline-variant)' }}>
78
- {['Claim', 'Value'].map((h) => (
79
- <DataTh key={h} className="px-4 py-3 font-mono text-xs uppercase tracking-wider text-left" style={{ color: 'var(--m3-secondary)', background: 'var(--m3-surface-container)' }}>{h}</DataTh>
80
- ))}
81
- </tr>
82
- </thead>
83
- <tbody>
84
- {ordered.map(([name, value], i) => (
85
- <tr key={name} style={{ borderTop: i > 0 ? '1px solid var(--m3-outline-variant)' : 'none' }}>
86
- <td className="px-4 py-2 font-mono text-xs whitespace-nowrap align-top" style={{ color: 'var(--m3-secondary)' }}>{name}</td>
87
- <td className="px-4 py-2 font-mono text-xs break-all" style={{ color: 'var(--m3-on-surface)' }}>{formatClaim(value)}</td>
88
- </tr>
89
- ))}
90
- </tbody>
91
- </DataTable>
92
- </div>
93
- )}
94
- </>
95
- )}
96
- </PageContainer>
97
- )
98
- }
99
-
100
- /** The note for a test sign-in that would create an account, worded by the application. */
101
- function newAccountNote(user: string | null) {
102
- const [before, after] = uiConfig().texts.ssoNewAccount.split('{user}')
103
- return <>{before}<strong>{user}</strong>{after}</>
104
- }
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useSearchParams } from 'react-router-dom'
3
+ import { api } from '../api/client'
4
+ import { uiConfig } from '../config'
5
+ import { Alert, ErrorState, LoadingState, PageContainer, PageHeader, DataTable, DataTh } from '../components/ui'
6
+ import { formatDateTime } from '../lib/dateFormat'
7
+
8
+ interface TestResult {
9
+ issuer: string
10
+ testedAt: number
11
+ claims: Record<string, unknown> | null
12
+ outcome: 'sign_in' | 'link' | 'provision' | 'deny'
13
+ user: string | null
14
+ denial: { code: string; reason: string; email?: string } | null
15
+ }
16
+
17
+ // Claims that decide the outcome, shown first.
18
+ const KEY_CLAIMS = ['sub', 'email', 'email_verified', 'xms_edov', 'preferred_username', 'name']
19
+
20
+ function formatClaim(value: unknown) {
21
+ return typeof value === 'string' ? value : JSON.stringify(value)
22
+ }
23
+
24
+ function Outcome({ result }: { result: TestResult }) {
25
+ if (result.outcome === 'sign_in') {
26
+ return <Alert tone="success" title="Sign-in would succeed">Signs in as <strong>{result.user}</strong>, matched by their linked identity-provider account.</Alert>
27
+ }
28
+ if (result.outcome === 'link') {
29
+ return <Alert tone="success" title="Sign-in would succeed">Signs in as <strong>{result.user}</strong>, matched by email. The first real sign-in links this identity-provider account to the user.</Alert>
30
+ }
31
+ if (result.outcome === 'provision') {
32
+ return <Alert tone="success" title="Sign-in would succeed">{newAccountNote(result.user)}</Alert>
33
+ }
34
+ return (
35
+ <Alert tone="error" title={`Sign-in would be refused (${result.denial?.code})`}>
36
+ {result.denial?.reason}
37
+ </Alert>
38
+ )
39
+ }
40
+
41
+ export default function SsoTestResultPage() {
42
+ const [params] = useSearchParams()
43
+ const id = params.get('result') ?? ''
44
+ const { data, isLoading, error } = useQuery<TestResult>({
45
+ queryKey: ['sso-test', id],
46
+ queryFn: () => api.get(`/admin/oidc/test-sign-in/${encodeURIComponent(id)}`),
47
+ enabled: !!id,
48
+ retry: false,
49
+ })
50
+
51
+ const claims = data?.claims ? Object.entries(data.claims) : []
52
+ const ordered = [
53
+ ...KEY_CLAIMS.flatMap((key) => claims.filter(([name]) => name === key)),
54
+ ...claims.filter(([name]) => !KEY_CLAIMS.includes(name)).sort(([a], [b]) => a.localeCompare(b)),
55
+ ]
56
+
57
+ return (
58
+ <PageContainer className="max-w-3xl">
59
+ <PageHeader
60
+ title="Single sign-on test"
61
+ subtitle="Nobody was signed in and nothing was saved. Close this tab when you are done."
62
+ />
63
+ {!id ? <ErrorState message="No test result to show. Start a test sign-in from Users → Single sign-on." />
64
+ : isLoading ? <LoadingState label="Loading the test result…" />
65
+ : error || !data ? <ErrorState message={error instanceof Error ? error.message : 'Could not load the test result.'} />
66
+ : (
67
+ <>
68
+ <Outcome result={data} />
69
+ <p className="text-sm" style={{ color: 'var(--m3-secondary)' }}>
70
+ Issuer <code>{data.issuer}</code> · tested {formatDateTime(data.testedAt)}
71
+ </p>
72
+ {ordered.length > 0 && (
73
+ <div className="rounded-2xl overflow-x-auto" style={{ background: 'var(--m3-surface-container-low)', border: '1px solid var(--m3-outline-variant)' }}>
74
+ <DataTable className="w-full text-sm">
75
+ <caption className="sr-only">ID token claims</caption>
76
+ <thead>
77
+ <tr style={{ borderBottom: '1px solid var(--m3-outline-variant)' }}>
78
+ {['Claim', 'Value'].map((h) => (
79
+ <DataTh key={h} className="px-4 py-3 font-mono text-xs uppercase tracking-wider text-left" style={{ color: 'var(--m3-secondary)', background: 'var(--m3-surface-container)' }}>{h}</DataTh>
80
+ ))}
81
+ </tr>
82
+ </thead>
83
+ <tbody>
84
+ {ordered.map(([name, value], i) => (
85
+ <tr key={name} style={{ borderTop: i > 0 ? '1px solid var(--m3-outline-variant)' : 'none' }}>
86
+ <td className="px-4 py-2 font-mono text-xs whitespace-nowrap align-top" style={{ color: 'var(--m3-secondary)' }}>{name}</td>
87
+ <td className="px-4 py-2 font-mono text-xs break-all" style={{ color: 'var(--m3-on-surface)' }}>{formatClaim(value)}</td>
88
+ </tr>
89
+ ))}
90
+ </tbody>
91
+ </DataTable>
92
+ </div>
93
+ )}
94
+ </>
95
+ )}
96
+ </PageContainer>
97
+ )
98
+ }
99
+
100
+ /** The note for a test sign-in that would create an account, worded by the application. */
101
+ function newAccountNote(user: string | null) {
102
+ const [before, after] = uiConfig().texts.ssoNewAccount.split('{user}')
103
+ return <>{before}<strong>{user}</strong>{after}</>
104
+ }