Files
arcadia-admin/app/routes/login.2fa.tsx
jules 20c592dfa7 admin: completeness + UI consistency pass
Arcadia wiring:
- home: real Overview dashboard (tenants/users/audit/health probe) replacing the inherited Vibespace welcome tiles; skeleton loaders, refresh button, registers admin context
- profile: split into Account (synced via getUser/updateUser of session user) and local Preferences; updateSessionUser keeps the appbar in sync after edits
- session: drop unused signIn mock, add updateSessionUser, refresh tests
- profile schema: drop redundant Profile.name/email (session is the source of truth)
- routes: delete orphaned resources route + lib

Auth flows that previously 404'd:
- /signup, /login/forgot, /login/reset, /login/2fa wired via @crema/arcadia-auth-ui
- shared AuthShell + AuthBrand wrapper

Assistant tools (admin-tools.ts):
- +10 tools: deactivate_tenant, set_user_status, delete_user, list_memberships, list_roles, revoke_api_key, create_user, update_user, assign_role, remove_role
- list_memberships gains user_id filter for "tenants this user belongs to" queries
- search_kb / read_chunk: new token resolution (window override → VITE_ARCADIA_SEARCH_TOKEN service token → operator session JWT → "dev"); on 401/403 emit a tailored hint based on which token was used

UI consistency:
- new PageHeader component
- AppShell.title was unrendered — dropped; first-child padding on #main-content keeps the floating actions pill from colliding with header content
- removed dead "Sign in required" fallback cards from 14 routes (AppShell already redirects)
- stripped p-6 from outer wrappers across 14 routes (was double-padding under AppShell's own p-6)
- migrated home + tenants to PageHeader

arcadia-search ergonomics:
- scripts/mint-search-token.mjs + `npm run mint:search-token` mints HS512 JWT with required tenant_id claim, upserts VITE_ARCADIA_SEARCH_TOKEN into .env.local
- README/.env document the new VITE_ARCADIA_SEARCH_URL / VITE_ARCADIA_SEARCH_TOKEN knobs
- .env.local now gitignored

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:37:31 +10:00

62 lines
1.9 KiB
TypeScript

import { useState } from "react"
import { useNavigate, useSearchParams } from "react-router"
import { TwoFactorChallengeForm } from "@crema/arcadia-auth-ui"
import { pageTitle } from "~/lib/page-meta"
import { persistFromArcadiaLogin } from "~/lib/session"
import { AuthBrand, AuthShell } from "~/components/auth/auth-shell"
export const meta = () => pageTitle("Two-factor verification")
export default function TwoFactorRoute() {
const [params] = useSearchParams()
const navigate = useNavigate()
const challenge = params.get("challenge") ?? ""
const next = params.get("next") || "/"
const [mode, setMode] = useState<"totp" | "recovery">("totp")
if (!challenge) {
return (
<AuthShell>
<div
className="flex w-full max-w-sm flex-col items-center gap-3 rounded-xl border bg-card p-6 text-center text-sm"
style={{ borderColor: "var(--border)" }}
>
<h1 className="text-base font-semibold">Challenge missing</h1>
<p className="text-muted-foreground">
This page is only reachable after a sign-in attempt. Start over.
</p>
<button
type="button"
onClick={() => navigate("/login")}
className="mt-2 text-xs font-medium text-primary hover:underline"
data-action="2fa-back-to-login"
>
Back to sign in
</button>
</div>
</AuthShell>
)
}
return (
<AuthShell>
<TwoFactorChallengeForm
brand={<AuthBrand />}
challenge={challenge}
mode={mode}
onUseRecoveryCode={mode === "totp" ? () => setMode("recovery") : undefined}
onBack={
mode === "recovery"
? () => setMode("totp")
: () => navigate("/login")
}
onSuccess={({ tokens, user }) => {
persistFromArcadiaLogin(tokens, user)
navigate(next, { replace: true })
}}
/>
</AuthShell>
)
}