From 36084bf487b055f37899caed0a2a569b14267df3 Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Wed, 8 Jul 2026 21:19:35 +0100 Subject: [PATCH 1/2] feat(user-ui): expose shared branding preview surfaces --- packages/user-ui/src/App.tsx | 20 +---- packages/user-ui/src/components/Authorize.tsx | 84 +++++++++++++++++-- .../src/components/AuthorizePageFrame.tsx | 25 ++++++ packages/user-ui/src/components/Dashboard.tsx | 23 +++-- packages/user-ui/src/components/Profile.tsx | 27 ++++-- .../src/components/SettingsSecurity.tsx | 75 ++++++++++++++--- .../src/components/SettingsSecurityView.tsx | 6 +- packages/user-ui/src/exports.ts | 6 ++ 8 files changed, 216 insertions(+), 50 deletions(-) create mode 100644 packages/user-ui/src/components/AuthorizePageFrame.tsx diff --git a/packages/user-ui/src/App.tsx b/packages/user-ui/src/App.tsx index fe8f0f4c..bf9cb1a1 100644 --- a/packages/user-ui/src/App.tsx +++ b/packages/user-ui/src/App.tsx @@ -8,6 +8,7 @@ import { useNavigate, } from "react-router-dom"; import Authorize from "./components/Authorize"; +import AuthorizePageFrame from "./components/AuthorizePageFrame"; import ChangePasswordView from "./components/ChangePasswordView"; import Dashboard from "./components/Dashboard"; import EmailResetPasswordView from "./components/EmailResetPasswordView"; @@ -572,22 +573,9 @@ function AppContent() { ) : sessionData.passwordResetRequired ? ( ) : ( -
-
-
-
- - {branding.getTitle()} - -

{branding.getTitle()}

-
-
- -
-
- -
-
+ + + ) } /> diff --git a/packages/user-ui/src/components/Authorize.tsx b/packages/user-ui/src/components/Authorize.tsx index 60e8702e..1f8c768b 100644 --- a/packages/user-ui/src/components/Authorize.tsx +++ b/packages/user-ui/src/components/Authorize.tsx @@ -85,12 +85,19 @@ interface AuthorizeProps { keyState?: "locked" | "unlocked" | "setup_required"; }; onRecoverWithOldPassword?: () => void; + previewData?: { + organizations?: UserOrganization[]; + organizationsLoading?: boolean; + trustedDeviceCount?: number; + hasZkDeliveryScope?: boolean; + }; } export default function Authorize({ authRequest, sessionData, onRecoverWithOldPassword: _onRecoverWithOldPassword, + previewData, }: AuthorizeProps) { const branding = useBranding(); const [loading, setLoading] = useState(false); @@ -101,9 +108,15 @@ export default function Authorize({ const [recoverySecret, setRecoverySecret] = useState(""); const [recoveryLoading, setRecoveryLoading] = useState(false); const [keyUnlocked, setKeyUnlocked] = useState(sessionData.keyState === "unlocked"); - const [organizations, setOrganizations] = useState([]); - const [organizationsLoading, setOrganizationsLoading] = useState(true); - const [trustedDeviceCount, setTrustedDeviceCount] = useState(0); + const [organizations, setOrganizations] = useState( + previewData?.organizations || [] + ); + const [organizationsLoading, setOrganizationsLoading] = useState( + previewData?.organizationsLoading ?? !previewData + ); + const [trustedDeviceCount, setTrustedDeviceCount] = useState( + previewData?.trustedDeviceCount || 0 + ); const [unlockPolicy, setUnlockPolicy] = useState( authRequest.unlockPolicy || defaultUnlockPolicy ); @@ -127,7 +140,8 @@ export default function Authorize({ const sessionOrganizationId = sessionData.organizationId || ""; const requireOrganizationSelection = authRequest.requireOrganizationSelection !== false; const hasZkDeliveryScope = - authRequest.hasZk && new URL(window.location.href).searchParams.has("zk_pub"); + previewData?.hasZkDeliveryScope ?? + (authRequest.hasZk && new URL(window.location.href).searchParams.has("zk_pub")); const activeOrganizations = organizations.filter((organization) => organization.status ? organization.status === "active" : true ); @@ -167,12 +181,49 @@ export default function Authorize({ }, []); useEffect(() => { + if (previewData) { + return; + } if (authRequest.hasZk) { generateZkKeyPair(); } - }, [authRequest.hasZk, generateZkKeyPair]); + }, [authRequest.hasZk, generateZkKeyPair, previewData]); useEffect(() => { + if (previewData) { + const nextOrganizations = previewData.organizations || []; + const nextActiveOrganizations = nextOrganizations.filter((organization) => + organization.status ? organization.status === "active" : true + ); + setOrganizations(nextOrganizations); + setOrganizationsLoading(previewData.organizationsLoading ?? false); + setTrustedDeviceCount(previewData.trustedDeviceCount || 0); + setUnlockPolicy(authRequest.unlockPolicy || defaultUnlockPolicy); + setError(null); + setRecoveryVisible(false); + setDeviceApproval(null); + setDeviceApprovalCode(null); + setDeviceApprovalStatus(null); + setSelectedOrganizationId(() => { + if (!requireOrganizationSelection) return ""; + if ( + explicitOrganizationId && + nextActiveOrganizations.some((org) => org.organizationId === explicitOrganizationId) + ) { + return explicitOrganizationId; + } + if ( + sessionOrganizationId && + nextActiveOrganizations.some((org) => org.organizationId === sessionOrganizationId) + ) { + return sessionOrganizationId; + } + return nextActiveOrganizations.length === 1 + ? nextActiveOrganizations[0].organizationId + : ""; + }); + return; + } if (!requireOrganizationSelection) { setOrganizations([]); setOrganizationsLoading(false); @@ -218,9 +269,19 @@ export default function Authorize({ return () => { cancelled = true; }; - }, [explicitOrganizationId, requireOrganizationSelection, sessionOrganizationId]); + }, [ + authRequest.unlockPolicy, + explicitOrganizationId, + previewData, + requireOrganizationSelection, + sessionOrganizationId, + ]); useEffect(() => { + if (previewData) { + setTrustedDeviceCount(previewData.trustedDeviceCount || 0); + return; + } if (!authRequest.hasZk || keyUnlocked) { setTrustedDeviceCount(0); return; @@ -240,7 +301,7 @@ export default function Authorize({ return () => { cancelled = true; }; - }, [authRequest.hasZk, keyUnlocked]); + }, [authRequest.hasZk, keyUnlocked, previewData]); useEffect(() => { return () => { @@ -251,6 +312,10 @@ export default function Authorize({ }, []); useEffect(() => { + if (previewData) { + setUnlockPolicy(authRequest.unlockPolicy || defaultUnlockPolicy); + return; + } if (!authRequest.hasZk) { setUnlockPolicy(authRequest.unlockPolicy || defaultUnlockPolicy); return; @@ -267,7 +332,7 @@ export default function Authorize({ return () => { cancelled = true; }; - }, [authRequest.hasZk, authRequest.unlockPolicy]); + }, [authRequest.hasZk, authRequest.unlockPolicy, previewData]); useEffect(() => { if (!isUnlockMethodAllowed(unlockPolicy, unlockMethod)) { @@ -562,6 +627,9 @@ export default function Authorize({ setError("Choose which organization to use for this sign-in."); return; } + if (previewData) { + return; + } setLoading(true); setError(null); diff --git a/packages/user-ui/src/components/AuthorizePageFrame.tsx b/packages/user-ui/src/components/AuthorizePageFrame.tsx new file mode 100644 index 00000000..49021792 --- /dev/null +++ b/packages/user-ui/src/components/AuthorizePageFrame.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; +import { useBranding } from "../hooks/useBranding"; +import ThemeToggle from "./ThemeToggle"; + +export default function AuthorizePageFrame({ children }: { children: ReactNode }) { + const branding = useBranding(); + return ( +
+
+
+
+ + {branding.getTitle()} + +

{branding.getTitle()}

+
+
+ +
+
+ {children} +
+
+ ); +} diff --git a/packages/user-ui/src/components/Dashboard.tsx b/packages/user-ui/src/components/Dashboard.tsx index 0568e4df..6f32679c 100644 --- a/packages/user-ui/src/components/Dashboard.tsx +++ b/packages/user-ui/src/components/Dashboard.tsx @@ -32,6 +32,8 @@ interface App { interface DashboardProps { sessionData: SessionData; onLogout: () => void; + previewApps?: App[]; + previewKeyState?: KeyState; } function resolveKeyState(sessionData: SessionData): KeyState { @@ -58,11 +60,13 @@ function AppIcon({ app }: { app: App }) { return {appInitial(app)}; } -export default function Dashboard({ sessionData }: DashboardProps) { - const [apps, setApps] = useState([]); +export default function Dashboard({ sessionData, previewApps, previewKeyState }: DashboardProps) { + const [apps, setApps] = useState(previewApps || []); const [query, setQuery] = useState(""); - const [loading, setLoading] = useState(true); - const [keyState, setKeyState] = useState(resolveKeyState(sessionData)); + const [loading, setLoading] = useState(!previewApps); + const [keyState, setKeyState] = useState( + previewKeyState || resolveKeyState(sessionData) + ); const loadUserApps = useCallback(async () => { try { @@ -77,12 +81,17 @@ export default function Dashboard({ sessionData }: DashboardProps) { }, []); useEffect(() => { + if (previewApps) { + setApps(previewApps); + setLoading(false); + return; + } loadUserApps(); - }, [loadUserApps]); + }, [loadUserApps, previewApps]); useEffect(() => { - setKeyState(resolveKeyState(sessionData)); - }, [sessionData]); + setKeyState(previewKeyState || resolveKeyState(sessionData)); + }, [previewKeyState, sessionData]); const filteredApps = useMemo(() => { const normalized = query.trim().toLowerCase(); diff --git a/packages/user-ui/src/components/Profile.tsx b/packages/user-ui/src/components/Profile.tsx index 3dac6a68..5d2ddb52 100644 --- a/packages/user-ui/src/components/Profile.tsx +++ b/packages/user-ui/src/components/Profile.tsx @@ -23,6 +23,7 @@ interface ProfileProps { email?: string | null; signInEmail?: string | null; }) => void; + previewProfile?: UserProfile; } type EditingField = "name" | "email" | null; @@ -53,13 +54,20 @@ function isEmailLike(value: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); } -export default function Profile({ sessionData, onLogout, onProfileChanged }: ProfileProps) { +export default function Profile({ + sessionData, + onLogout, + onProfileChanged, + previewProfile, +}: ProfileProps) { const navigate = useNavigate(); const portal = useUserPortal(); const organizations = portal?.organizations || []; const organizationsLoading = portal?.organizationsLoading || false; - const [profile, setProfile] = useState(() => profileFromSession(sessionData)); - const [loadingProfile, setLoadingProfile] = useState(true); + const [profile, setProfile] = useState( + () => previewProfile || profileFromSession(sessionData) + ); + const [loadingProfile, setLoadingProfile] = useState(!previewProfile); const [editingField, setEditingField] = useState(null); const [nameDraft, setNameDraft] = useState(sessionData.name || ""); const [emailDraft, setEmailDraft] = useState(""); @@ -88,10 +96,19 @@ export default function Profile({ sessionData, onLogout, onProfileChanged }: Pro }, [onProfileChanged]); useEffect(() => { + if (previewProfile) { + setProfile(previewProfile); + setNameDraft(previewProfile.name || ""); + setLoadingProfile(false); + return; + } setProfile((current) => ({ ...profileFromSession(sessionData), ...current })); - }, [sessionData]); + }, [previewProfile, sessionData]); useEffect(() => { + if (previewProfile) { + return; + } let cancelled = false; apiService .getProfile() @@ -114,7 +131,7 @@ export default function Profile({ sessionData, onLogout, onProfileChanged }: Pro return () => { cancelled = true; }; - }, [onProfileChanged]); + }, [onProfileChanged, previewProfile]); const activeOrganizations = useMemo( () => diff --git a/packages/user-ui/src/components/SettingsSecurity.tsx b/packages/user-ui/src/components/SettingsSecurity.tsx index c2e6af02..e94d0aea 100644 --- a/packages/user-ui/src/components/SettingsSecurity.tsx +++ b/packages/user-ui/src/components/SettingsSecurity.tsx @@ -45,16 +45,37 @@ type SecuritySection = | "mfa" | "advanced"; +export type SettingsSecurityPreviewData = { + status: { + enabled: boolean; + verified: boolean; + created_at?: string | null; + last_used_at?: string | null; + backup_codes_remaining?: number; + required?: boolean; + } | null; + keybag: KeybagResponse | null; + trustedDevices: TrustedDeviceResponse[]; + deviceApprovals: DeviceApprovalResponse[]; + recoveryKeys: RecoveryKeyResponse[]; + passkeys: WebAuthnCredentialResponse[]; + unlockPolicy: UnlockPolicy; + connectedIdentities: ConnectedIdentityResponse[]; + enterpriseSsoRoute: FederationConnectionRoute | null; +}; + export default function SettingsSecurity({ sessionData, + previewData, }: { sessionData: { sub: string; email?: string; keyState?: "locked" | "unlocked" | "setup_required"; }; + previewData?: SettingsSecurityPreviewData; }) { - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(!previewData); const [error, setError] = useState(null); const [status, setStatus] = useState<{ enabled: boolean; @@ -63,29 +84,41 @@ export default function SettingsSecurity({ last_used_at?: string | null; backup_codes_remaining?: number; required?: boolean; - } | null>(null); + } | null>(previewData?.status || null); const [provisioningUri, setProvisioningUri] = useState(null); const [secret, setSecret] = useState(null); const [code, setCode] = useState(""); const [backupCodes, setBackupCodes] = useState(null); - const [keybag, setKeybag] = useState(null); - const [trustedDevices, setTrustedDevices] = useState([]); - const [deviceApprovals, setDeviceApprovals] = useState([]); + const [keybag, setKeybag] = useState(previewData?.keybag || null); + const [trustedDevices, setTrustedDevices] = useState( + previewData?.trustedDevices || [] + ); + const [deviceApprovals, setDeviceApprovals] = useState( + previewData?.deviceApprovals || [] + ); const [deviceActionLoading, setDeviceActionLoading] = useState(null); const [trustDeviceLoading, setTrustDeviceLoading] = useState(false); const [trustedDeviceMessage, setTrustedDeviceMessage] = useState(null); - const [recoveryKeys, setRecoveryKeys] = useState([]); + const [recoveryKeys, setRecoveryKeys] = useState( + previewData?.recoveryKeys || [] + ); const [recoverySecret, setRecoverySecret] = useState(null); const [recoverySecretSaved, setRecoverySecretSaved] = useState(false); const [recoveryActionLoading, setRecoveryActionLoading] = useState(null); const [passkeyActionLoading, setPasskeyActionLoading] = useState(false); const [passkeyRevokeLoading, setPasskeyRevokeLoading] = useState(null); const [passkeyMessage, setPasskeyMessage] = useState(null); - const [passkeys, setPasskeys] = useState([]); - const [unlockPolicy, setUnlockPolicy] = useState(defaultUnlockPolicy); - const [connectedIdentities, setConnectedIdentities] = useState([]); + const [passkeys, setPasskeys] = useState( + previewData?.passkeys || [] + ); + const [unlockPolicy, setUnlockPolicy] = useState( + previewData?.unlockPolicy || defaultUnlockPolicy + ); + const [connectedIdentities, setConnectedIdentities] = useState( + previewData?.connectedIdentities || [] + ); const [enterpriseSsoRoute, setEnterpriseSsoRoute] = useState( - null + previewData?.enterpriseSsoRoute || null ); const [passkeyCompatibility, setPasskeyCompatibility] = useState<{ webauthn: boolean; @@ -97,6 +130,23 @@ export default function SettingsSecurity({ const [activeSection, setActiveSection] = useState("overview"); const reload = useCallback(async () => { + if (previewData) { + setLoading(false); + setError(null); + setBackupCodes(null); + setStatus(previewData.status); + setKeybag(previewData.keybag); + setTrustedDevices(previewData.trustedDevices); + setDeviceApprovals(previewData.deviceApprovals); + setRecoveryKeys(previewData.recoveryKeys); + setPasskeys(previewData.passkeys); + setUnlockPolicy(previewData.unlockPolicy); + setConnectedIdentities(previewData.connectedIdentities); + setEnterpriseSsoRoute(previewData.enterpriseSsoRoute); + setProvisioningUri(null); + setSecret(null); + return; + } try { setLoading(true); setError(null); @@ -129,7 +179,7 @@ export default function SettingsSecurity({ } finally { setLoading(false); } - }, [sessionData.email]); + }, [previewData, sessionData.email]); const refreshDeviceApprovals = useCallback(async () => { try { @@ -144,6 +194,7 @@ export default function SettingsSecurity({ }, [reload]); useEffect(() => { + if (previewData) return undefined; const activeDeviceCount = trustedDevices.filter((device) => !device.revoked_at).length; if (loading || activeDeviceCount < 1) return undefined; const interval = window.setInterval(() => { @@ -153,7 +204,7 @@ export default function SettingsSecurity({ .catch(() => {}); }, 10000); return () => window.clearInterval(interval); - }, [loading, trustedDevices]); + }, [loading, previewData, trustedDevices]); useEffect(() => { const credentialApi = window.PublicKeyCredential as diff --git a/packages/user-ui/src/components/SettingsSecurityView.tsx b/packages/user-ui/src/components/SettingsSecurityView.tsx index 5bb9b5c7..cc41a5b6 100644 --- a/packages/user-ui/src/components/SettingsSecurityView.tsx +++ b/packages/user-ui/src/components/SettingsSecurityView.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { loadUnlockedArk } from "../services/unlockedArk"; import KeyUnlockPanel from "./KeyUnlockPanel"; import { PortalHeader, PortalPage, PortalSection } from "./Portal"; -import SettingsSecurity from "./SettingsSecurity"; +import SettingsSecurity, { type SettingsSecurityPreviewData } from "./SettingsSecurity"; import UserLayout from "./UserLayout"; type KeyState = "locked" | "unlocked" | "setup_required"; @@ -25,9 +25,11 @@ function resolveKeyState(sessionData: SettingsSessionData): KeyState { export default function SettingsSecurityView({ sessionData, onLogout, + previewData, }: { sessionData: SettingsSessionData; onLogout: () => void; + previewData?: SettingsSecurityPreviewData; }) { const navigate = useNavigate(); const [keyState, setKeyState] = useState(() => resolveKeyState(sessionData)); @@ -68,7 +70,7 @@ export default function SettingsSecurityView({ /> ) : null} - + ); diff --git a/packages/user-ui/src/exports.ts b/packages/user-ui/src/exports.ts index abaef536..f1c8ca4d 100644 --- a/packages/user-ui/src/exports.ts +++ b/packages/user-ui/src/exports.ts @@ -1,5 +1,11 @@ export { default as Authorize } from "./components/Authorize"; +export { default as AuthorizePageFrame } from "./components/AuthorizePageFrame"; +export { default as Dashboard } from "./components/Dashboard"; export { default as Login } from "./components/Login"; export { default as LoginView } from "./components/LoginView"; +export { default as Profile } from "./components/Profile"; export { default as Register } from "./components/Register"; +export type { SettingsSecurityPreviewData } from "./components/SettingsSecurity"; +export { default as SettingsSecurityView } from "./components/SettingsSecurityView"; +export { UserPortalProvider } from "./components/UserPortalContext"; export { useBranding } from "./hooks/useBranding"; From 21e4eaa47725702208d5269639821d10689725e8 Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Wed, 8 Jul 2026 21:19:44 +0100 Subject: [PATCH 2/2] feat(admin): reuse user ui in branding previews --- packages/admin-ui/package.json | 1 + packages/admin-ui/src/pages/Branding.tsx | 48 +-- .../src/preview/BrandingPreviewShared.test.js | 34 ++ packages/admin-ui/src/preview/main.tsx | 401 +++++++++--------- packages/admin-ui/src/preview/preview.css | 141 ------ .../tests/admin/branding/branding.spec.ts | 4 +- 6 files changed, 245 insertions(+), 384 deletions(-) create mode 100644 packages/admin-ui/src/preview/BrandingPreviewShared.test.js delete mode 100644 packages/admin-ui/src/preview/preview.css diff --git a/packages/admin-ui/package.json b/packages/admin-ui/package.json index edfc7e24..ad07ee3f 100644 --- a/packages/admin-ui/package.json +++ b/packages/admin-ui/package.json @@ -8,6 +8,7 @@ "dev": "vite --port 5174", "build": "tsc && vite build", "preview": "vite preview", + "test": "node --test \"src/**/*.test.js\"", "typecheck": "tsc --noEmit", "format": "biome format --write .", "lint": "biome lint .", diff --git a/packages/admin-ui/src/pages/Branding.tsx b/packages/admin-ui/src/pages/Branding.tsx index 76520394..827bfb21 100644 --- a/packages/admin-ui/src/pages/Branding.tsx +++ b/packages/admin-ui/src/pages/Branding.tsx @@ -412,13 +412,13 @@ export default function Branding() { faviconDark, ]); - useEffect(() => { + const postPreviewState = useCallback(() => { try { - iframeRef.current?.contentWindow?.postMessage( - { type: "da:theme", theme: activeMode }, - window.location.origin - ); - iframeRef.current?.contentWindow?.postMessage( + const target = iframeRef.current?.contentWindow; + if (!target) return; + target.postMessage({ type: "da:theme", theme: activeMode }, window.location.origin); + target.postMessage({ type: "da:branding", payload: previewPayload }, window.location.origin); + target.postMessage( { type: "da:preview-screen", screen: previewScreen, @@ -429,21 +429,14 @@ export default function Branding() { } catch { return; } - }, [activeMode, previewScreen, authorizePreviewVariant]); + }, [activeMode, authorizePreviewVariant, previewPayload, previewScreen]); useEffect(() => { const timer = window.setTimeout(() => { - try { - iframeRef.current?.contentWindow?.postMessage( - { type: "da:branding", payload: previewPayload }, - window.location.origin - ); - } catch { - return; - } + postPreviewState(); }, 0); return () => window.clearTimeout(timer); - }, [previewPayload]); + }, [postPreviewState]); useEffect(() => { const onMessage = (event: MessageEvent) => { @@ -955,28 +948,7 @@ export default function Branding() { ref={iframeRef} title="Branding Preview" src={previewUrl} - onLoad={() => { - try { - iframeRef.current?.contentWindow?.postMessage( - { type: "da:theme", theme: activeMode }, - window.location.origin - ); - iframeRef.current?.contentWindow?.postMessage( - { type: "da:branding", payload: previewPayload }, - window.location.origin - ); - iframeRef.current?.contentWindow?.postMessage( - { - type: "da:preview-screen", - screen: previewScreen, - authorizeVariant: authorizePreviewVariant, - }, - window.location.origin - ); - } catch { - return; - } - }} + onLoad={postPreviewState} style={{ width: previewWidth, height: 760, diff --git a/packages/admin-ui/src/preview/BrandingPreviewShared.test.js b/packages/admin-ui/src/preview/BrandingPreviewShared.test.js new file mode 100644 index 00000000..8f4954ef --- /dev/null +++ b/packages/admin-ui/src/preview/BrandingPreviewShared.test.js @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const source = readFileSync(resolve(here, "main.tsx"), "utf8"); + +test("branding preview delegates user surfaces to shared user-ui components", () => { + assert.match(source, /from "@DarkAuth\/user-ui\/src\/exports"/); + for (const component of [ + "Authorize", + "AuthorizePageFrame", + "Dashboard", + "LoginView", + "Profile", + "SettingsSecurityView", + ]) { + assert.match(source, new RegExp(`\\b${component}\\b`)); + } + assert.equal(/function\s+PreviewOrganizations\b/.test(source), false); + assert.equal(/function\s+PreviewAuthorizeScopes\b/.test(source), false); + assert.equal(source.includes("./preview.css"), false); + assert.equal(existsSync(resolve(here, "preview.css")), false); +}); + +test("branding authorize preview uses the real authorize component for variants", () => { + assert.match(source, //); + assert.match(source, / - - + ); } function PreviewSecurity() { return ( - -
-

Security

-

Security overview

-

Review sign-in, encrypted access, and recovery.

-
- {[ - ["Sign-in", "Password and enterprise identity", "Ready"], - ["Encrypted app access", "Passkey unlock and trusted browser", "Ready"], - ["Recovery", "Offline recovery key", "Action needed"], - ].map(([title, detail, state]) => ( - - ))} -
-
-
- ); -} - -function PreviewOrganizations({ variant }: { variant: AuthorizePreviewVariant }) { - if (variant === "without-orgs" || variant === "zk-key") return null; - if (variant === "no-active-orgs") { - return ( -
-

Organization

-
-

No active organizations are available for this sign-in.

-
-
- ); - } - return ( -
-

Organization

-
- Choose which organization to use for this sign-in. -
- - -
-
-
+ ); } -function PreviewAuthorizeScopes({ variant }: { variant: AuthorizePreviewVariant }) { - const scopes = - variant === "zk-key" - ? [ - ["K", "Encrypted app key", "Share an encrypted app key with Gitea"], - ["@", "Email address", "Access your email address"], - ["P", "Profile information", "Access your profile information"], - ] - : [ - ["@", "", "Access your email address"], - ["P", "", "Access your profile information"], - ["O", "", "Authenticate you"], - ]; - return ( -
-

Permissions

-
    - {scopes.map(([icon, name, description]) => ( -
  • - {icon} -
    - {name ? {name} : null} - {description} -
    -
  • - ))} -
-
- ); +function authorizeOrganizationsForVariant() { + if (authorizeVariant === "without-orgs" || authorizeVariant === "zk-key") return []; + if (authorizeVariant === "no-active-orgs") return inactivePreviewOrganizations; + return previewOrganizations; } function PreviewAuthorize() { - const branding = useBranding(); - const appName = authorizeVariant === "zk-key" ? "Atlas" : "Gitea"; - const appInitial = authorizeVariant === "zk-key" ? "A" : "G"; + const isZkKey = authorizeVariant === "zk-key"; + const requireOrganizationSelection = authorizeVariant !== "without-orgs" && !isZkKey; return ( -
-
-
-
- - {branding.getTitle()} - -

{branding.getTitle() || "DarkAuth"}

-
-
- -
-
-
-
-
-
-
{appInitial}
-
-

- {authorizeVariant === "zk-key" - ? `Continue to ${appName}` - : "Authorize Application"} -

-

- {authorizeVariant === "zk-key" - ? "Review what this app can access before continuing." - : `${appName} would like to:`} -

-
-
-
-
M
-
-

Signed in as

-

Mark Wylde

-

mark@wylde.net

-
-
-
- - -
- - -
-
-

- By continuing, you allow {appName} to access the requested information. You can - revoke this access later. -

-
-
-
-
-
+ + + ); } function PreviewProfile() { return ( - -
-

Profile

-

Avery Stone

-

Manage account details and organization context.

-
-
- Name - Avery Stone -
-
- Email - avery@example.com -
-
- Current organization - Acme Labs -
-
-
-
+ {}, + refreshOrganizations: async () => previewOrganizations, + addCreatedOrganization: noop, + }} + > + + ); } diff --git a/packages/admin-ui/src/preview/preview.css b/packages/admin-ui/src/preview/preview.css deleted file mode 100644 index e3185be2..00000000 --- a/packages/admin-ui/src/preview/preview.css +++ /dev/null @@ -1,141 +0,0 @@ -.preview-screen { - display: grid; - gap: 1rem; - width: 100%; - color: var(--da-color-text); - text-align: left; -} - -.preview-narrow { - width: min(100%, 760px); - margin: 0 auto; -} - -.preview-kicker { - margin: 0; - color: var(--da-color-action); - font-size: 0.8125rem; - font-weight: 800; -} - -.preview-screen h2 { - margin: 0; - color: var(--da-color-text); - font-size: clamp(1.75rem, 7vw, 2.5rem); - font-weight: 800; - letter-spacing: 0; -} - -.preview-muted { - margin: -0.75rem 0 0; - color: var(--da-color-text-muted); - font-size: 0.9375rem; -} - -.preview-app-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); - gap: 0.75rem; -} - -.preview-app-tile, -.preview-status-row, -.preview-profile-row { - display: grid; - align-items: center; - gap: 0.75rem; - padding: 0.875rem; - border: 1px solid var(--da-color-border); - border-radius: 0.5rem; - background: var(--da-color-surface); - color: var(--da-color-text); - text-decoration: none; -} - -.preview-app-tile { - grid-template-columns: auto minmax(0, 1fr) auto; -} - -.preview-app-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 2.75rem; - height: 2.75rem; - border-radius: 0.5rem; - background: var(--da-color-icon-bg); - color: var(--da-color-icon-text); - font-weight: 800; -} - -.preview-app-tile span:not(.preview-app-icon), -.preview-status-row span { - display: grid; - gap: 0.125rem; - min-width: 0; -} - -.preview-app-tile strong, -.preview-status-row strong, -.preview-profile-row strong { - color: var(--da-color-text); - font-size: 1rem; -} - -.preview-app-tile small, -.preview-status-row small, -.preview-profile-row span { - color: var(--da-color-text-muted); - font-size: 0.875rem; -} - -.preview-app-tile b, -.preview-status-row b { - color: var(--da-color-action); - font-size: 0.8125rem; -} - -.preview-status-list { - display: grid; - gap: 0.75rem; -} - -.preview-status-row { - grid-template-columns: minmax(0, 1fr) auto; - width: 100%; - text-align: left; - cursor: default; -} - -.authorize-organization-empty { - padding: 0.875rem; - border: 1px solid var(--da-color-border); - border-radius: var(--da-control-radius, 0.5rem); - background: var(--da-color-surface-raised); - color: var(--da-color-text-muted); -} - -.authorize-organization-empty p { - margin: 0; -} - -[data-da-theme] .success-button { - border-color: color-mix(in srgb, var(--da-color-success, var(--da-color-action)) 70%, black); - background: var(--da-color-success, var(--da-color-action)); - color: var(--da-color-success-text, var(--da-color-action-text)); -} - -[data-da-theme] .success-button:hover:not(:disabled) { - background: color-mix(in srgb, var(--da-color-success, var(--da-color-action)) 88%, black); -} - -.preview-profile-row { - grid-template-columns: 9rem minmax(0, 1fr); -} - -@media (max-width: 520px) { - .preview-app-grid, - .preview-profile-row { - grid-template-columns: 1fr; - } -} diff --git a/packages/test-suite/tests/admin/branding/branding.spec.ts b/packages/test-suite/tests/admin/branding/branding.spec.ts index 1f1f7d05..020803f9 100644 --- a/packages/test-suite/tests/admin/branding/branding.spec.ts +++ b/packages/test-suite/tests/admin/branding/branding.spec.ts @@ -249,10 +249,10 @@ test.describe('Admin - Branding Settings', () => { await page.getByRole('tab', { name: 'Dark' }).click(); await page.getByRole('button', { name: 'App key' }).click(); - await expect(frame.getByText('Continue to Atlas')).toBeVisible(); + await expect(frame.getByText('Example Client 1 would like to:')).toBeVisible(); await expect(frame.getByText('Encrypted app key', { exact: true })).toBeVisible(); - const buttonColor = await frame.getByRole('button', { name: 'Continue' }).evaluate((element) => { + const buttonColor = await frame.getByRole('button', { name: 'Authorize' }).evaluate((element) => { const styles = window.getComputedStyle(element); return { backgroundColor: styles.backgroundColor, color: styles.color }; });