pediatric-ai-scribe-v3/public/app/assets/Dictation-Dk3CctmR.js
Daniel c157c709c8 feat: React auth screen + SPA now serves every route (vanilla retired)
The final big piece of "everything in React + Tailwind". Login,
register, forgot-password, reset-password, and email-verification all
render from the React bundle now. The root path / serves the SPA,
vanilla index.html + public/js/* are no longer served by the server.

BACKEND — src/routes/auth.ts
  New GET /api/auth/public-config (public — no auth required) returns
  { registrationEnabled, turnstileSiteKey, oidcEnabled,
    disableLocalAuth, ssoButtonLabel }.
  Single round-trip the React auth screen needs on mount. Reuses
  existing DB settings; no new tables.

BACKEND — server.ts
  • / and /index.html now send public/app/index.html (React SPA),
    not public/index.html (vanilla).
  • /auth, /reset-password, /verify-email explicitly route to the SPA
    so the email links land on the React router.
  • /app/*splat preserved as an alias so old bookmarks keep working.
  • SPA fallback added after express.static so hard-refresh on
    /encounter / /bedside / /settings etc. serves the React index
    instead of 404ing. API paths and static-file extensions still
    fall through to their existing handlers.
  • The dead app.get('/') duplicate that also pointed at the vanilla
    index is removed.

CLIENT — React auth flow
  client/src/pages/Auth.tsx (new)
    Login / register / forgot sub-forms with a single useQuery on
    ['public-config'] driving Turnstile + SSO button visibility.
    Login flow handles all three vanilla-equivalent responses
    (token / requires2FA / needsVerification). 2FA field reveals
    inline when the server asks for it; resend-verification link
    appears when needsVerification fires. SSO button renders
    whenever oidcEnabled is true, even if local auth is disabled
    (disableLocalAuth hides the login/register/forgot forms
    entirely). HIPAA notice + APK download link preserved.

  client/src/pages/ResetPassword.tsx (new)
    Reads ?token=xxx from the URL, POSTs /api/auth/reset-password.
    Confirm-password match, 8+ char validation, server
    passwordWarning (pwned password) surfaces as an amber info box.
    Redirects to /auth 2.5 s after success.

  client/src/components/Turnstile.tsx (new)
    Loads the challenges.cloudflare.com/turnstile script once,
    renders a widget per form, calls onToken(token) on success and
    onToken('') on error / expiry. If siteKey is null/empty (e2e
    container with TURNSTILE_SITE_KEY="") renders nothing and
    auto-reports empty — matches the vanilla no-key-no-widget
    behaviour.

  client/src/components/AuthGuard.tsx (new)
    useQuery(['auth-me']) with retry: false. On 401/error redirects
    to /auth?next=<current-url> so the deep link survives sign-in.
    Used as a parent route in App.tsx wrapping every private page.

  client/src/components/Layout.tsx
    "← back to legacy app" link replaced with "Sign out" — calls
    POST /api/auth/logout then window.location = /auth.

  client/src/App.tsx
    BrowserRouter no longer has basename (was "/app"). Public
    routes: /auth, /reset-password. Everything else lives under
    <AuthGuard> → <Layout>. Lazy-loaded Auth + ResetPassword join
    the existing heavy-route code-split.

  client/vite.config.ts
    base stays "/app/" so hashed asset URLs resolve to
    /app/assets/... (served unchanged by express.static).

shared/types.ts + client/src/shared/types.ts — additive:
  PublicConfigOk { registrationEnabled, turnstileSiteKey,
    oidcEnabled, disableLocalAuth, ssoButtonLabel }.

Bundle — Auth chunk splits out at 10.87 kB / 3.35 kB gz, lazy-loaded
only on the sign-in path; initial bundle unchanged at 343.97 kB /
106.59 kB gz.

Backend tsc + client tsc + vite build + 136/136 vitest all green.
2026-04-24 02:45:21 +02:00

1 line
No EOL
3.8 KiB
JavaScript

import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{c as r,i,t as a}from"./index-BuU36CvS.js";var o=e(t(),1),s=n();function c(){let[e,t]=(0,o.useState)(``),[n,c]=(0,o.useState)(``),[l,u]=(0,o.useState)(`outpatient`),[d,f]=(0,o.useState)(``),[p,m]=(0,o.useState)(null),[h,g]=(0,o.useState)(null),_=r({mutationFn:e=>i.post(`/api/generate-hpi-dictation`,e),onSuccess:e=>m(e.hpi),onError:()=>m(null)});function v(t){t.preventDefault(),g(null);let r={transcript:d,patientAge:e,patientGender:n,setting:l},i=a.safeParse(r);if(!i.success){g(i.error.issues.map(e=>e.message).join(`, `));return}m(null),_.mutate(i.data)}function y(){f(``),m(null),g(null)}let b=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`;return(0,s.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,s.jsxs)(`header`,{children:[(0,s.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Voice Dictation → HPI`}),(0,s.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Dictate your narrative → AI restructures into polished HPI. Audio-capture UI is a follow-up; this minimal form supports typed/pasted transcripts.`})]}),(0,s.jsxs)(`form`,{onSubmit:v,className:`space-y-4`,children:[(0,s.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,s.jsx)(`input`,{className:b,placeholder:`e.g. 8 months`,value:e,onChange:e=>t(e.target.value)})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,s.jsxs)(`select`,{className:b,value:n,onChange:e=>c(e.target.value),children:[(0,s.jsx)(`option`,{value:``,children:`Select`}),(0,s.jsx)(`option`,{children:`Male`}),(0,s.jsx)(`option`,{children:`Female`})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Setting`}),(0,s.jsxs)(`select`,{className:b,value:l,onChange:e=>u(e.target.value),children:[(0,s.jsx)(`option`,{value:`outpatient`,children:`Outpatient`}),(0,s.jsx)(`option`,{value:`inpatient`,children:`Inpatient / Floors`})]})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript / dictation`}),(0,s.jsx)(`button`,{type:`button`,onClick:y,className:`text-xs text-muted-foreground underline`,children:`Clear`})]}),(0,s.jsx)(`textarea`,{className:b+` min-h-[200px] font-mono text-sm`,placeholder:`Type or paste your dictation here, then click Generate.`,value:d,onChange:e=>f(e.target.value)})]}),h&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:h}),_.error&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:_.error.message}),(0,s.jsx)(`div`,{className:`flex gap-2`,children:(0,s.jsx)(`button`,{type:`submit`,disabled:_.isPending||!d.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:_.isPending?`Generating…`:`Generate HPI`})})]}),p&&(0,s.jsxs)(`section`,{className:`rounded-lg border border-border bg-card`,children:[(0,s.jsxs)(`header`,{className:`px-4 py-2 border-b border-border flex items-center justify-between bg-muted/40`,children:[(0,s.jsx)(`h2`,{className:`text-sm font-semibold`,children:`Generated HPI`}),(0,s.jsx)(`button`,{onClick:()=>navigator.clipboard.writeText(p),className:`text-xs text-muted-foreground underline`,children:`Copy`})]}),(0,s.jsx)(`div`,{className:`p-4 whitespace-pre-wrap text-sm`,children:p})]})]})}export{c as default};