From da5de3fa8c290bf69f58c5497c2dad4727954047 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Thu, 8 Oct 2026 18:46:04 +0700 Subject: [PATCH] feat(auth) : add optional email verification and profile setup --- app/auth/register/page.tsx | 4 +- components/auth/account-form.tsx | 10 ++-- components/auth/account-page.test.tsx | 62 +++++++++++++++++++++++ components/auth/account-page.tsx | 19 +++++-- components/auth/profile-form.tsx | 15 ++++-- components/auth/registration-otp-form.tsx | 2 +- lib/auth/server.test.ts | 12 ++++- lib/auth/server.ts | 3 +- 8 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 components/auth/account-page.test.tsx diff --git a/app/auth/register/page.tsx b/app/auth/register/page.tsx index 2ec4155..740f5d8 100644 --- a/app/auth/register/page.tsx +++ b/app/auth/register/page.tsx @@ -5,6 +5,6 @@ export const instant = false; export default async function RegisterPage({ searchParams }: PageProps<"/auth/register">) { await connection(); - const { next } = await searchParams; - return ; + const { next, step } = await searchParams; + return ; } diff --git a/components/auth/account-form.tsx b/components/auth/account-form.tsx index 046e494..b7aeadf 100644 --- a/components/auth/account-form.tsx +++ b/components/auth/account-form.tsx @@ -168,7 +168,7 @@ export function AccountForm({ email: pendingEmail, otp: String(form.get("otp") ?? "").trim(), }); if (result.error) throw new Error("รหัสไม่ถูกต้องหรือหมดอายุ กรุณาลองอีกครั้ง"); - router.replace(`/profile?setup=1&next=${encodeURIComponent(nextPath)}`); + router.replace(`/auth/register?step=profile&next=${encodeURIComponent(nextPath)}`); router.refresh(); return; } @@ -199,7 +199,7 @@ export function AccountForm({ {register ? pendingEmail ? "ยืนยันอีเมล" : "สร้างบัญชี Buzz Guide" : "เข้าสู่ระบบ"} {nextPath.startsWith("/commission") && เข้าสู่ระบบเพื่อบันทึกคำขอ รับผล และพูดคุยกับทีมงาน} - {register && ขั้นตอน {pendingEmail ? "2 จาก 2 - ยืนยันอีเมล" : "1 จาก 2 - ข้อมูลบัญชี"}} + {register && ขั้นตอน {pendingEmail ? "2 จาก 3 - ยืนยันอีเมล" : "1 จาก 3 - ข้อมูลบัญชี"}} {verified && ยืนยันอีเมลแล้ว กรุณาเข้าสู่ระบบ} @@ -268,8 +268,12 @@ export function AccountForm({ } {error && {error}} + {register && pendingEmail && } {register ? - {nextPath && ข้ามและเริ่มใช้งาน} + {nextPath && } ); diff --git a/components/auth/registration-otp-form.tsx b/components/auth/registration-otp-form.tsx index 01c1219..85ebfbe 100644 --- a/components/auth/registration-otp-form.tsx +++ b/components/auth/registration-otp-form.tsx @@ -33,7 +33,7 @@ export function RegistrationOtpForm({ email, nextPath, setup }: { try { const result = await authClient.emailOtp.verifyEmail({ email: address, otp }); if (result.error) throw new Error("รหัสไม่ถูกต้องหรือหมดอายุ กรุณาลองอีกครั้ง"); - const returnPath = setup ? `/profile?setup=1&next=${encodeURIComponent(nextPath)}` : nextPath; + const returnPath = setup ? `/auth/register?step=profile&next=${encodeURIComponent(nextPath)}` : nextPath; router.replace(returnPath); router.refresh(); } catch (cause) { diff --git a/lib/auth/server.test.ts b/lib/auth/server.test.ts index 95ee3d7..90715f4 100644 --- a/lib/auth/server.test.ts +++ b/lib/auth/server.test.ts @@ -74,13 +74,21 @@ describe("actual administrator session boundary", () => { expect(await getCustomerSession()).toMatchObject({ user: { id: "user-1" } }); expect(await getAdminSession()).toMatchObject({ user: { id: "user-1" } }); }); - it("requires verification and sends auth links from the configured sender", async () => { + it("allows an unverified customer session but rejects administrator access", async () => { + mocks.session.mockResolvedValue({ user: { id: "user-1", email: "a@test.invalid", + emailVerified: false, role: "admin" }, session: { id: "guide-session" } }); + const { getAdminSession, getCustomerSession, requireAdmin } = await import("./server"); + expect(await getCustomerSession()).toMatchObject({ user: { id: "user-1", emailVerified: false } }); + expect(await getAdminSession()).toBeNull(); + await expect(requireAdmin()).rejects.toMatchObject({ status: 401 }); + }); + it("allows unverified sign-in and sends auth links from the configured sender", async () => { testEnv.RESEND_API_KEY = "test-key"; const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(null, { status: 200 })); try { (await import("./server")).getAuth(); const options = mocks.auth.mock.calls.at(-1)![0]; - expect(options.emailAndPassword.requireEmailVerification).toBe(true); + expect(options.emailAndPassword).toMatchObject({ requireEmailVerification: false, autoSignIn: true }); expect(options.emailAndPassword.resetPasswordTokenExpiresIn).toBe(3600); expect(options.emailVerification).toMatchObject({ sendOnSignUp: false, autoSignInAfterVerification: true, expiresIn: 3600, diff --git a/lib/auth/server.ts b/lib/auth/server.ts index 0abf91d..88ed92a 100644 --- a/lib/auth/server.ts +++ b/lib/auth/server.ts @@ -53,7 +53,8 @@ function createAuth() { secret: required("BETTER_AUTH_SECRET"), emailAndPassword: { enabled: true, - requireEmailVerification: true, + requireEmailVerification: false, + autoSignIn: true, minPasswordLength: 6, maxPasswordLength: 128, resetPasswordTokenExpiresIn: 3600,