88 useLayoutEffect ,
99 useMemo ,
1010 useState ,
11- type FormEvent ,
1211 type ReactNode ,
1312} from "react" ;
13+ import { useRouter } from "next/navigation" ;
1414import {
1515 clearConfigApiIdentity ,
1616 configApi ,
@@ -19,6 +19,7 @@ import {
1919 type ConfigApiIdentity ,
2020} from "../../lib/config-api/client" ;
2121import type { DevIdentityUser } from "../../lib/config-api" ;
22+ import { AUTH_BUTTON_CLASS , PasswordAuthShell } from "../../components/auth/auth-flow" ;
2223
2324const IDENTITY_STORAGE_KEY = "data-tasks:identity:v1" ;
2425const DEV_SIGNED_OUT_STORAGE_KEY = "data-tasks:identity:signed-out:v1" ;
@@ -277,16 +278,13 @@ function DevIdentityProvider({ children }: { children: ReactNode }) {
277278
278279function DevSignedOutScreen ( { onContinue } : { onContinue : ( ) => void } ) {
279280 return (
280- < PasswordAuthShell title = "Signed out" >
281- < div className = "flex w-full max-w-sm flex-col gap-3" >
282- < p className = "text-sm text-muted-light" >
283- Local dev mode uses a built-in account. Continue when you are ready to return to the workspace.
281+ < PasswordAuthShell title = "Signed out" subtitle = "Local development mode" >
282+ < div className = "flex flex-col gap-4" >
283+ < p className = "text-sm leading-relaxed text-muted" >
284+ Local dev mode uses a built-in account. Continue when you are ready to return to the
285+ workspace.
284286 </ p >
285- < button
286- type = "button"
287- onClick = { onContinue }
288- className = "h-10 rounded-md bg-primary px-3 text-sm font-semibold text-white"
289- >
287+ < button type = "button" onClick = { onContinue } className = { AUTH_BUTTON_CLASS } >
290288 Continue as Dev User
291289 </ button >
292290 </ div >
@@ -295,6 +293,7 @@ function DevSignedOutScreen({ onContinue }: { onContinue: () => void }) {
295293}
296294
297295function PasswordIdentityProvider ( { children } : { children : ReactNode } ) {
296+ const router = useRouter ( ) ;
298297 const [ currentUser , setCurrentUser ] = useState < ConfigApiIdentity | null > ( null ) ;
299298 const [ loading , setLoading ] = useState ( true ) ;
300299 const [ error , setError ] = useState < string | null > ( null ) ;
@@ -365,11 +364,17 @@ function PasswordIdentityProvider({ children }: { children: ReactNode }) {
365364 } ;
366365 } , [ changePassword , createUser , currentUser , error , loading , signOut , signOutAll ] ) ;
367366
367+ useEffect ( ( ) => {
368+ if ( ! loading && ( ! currentUser || ! value ) ) {
369+ router . replace ( "/login" ) ;
370+ }
371+ } , [ loading , currentUser , value , router ] ) ;
372+
368373 if ( loading ) {
369374 return < PasswordAuthShell title = "Loading account..." /> ;
370375 }
371376 if ( ! currentUser || ! value ) {
372- return < PasswordAuthScreen error = { error } onAuthenticated = { loadMe } /> ;
377+ return < PasswordAuthShell title = "Redirecting to sign in..." /> ;
373378 }
374379 return (
375380 < DataTaskIdentityContext . Provider value = { value } >
@@ -378,134 +383,6 @@ function PasswordIdentityProvider({ children }: { children: ReactNode }) {
378383 ) ;
379384}
380385
381- function PasswordAuthScreen ( {
382- error,
383- onAuthenticated,
384- } : {
385- error : string | null ;
386- onAuthenticated : ( ) => Promise < void > ;
387- } ) {
388- const [ mode , setMode ] = useState < "login" | "register" | "forgot" | "verify" | "reset" > ( "login" ) ;
389- const [ displayName , setDisplayName ] = useState ( "" ) ;
390- const [ email , setEmail ] = useState ( "" ) ;
391- const [ password , setPassword ] = useState ( "" ) ;
392- const [ token , setToken ] = useState ( "" ) ;
393- const [ message , setMessage ] = useState < string | null > ( null ) ;
394- const [ submitting , setSubmitting ] = useState ( false ) ;
395- const [ localError , setLocalError ] = useState < string | null > ( null ) ;
396-
397- const submit = async ( event : FormEvent < HTMLFormElement > ) => {
398- event . preventDefault ( ) ;
399- setSubmitting ( true ) ;
400- setLocalError ( null ) ;
401- setMessage ( null ) ;
402- try {
403- if ( mode === "login" ) {
404- await configApi . login ( { email, password } ) ;
405- await onAuthenticated ( ) ;
406- return ;
407- }
408- if ( mode === "register" ) {
409- const result = await configApi . register ( { email, password, displayName } ) ;
410- setMessage ( result . verificationToken ? `Verify email token: ${ result . verificationToken } ` : "Verify email" ) ;
411- setMode ( "verify" ) ;
412- return ;
413- }
414- if ( mode === "forgot" ) {
415- const result = await configApi . forgotPassword ( { email } ) ;
416- setMessage ( result . resetToken ? `Reset token: ${ result . resetToken } ` : "Check your email for a reset link." ) ;
417- setMode ( "reset" ) ;
418- return ;
419- }
420- if ( mode === "verify" ) {
421- await configApi . verifyEmail ( { token } ) ;
422- setMessage ( "Email verified. Sign in to continue." ) ;
423- setMode ( "login" ) ;
424- return ;
425- }
426- if ( mode === "reset" ) {
427- await configApi . resetPassword ( { token, password } ) ;
428- setMessage ( "Password reset. Sign in to continue." ) ;
429- setMode ( "login" ) ;
430- }
431- } catch ( err ) {
432- setLocalError ( err instanceof Error ? err . message : "Authentication failed" ) ;
433- } finally {
434- setSubmitting ( false ) ;
435- }
436- } ;
437-
438- return (
439- < PasswordAuthShell title = { mode === "register" ? "Create account" : mode === "forgot" ? "Forgot password" : mode === "verify" ? "Verify email" : mode === "reset" ? "Reset password" : "Sign in" } >
440- < form className = "flex w-full max-w-sm flex-col gap-3" onSubmit = { submit } >
441- { mode === "register" ? (
442- < input
443- className = "h-10 rounded-md border border-border bg-surface px-3 text-sm outline-none"
444- value = { displayName }
445- onChange = { ( event ) => setDisplayName ( event . target . value ) }
446- placeholder = "Display name"
447- />
448- ) : null }
449- { mode === "verify" || mode === "reset" ? (
450- < input
451- className = "h-10 rounded-md border border-border bg-surface px-3 text-sm outline-none"
452- value = { token }
453- onChange = { ( event ) => setToken ( event . target . value ) }
454- placeholder = { mode === "verify" ? "Verify email token" : "Reset token" }
455- />
456- ) : null }
457- { mode !== "verify" && mode !== "reset" ? (
458- < input
459- className = "h-10 rounded-md border border-border bg-surface px-3 text-sm outline-none"
460- value = { email }
461- onChange = { ( event ) => setEmail ( event . target . value ) }
462- placeholder = "Email"
463- type = "email"
464- />
465- ) : null }
466- { mode !== "forgot" && mode !== "verify" ? (
467- < input
468- className = "h-10 rounded-md border border-border bg-surface px-3 text-sm outline-none"
469- value = { password }
470- onChange = { ( event ) => setPassword ( event . target . value ) }
471- placeholder = "Password"
472- type = "password"
473- />
474- ) : null }
475- < button
476- className = "h-10 rounded-md bg-primary px-3 text-sm font-semibold text-white disabled:opacity-50"
477- disabled = { submitting }
478- type = "submit"
479- >
480- { mode === "register" ? "Create account" : mode === "forgot" ? "Send reset link" : mode === "verify" ? "Verify email" : mode === "reset" ? "Reset password" : "Sign in" }
481- </ button >
482- < div className = "flex flex-wrap gap-2 text-xs text-muted-light" >
483- < button type = "button" onClick = { ( ) => setMode ( "login" ) } > Sign in</ button >
484- < button type = "button" onClick = { ( ) => setMode ( "register" ) } > Create account</ button >
485- < button type = "button" onClick = { ( ) => setMode ( "forgot" ) } > Forgot password</ button >
486- < button type = "button" onClick = { ( ) => setMode ( "verify" ) } > Verify email</ button >
487- </ div >
488- { message ? < p className = "text-xs text-emerald-700" > { message } </ p > : null }
489- { localError || error ? < p className = "text-xs text-rose-700" > { localError || error } </ p > : null }
490- </ form >
491- </ PasswordAuthShell >
492- ) ;
493- }
494-
495- function PasswordAuthShell ( { children, title } : { children ?: ReactNode ; title : string } ) {
496- return (
497- < main className = "flex min-h-screen items-center justify-center bg-surface-subtle p-6 text-foreground" >
498- < section className = "flex w-full max-w-sm flex-col gap-4" >
499- < div >
500- < h1 className = "text-2xl font-semibold" > { title } </ h1 >
501- < p className = "mt-1 text-sm text-muted-light" > DataFoundry</ p >
502- </ div >
503- { children }
504- </ section >
505- </ main >
506- ) ;
507- }
508-
509386function csrfAuthHeaders ( ) : Record < string , string > {
510387 if ( typeof document === "undefined" ) return { } ;
511388 const token = document . cookie
0 commit comments