@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.
- package/LICENSE +21 -21
- package/README.md +13 -13
- package/icons.json +170 -170
- package/package.json +71 -71
- package/src/api/client.ts +139 -139
- package/src/components/ConfirmIdentityModal.tsx +82 -82
- package/src/components/ConfirmModal.tsx +83 -83
- package/src/components/CopyButton.tsx +71 -71
- package/src/components/CredentialSection.tsx +270 -270
- package/src/components/ErrorBoundary.tsx +47 -47
- package/src/components/ModalShell.tsx +160 -160
- package/src/components/ResetTwoFactorModal.tsx +74 -74
- package/src/components/SidePanel.tsx +96 -96
- package/src/components/SingleSignOnModal.tsx +161 -161
- package/src/components/SsoConfirmation.tsx +96 -96
- package/src/components/VaultTypePicker.tsx +131 -131
- package/src/components/WhatsNew.tsx +84 -84
- package/src/components/formParts.tsx +59 -59
- package/src/components/ui/Alert.tsx +45 -45
- package/src/components/ui/DataTable.tsx +225 -225
- package/src/components/ui/Field.tsx +63 -63
- package/src/components/ui/PageHeader.tsx +36 -36
- package/src/components/ui/Pagination.tsx +37 -37
- package/src/components/ui/QuickStartPanel.tsx +57 -57
- package/src/components/ui/StateViews.tsx +135 -135
- package/src/components/ui/Switch.tsx +65 -65
- package/src/components/ui/Toast.tsx +104 -104
- package/src/components/ui/index.ts +9 -9
- package/src/config.ts +46 -46
- package/src/hooks/useDarkMode.ts +38 -38
- package/src/hooks/useDebouncedValue.ts +22 -22
- package/src/index.ts +42 -42
- package/src/lib/changelog.ts +93 -93
- package/src/lib/dateFormat.ts +90 -90
- package/src/navigation.ts +6 -6
- package/src/pages/ApiTokens.tsx +361 -361
- package/src/pages/AuditLog.tsx +388 -388
- package/src/pages/Backups.tsx +170 -170
- package/src/pages/ChangePassword.tsx +84 -84
- package/src/pages/Login.tsx +275 -275
- package/src/pages/Settings.tsx +264 -264
- package/src/pages/Setup.tsx +438 -438
- package/src/pages/SsoConfirm.tsx +26 -26
- package/src/pages/SsoTestResult.tsx +104 -104
- package/src/pages/Users.tsx +426 -426
- package/src/pages/Vault.tsx +1036 -1036
- package/src/shell/AdminShell.tsx +256 -256
- package/src/shell/guards.tsx +88 -88
- package/styles/base.css +521 -521
- package/styles/icons.css +4 -4
- package/tailwind-preset.js +79 -79
package/src/shell/guards.tsx
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
import { useEffect, useState, type ReactNode } from 'react'
|
|
2
|
-
import { Navigate } from 'react-router-dom'
|
|
3
|
-
import { api, clearSession, getCurrentUser, mustChangePassword, setSession, type AuthUser } from '../api/client'
|
|
4
|
-
import { uiConfig } from '../config'
|
|
5
|
-
import { navigation } from '../navigation'
|
|
6
|
-
|
|
7
|
-
/** Sends a first visit to the setup page, while the application has no administrator yet. */
|
|
8
|
-
export function SetupGate({ children }: { children: ReactNode }) {
|
|
9
|
-
const [state, setState] = useState<'loading' | 'setup' | 'ready'>('loading')
|
|
10
|
-
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
fetch(`${uiConfig().apiBase}/setup/status`)
|
|
13
|
-
.then((r) => r.json())
|
|
14
|
-
.then((data: { needsSetup: boolean }) => {
|
|
15
|
-
setState(data.needsSetup ? 'setup' : 'ready')
|
|
16
|
-
})
|
|
17
|
-
.catch(() => setState('ready'))
|
|
18
|
-
}, [])
|
|
19
|
-
|
|
20
|
-
if (state === 'loading') {
|
|
21
|
-
return (
|
|
22
|
-
<div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--m3-surface)' }}>
|
|
23
|
-
<div className="flex flex-col items-center gap-4">
|
|
24
|
-
<div
|
|
25
|
-
className="w-10 h-10 rounded-2xl flex items-center justify-center"
|
|
26
|
-
style={{ background: 'var(--m3-primary-fixed)' }}
|
|
27
|
-
>
|
|
28
|
-
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
29
|
-
<circle cx="10" cy="10" r="3.5" fill="var(--m3-primary)" />
|
|
30
|
-
<circle cx="10" cy="10" r="8.5" stroke="var(--m3-primary)" strokeWidth="1.2" strokeOpacity="0.35" fill="none" />
|
|
31
|
-
</svg>
|
|
32
|
-
</div>
|
|
33
|
-
<span className="font-sans text-sm" style={{ color: 'var(--m3-secondary)' }}>Starting…</span>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (state === 'setup') return <Navigate to={`${uiConfig().basePath}/setup`} replace />
|
|
40
|
-
return <>{children}</>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Renders its children for a signed-in user. Someone who is not signed in goes to the sign-in page; someone the
|
|
45
|
-
* application sends elsewhere (`redirectAfterSignIn`) leaves the interface for that address.
|
|
46
|
-
*/
|
|
47
|
-
export function RequireAuth({ children }: { children: ReactNode }) {
|
|
48
|
-
const [state, setState] = useState<'checking' | 'authenticated' | 'anonymous' | 'elsewhere'>('checking')
|
|
49
|
-
const [elsewhere, setElsewhere] = useState<string | null>(null)
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
api.get<AuthUser>('/auth/session')
|
|
53
|
-
.then((user) => {
|
|
54
|
-
setSession(user)
|
|
55
|
-
const target = uiConfig().redirectAfterSignIn?.(user) ?? null
|
|
56
|
-
setElsewhere(target)
|
|
57
|
-
setState(target ? 'elsewhere' : 'authenticated')
|
|
58
|
-
})
|
|
59
|
-
.catch(() => { clearSession(); setState('anonymous') })
|
|
60
|
-
}, [])
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (state === 'elsewhere' && elsewhere) navigation.assign(elsewhere)
|
|
64
|
-
}, [state, elsewhere])
|
|
65
|
-
|
|
66
|
-
if (state === 'checking' || state === 'elsewhere') return null
|
|
67
|
-
if (state === 'anonymous') return <Navigate to={`${uiConfig().basePath}/login`} replace />
|
|
68
|
-
return <>{children}</>
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Keeps a user who has to replace a temporary password on the page where they do it. */
|
|
72
|
-
export function RequirePasswordChanged({ children }: { children: ReactNode }) {
|
|
73
|
-
if (mustChangePassword()) return <Navigate to={`${uiConfig().basePath}/change-password`} replace />
|
|
74
|
-
return <>{children}</>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* A guard for pages that need a role: a user whose rank is below the page's goes to the home page of their role.
|
|
79
|
-
* `roleRank` lists the roles by rank (higher sees more); a role it does not know ranks lowest.
|
|
80
|
-
*/
|
|
81
|
-
export function createRoleGuard(options: { roleRank: Readonly<Record<string, number>>; homeOf: (role?: string) => string }) {
|
|
82
|
-
return function RequireRole({ minRole, children }: { minRole: string; children: ReactNode }) {
|
|
83
|
-
const user = getCurrentUser()
|
|
84
|
-
const rank = options.roleRank[user?.role ?? ''] ?? 0
|
|
85
|
-
if (rank < (options.roleRank[minRole] ?? 99)) return <Navigate to={options.homeOf(user?.role)} replace />
|
|
86
|
-
return <>{children}</>
|
|
87
|
-
}
|
|
88
|
-
}
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
2
|
+
import { Navigate } from 'react-router-dom'
|
|
3
|
+
import { api, clearSession, getCurrentUser, mustChangePassword, setSession, type AuthUser } from '../api/client'
|
|
4
|
+
import { uiConfig } from '../config'
|
|
5
|
+
import { navigation } from '../navigation'
|
|
6
|
+
|
|
7
|
+
/** Sends a first visit to the setup page, while the application has no administrator yet. */
|
|
8
|
+
export function SetupGate({ children }: { children: ReactNode }) {
|
|
9
|
+
const [state, setState] = useState<'loading' | 'setup' | 'ready'>('loading')
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
fetch(`${uiConfig().apiBase}/setup/status`)
|
|
13
|
+
.then((r) => r.json())
|
|
14
|
+
.then((data: { needsSetup: boolean }) => {
|
|
15
|
+
setState(data.needsSetup ? 'setup' : 'ready')
|
|
16
|
+
})
|
|
17
|
+
.catch(() => setState('ready'))
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
if (state === 'loading') {
|
|
21
|
+
return (
|
|
22
|
+
<div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--m3-surface)' }}>
|
|
23
|
+
<div className="flex flex-col items-center gap-4">
|
|
24
|
+
<div
|
|
25
|
+
className="w-10 h-10 rounded-2xl flex items-center justify-center"
|
|
26
|
+
style={{ background: 'var(--m3-primary-fixed)' }}
|
|
27
|
+
>
|
|
28
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
29
|
+
<circle cx="10" cy="10" r="3.5" fill="var(--m3-primary)" />
|
|
30
|
+
<circle cx="10" cy="10" r="8.5" stroke="var(--m3-primary)" strokeWidth="1.2" strokeOpacity="0.35" fill="none" />
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
33
|
+
<span className="font-sans text-sm" style={{ color: 'var(--m3-secondary)' }}>Starting…</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (state === 'setup') return <Navigate to={`${uiConfig().basePath}/setup`} replace />
|
|
40
|
+
return <>{children}</>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Renders its children for a signed-in user. Someone who is not signed in goes to the sign-in page; someone the
|
|
45
|
+
* application sends elsewhere (`redirectAfterSignIn`) leaves the interface for that address.
|
|
46
|
+
*/
|
|
47
|
+
export function RequireAuth({ children }: { children: ReactNode }) {
|
|
48
|
+
const [state, setState] = useState<'checking' | 'authenticated' | 'anonymous' | 'elsewhere'>('checking')
|
|
49
|
+
const [elsewhere, setElsewhere] = useState<string | null>(null)
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
api.get<AuthUser>('/auth/session')
|
|
53
|
+
.then((user) => {
|
|
54
|
+
setSession(user)
|
|
55
|
+
const target = uiConfig().redirectAfterSignIn?.(user) ?? null
|
|
56
|
+
setElsewhere(target)
|
|
57
|
+
setState(target ? 'elsewhere' : 'authenticated')
|
|
58
|
+
})
|
|
59
|
+
.catch(() => { clearSession(); setState('anonymous') })
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (state === 'elsewhere' && elsewhere) navigation.assign(elsewhere)
|
|
64
|
+
}, [state, elsewhere])
|
|
65
|
+
|
|
66
|
+
if (state === 'checking' || state === 'elsewhere') return null
|
|
67
|
+
if (state === 'anonymous') return <Navigate to={`${uiConfig().basePath}/login`} replace />
|
|
68
|
+
return <>{children}</>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Keeps a user who has to replace a temporary password on the page where they do it. */
|
|
72
|
+
export function RequirePasswordChanged({ children }: { children: ReactNode }) {
|
|
73
|
+
if (mustChangePassword()) return <Navigate to={`${uiConfig().basePath}/change-password`} replace />
|
|
74
|
+
return <>{children}</>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A guard for pages that need a role: a user whose rank is below the page's goes to the home page of their role.
|
|
79
|
+
* `roleRank` lists the roles by rank (higher sees more); a role it does not know ranks lowest.
|
|
80
|
+
*/
|
|
81
|
+
export function createRoleGuard(options: { roleRank: Readonly<Record<string, number>>; homeOf: (role?: string) => string }) {
|
|
82
|
+
return function RequireRole({ minRole, children }: { minRole: string; children: ReactNode }) {
|
|
83
|
+
const user = getCurrentUser()
|
|
84
|
+
const rank = options.roleRank[user?.role ?? ''] ?? 0
|
|
85
|
+
if (rank < (options.roleRank[minRole] ?? 99)) return <Navigate to={options.homeOf(user?.role)} replace />
|
|
86
|
+
return <>{children}</>
|
|
87
|
+
}
|
|
88
|
+
}
|