Skip to content

Commit 9d04b1f

Browse files
authored
Merge pull request #27 from wangjk9527/feat/production-password-auth
feat: Change login/register to independent route and optimize authentication interface
2 parents 743c0ab + 998ec41 commit 9d04b1f

8 files changed

Lines changed: 442 additions & 140 deletions

File tree

apps/web/src/app/data-tasks/data-task-identity.tsx

Lines changed: 16 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
useLayoutEffect,
99
useMemo,
1010
useState,
11-
type FormEvent,
1211
type ReactNode,
1312
} from "react";
13+
import { useRouter } from "next/navigation";
1414
import {
1515
clearConfigApiIdentity,
1616
configApi,
@@ -19,6 +19,7 @@ import {
1919
type ConfigApiIdentity,
2020
} from "../../lib/config-api/client";
2121
import type { DevIdentityUser } from "../../lib/config-api";
22+
import { AUTH_BUTTON_CLASS, PasswordAuthShell } from "../../components/auth/auth-flow";
2223

2324
const IDENTITY_STORAGE_KEY = "data-tasks:identity:v1";
2425
const DEV_SIGNED_OUT_STORAGE_KEY = "data-tasks:identity:signed-out:v1";
@@ -277,16 +278,13 @@ function DevIdentityProvider({ children }: { children: ReactNode }) {
277278

278279
function 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

297295
function 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-
509386
function csrfAuthHeaders(): Record<string, string> {
510387
if (typeof document === "undefined") return {};
511388
const token = document.cookie

apps/web/src/app/globals.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,27 @@ nextjs-portal {
195195
animation: account-menu-popover-in 140ms ease-out;
196196
}
197197

198+
@keyframes auth-card-in {
199+
from {
200+
opacity: 0;
201+
transform: translateY(8px) scale(0.99);
202+
}
203+
to {
204+
opacity: 1;
205+
transform: translateY(0) scale(1);
206+
}
207+
}
208+
209+
.auth-card-in {
210+
animation: auth-card-in 220ms ease-out both;
211+
}
212+
213+
@media (prefers-reduced-motion: reduce) {
214+
.auth-card-in {
215+
animation: none;
216+
}
217+
}
218+
198219
@keyframes collapsed-sidebar-preview-in {
199220
from {
200221
opacity: 0;

apps/web/src/app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ const firaCode = Fira_Code({
1717
});
1818

1919
export const metadata: Metadata = {
20-
title: "DataFoundry",
20+
title: {
21+
default: "DataFoundry",
22+
template: "%s · DataFoundry",
23+
},
2124
description: "Agent-driven data task workspace",
2225
};
2326

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { AuthFlow, PasswordAuthShell } from "../../components/auth/auth-flow";
6+
import { configApi, isPasswordAuthMode } from "../../lib/config-api/client";
7+
8+
export function LoginClient() {
9+
const router = useRouter();
10+
const [checking, setChecking] = useState(true);
11+
12+
useEffect(() => {
13+
if (!isPasswordAuthMode()) {
14+
router.replace("/data-tasks");
15+
return;
16+
}
17+
let cancelled = false;
18+
configApi
19+
.getMe()
20+
.then(() => {
21+
if (!cancelled) router.replace("/data-tasks");
22+
})
23+
.catch(() => {
24+
if (!cancelled) setChecking(false);
25+
});
26+
return () => {
27+
cancelled = true;
28+
};
29+
}, [router]);
30+
31+
if (checking) {
32+
return <PasswordAuthShell title="Loading account..." />;
33+
}
34+
35+
return <AuthFlow initialMode="login" onAuthenticated={() => router.replace("/data-tasks")} />;
36+
}

apps/web/src/app/login/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Metadata } from "next";
2+
import { LoginClient } from "./login-client";
3+
4+
export const metadata: Metadata = {
5+
title: "登录",
6+
};
7+
8+
export default function LoginPage() {
9+
return <LoginClient />;
10+
}

apps/web/src/app/register/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Metadata } from "next";
2+
import { RegisterClient } from "./register-client";
3+
4+
export const metadata: Metadata = {
5+
title: "注册",
6+
};
7+
8+
export default function RegisterPage() {
9+
return <RegisterClient />;
10+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { AuthFlow, PasswordAuthShell } from "../../components/auth/auth-flow";
6+
import { configApi, isPasswordAuthMode } from "../../lib/config-api/client";
7+
8+
export function RegisterClient() {
9+
const router = useRouter();
10+
const [checking, setChecking] = useState(true);
11+
12+
useEffect(() => {
13+
if (!isPasswordAuthMode()) {
14+
router.replace("/data-tasks");
15+
return;
16+
}
17+
let cancelled = false;
18+
configApi
19+
.getMe()
20+
.then(() => {
21+
if (!cancelled) router.replace("/data-tasks");
22+
})
23+
.catch(() => {
24+
if (!cancelled) setChecking(false);
25+
});
26+
return () => {
27+
cancelled = true;
28+
};
29+
}, [router]);
30+
31+
if (checking) {
32+
return <PasswordAuthShell title="Loading account..." />;
33+
}
34+
35+
return <AuthFlow initialMode="register" onAuthenticated={() => router.replace("/data-tasks")} />;
36+
}

0 commit comments

Comments
 (0)