diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 729e276..337f616 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -65,6 +65,15 @@ export default function LoginPage() { 去输入激活码

+

+ 家里另一位家长想加入?{" "} + + 用邀请码加入家庭 + +

diff --git a/src/app/register/actions.ts b/src/app/register/actions.ts new file mode 100644 index 0000000..09f9fbd --- /dev/null +++ b/src/app/register/actions.ts @@ -0,0 +1,68 @@ +"use server"; + +import crypto from "node:crypto"; +import bcrypt from "bcryptjs"; +import { and, eq, gt, isNull } from "drizzle-orm"; +import { redirect } from "next/navigation"; +import { z } from "zod"; +import { db } from "@/db"; +import { familyInvites, users } from "@/db/schema"; +import { createSession } from "@/lib/auth"; + +function hashCode(code: string) { + return crypto.createHash("sha256").update(code).digest("hex"); +} + +const registerSchema = z.object({ + displayName: z.string().trim().min(1, "请输入称呼").max(20, "称呼最多 20 字"), + phone: z.string().regex(/^1\d{10}$/, "请输入 11 位手机号"), + password: z.string().min(6, "密码至少 6 位"), + code: z.string().regex(/^\d{6}$/, "邀请码是 6 位数字"), +}); + +export type RegisterState = { error?: string } | null; + +// xuchao: 家长凭邀请码注册:校验码→查重手机号→事务(码标记已用+建家长)→自动登录跳家长中心 +export async function registerWithInvite( + _prev: RegisterState, + formData: FormData, +): Promise { + const parsed = registerSchema.safeParse({ + displayName: formData.get("displayName"), + phone: formData.get("phone"), + password: formData.get("password"), + code: formData.get("code"), + }); + if (!parsed.success) return { error: parsed.error.issues[0].message }; + + const { displayName, phone, password, code } = parsed.data; + + const [invite] = await db + .select({ id: familyInvites.id, familyId: familyInvites.familyId }) + .from(familyInvites) + .where( + and( + eq(familyInvites.codeHash, hashCode(code)), + isNull(familyInvites.usedAt), + gt(familyInvites.expiresAt, new Date()), + ), + ) + .limit(1); + if (!invite) return { error: "邀请码无效或已过期,请让家长重新生成" }; + + const [dup] = await db.select({ id: users.id }).from(users).where(eq(users.phone, phone)).limit(1); + if (dup) return { error: "该手机号已注册" }; + + const passwordHash = await bcrypt.hash(password, 10); + const newUserId = await db.transaction(async (tx) => { + await tx.update(familyInvites).set({ usedAt: new Date() }).where(eq(familyInvites.id, invite.id)); + const [created] = await tx + .insert(users) + .values({ familyId: invite.familyId, role: "parent", displayName, phone, passwordHash }) + .$returningId(); + return created.id; + }); + + await createSession(newUserId, "家长设备", 30); + redirect("/parent"); +} \ No newline at end of file diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..8da58ac --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useActionState } from "react"; +import Link from "next/link"; +import { UserPlus } from "lucide-react"; +import { BackgroundBlobs } from "@/components/background-blobs"; +import { registerWithInvite } from "./actions"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function RegisterPage() { + const [state, formAction, pending] = useActionState(registerWithInvite, null); + + return ( +
+ + + +
+ +
+

加入家庭

+

需要家里已有成员的邀请码

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {state?.error &&

{state.error}

} + +

+ 已有账号?{" "} + + 去登录 + +

+
+
+
+
+ ); +} \ No newline at end of file