From 9555e0a884fb68a38f41e0fdb08d7e254f39b924 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sat, 3 Oct 2026 01:36:56 +0700 Subject: [PATCH] feat update --- .../page.tsx | 2 +- app/profile/page.tsx | 2 +- components/auth/account-form.tsx | 47 ++++++++++++++++--- components/auth/email-settings.tsx | 9 ++-- components/auth/profile-form.tsx | 10 ---- components/auth/registration-otp-form.tsx | 2 +- lib/auth/server.test.ts | 4 +- lib/auth/server.ts | 1 + lib/commission/discord.ts | 2 +- lib/commission/push.ts | 2 +- next.config.ts | 4 ++ 11 files changed, 59 insertions(+), 26 deletions(-) rename app/{reset-password => password-reset}/page.tsx (94%) diff --git a/app/reset-password/page.tsx b/app/password-reset/page.tsx similarity index 94% rename from app/reset-password/page.tsx rename to app/password-reset/page.tsx index 38411c7..7f9e3d0 100644 --- a/app/reset-password/page.tsx +++ b/app/password-reset/page.tsx @@ -4,7 +4,7 @@ import { EmailActionForm } from "@/components/auth/email-action-form"; export const instant = false; -export default async function ResetPasswordPage({ searchParams }: PageProps<"/reset-password">) { +export default async function ResetPasswordPage({ searchParams }: PageProps<"/password-reset">) { await connection(); const { token, error } = await searchParams; return
diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 67f3da7..710c250 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -29,7 +29,7 @@ export default async function ProfilePage({ searchParams }: PageProps<"/profile"

โปรไฟล์ของฉัน

{setup === "1" &&

- {upload === "failed" ? "สร้างบัญชีแล้ว แต่บันทึกรูปโปรไฟล์ไม่สำเร็จ กรุณาลองอีกครั้งหรือข้ามไปก่อน" : + {upload === "failed" ? "สร้างบัญชีแล้ว แต่บันทึกรูปโปรไฟล์ไม่สำเร็จ กรุณาลองอีกครั้ง" : "เพิ่มรูปโปรไฟล์หรือแก้ชื่อที่แสดงก่อนเริ่มใช้งาน"}

}
diff --git a/components/auth/account-form.tsx b/components/auth/account-form.tsx index 16680d4..0a502fb 100644 --- a/components/auth/account-form.tsx +++ b/components/auth/account-form.tsx @@ -4,11 +4,13 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState, type FormEvent } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { authClient } from "@/lib/auth/client"; +import { ProfileImageInput } from "./profile-image-input"; declare global { interface Window { @@ -42,12 +44,18 @@ export function AccountForm({ const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [pendingEmail, setPendingEmail] = useState(""); + const [pendingName, setPendingName] = useState(""); const [notice, setNotice] = useState(""); const [token, setToken] = useState(""); + const [profileImage, setProfileImage] = useState(null); + const [profilePreview, setProfilePreview] = useState(null); const formRef = useRef(null); const container = useRef(null); const widget = useRef(null); const captcha = process.env.NODE_ENV !== "development"; + useEffect(() => () => { + if (profilePreview) URL.revokeObjectURL(profilePreview); + }, [profilePreview]); useEffect(() => { if (!captcha) return; const script = document.createElement("script"); @@ -94,14 +102,16 @@ export function AccountForm({ try { const fields = new FormData(form); const email = String(fields.get("email") ?? "").trim(); + const name = String(fields.get("name") ?? "").trim(); const result = await authClient.signUp.email({ - name: String(fields.get("name") ?? "").trim(), + name, email, password: String(fields.get("password") ?? ""), fetchOptions: { headers: captcha ? { "x-captcha-response": token } : undefined }, }); if (result.error) throw new Error(result.error.message ?? "ส่งรหัสไม่สำเร็จ กรุณาลองอีกครั้ง"); setPendingEmail(email); + setPendingName(name); setNotice("ส่งรหัส 6 หลักแล้ว กรุณาตรวจสอบอีเมลและกรอกรหัสภายใน 5 นาที"); } catch (cause) { setError(cause instanceof Error ? cause.message : "ลองอีกครั้ง"); @@ -125,8 +135,18 @@ export function AccountForm({ email: pendingEmail, otp: String(form.get("otp") ?? "").trim(), }); if (result.error) throw new Error("รหัสไม่ถูกต้องหรือหมดอายุ กรุณาลองอีกครั้ง"); - const setupPath = `/profile?setup=1&next=${encodeURIComponent(nextPath)}`; - router.push(`/login?verified=1&next=${encodeURIComponent(setupPath)}`); + if (profileImage) { + const profile = new FormData(); + profile.set("name", pendingName); + profile.set("image", profileImage); + const response = await fetch("/api/profile", { method: "POST", body: profile }); + if (!response.ok) { + router.replace(`/profile?setup=1&upload=failed&next=${encodeURIComponent(nextPath)}`); + router.refresh(); + return; + } + } + router.replace(nextPath); router.refresh(); return; } @@ -180,6 +200,21 @@ export function AccountForm({ /> )} + {register && + รูปโปรไฟล์ (ไม่บังคับ) +
+ + {profilePreview && } + ? + + { + setProfileImage(cropped); + setProfilePreview(URL.createObjectURL(cropped)); + setError(""); + }} onError={setError} /> +
+
} อีเมล
@@ -217,13 +252,13 @@ export function AccountForm({ placeholder="กดส่ง OTP ก่อน" disabled={busy || !pendingEmail} /> } {captcha && !pendingEmail &&
} - {(!register || pendingEmail) && } + {register ? - {nextPath && ( - - )} ); diff --git a/components/auth/registration-otp-form.tsx b/components/auth/registration-otp-form.tsx index a0bbd5f..be54956 100644 --- a/components/auth/registration-otp-form.tsx +++ b/components/auth/registration-otp-form.tsx @@ -32,7 +32,7 @@ export function RegistrationOtpForm({ email, nextPath, setup }: { 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; - router.push(`/login?verified=1&next=${encodeURIComponent(returnPath)}`); + router.replace(returnPath); router.refresh(); } catch (cause) { setError(cause instanceof Error ? cause.message : "ยืนยันอีเมลไม่สำเร็จ"); diff --git a/lib/auth/server.test.ts b/lib/auth/server.test.ts index b3014ea..17d9721 100644 --- a/lib/auth/server.test.ts +++ b/lib/auth/server.test.ts @@ -63,7 +63,9 @@ describe("actual administrator session boundary", () => { (await import("./server")).getAuth(); const options = mocks.auth.mock.calls.at(-1)![0]; expect(options.emailAndPassword.requireEmailVerification).toBe(true); - expect(options.emailVerification).toMatchObject({ sendOnSignUp: false }); + expect(options.emailVerification).toMatchObject({ + sendOnSignUp: false, autoSignInAfterVerification: true, + }); expect(options.user.changeEmail.enabled).toBe(true); const otp = options.plugins.find((plugin: { id?: string }) => plugin.id === "email-otp"); expect(otp.options).toMatchObject({ sendVerificationOnSignUp: true, storeOTP: "hashed" }); diff --git a/lib/auth/server.ts b/lib/auth/server.ts index 80bc58e..a2db7a7 100644 --- a/lib/auth/server.ts +++ b/lib/auth/server.ts @@ -72,6 +72,7 @@ function createAuth() { }, emailVerification: { sendOnSignUp: false, + autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { await sendAuthEmail(user.email, "ยืนยันอีเมล Buzz Guide", `เปิดลิงก์นี้เพื่อยืนยันอีเมล: ${url}\n\nลิงก์นี้หมดอายุใน 1 ชั่วโมง`); }, diff --git a/lib/commission/discord.ts b/lib/commission/discord.ts index 526f10a..d6e59b6 100644 --- a/lib/commission/discord.ts +++ b/lib/commission/discord.ts @@ -15,7 +15,7 @@ export async function notifyPaidTicketDiscord(ticketId: string, amountBaht: numb embeds: [{ title: "มี Ticket คอมมิชชันใหม่ที่ชำระเงินแล้ว", url: new URL(`/admin/commission/${ticketId}`, process.env.BETTER_AUTH_URL ?? siteUrl).toString(), - description: `Ticket #${ticketId.slice(0, 8)} · ฿${amountBaht}`, + description: `Ticket #${ticketId.slice(0, 8)} - ฿${amountBaht}`, color: 0x57f287, timestamp: new Date().toISOString(), }], diff --git a/lib/commission/push.ts b/lib/commission/push.ts index a76fb91..ca46ec0 100644 --- a/lib/commission/push.ts +++ b/lib/commission/push.ts @@ -39,7 +39,7 @@ export async function sendCommissionMessagePush(message: { for (let offset = 0; offset < subscriptions.length; offset += 10) { await Promise.all(subscriptions.slice(offset, offset + 10).map(async ({ subscription }) => { const payload = JSON.stringify({ - id: message.id, title: `${message.authorName} · ${message.ticketTitle}`, + id: message.id, title: `${message.authorName} - ${message.ticketTitle}`, body: message.text?.slice(0, 300) || "ส่งรูปภาพ", url: `/${subscription.scope === "admin" ? "admin/commission" : "commission/tickets"}/${message.ticketId}`, }); diff --git a/next.config.ts b/next.config.ts index 0b0bc95..a26d9b8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,6 +7,10 @@ const nextConfig: NextConfig = { async headers() { return [{ source: "/:path*", headers: securityHeaders(process.env.NODE_ENV === "production", "https://cdn.jsdelivr.net") }]; }, + async rewrites() { + // The reset-password segment resolves to the dynamic guide route in this Next version. + return { beforeFiles: [{ source: "/reset-password", destination: "/password-reset" }] }; + }, cacheComponents: true, cacheHandlers: { remote: resolve(process.cwd(), "cache-handlers/redis-handler.cjs"),