Google OAuth cannot provide phone numbers (no scope returns them reliably), so we collect it ourselves. Optional, stored unverified. - Migration 0011: users.phone text column (+ debug-migration hot-apply step) - schema/auth.ts: add phone field - onboarding: optional phone input on step 1; saved to users.phone via the onboarding API (normalised: leading + then digits, 8-15 digit validation) - profile page: editable Phone field; loaded from + saved to /api/auth/profile - /api/auth/profile: GET returns phone; POST accepts & normalises it (empty string clears, undefined leaves untouched) Capture point covers both Google and email/password signups since both land on onboarding. Verification (OTP) and marketing-consent flag intentionally deferred per product decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
No EOL
3.8 KiB
TypeScript
123 lines
No EOL
3.8 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { sql } from "@/db";
|
|
import { cookies } from "next/headers";
|
|
import { toProxyUrl } from "@/lib/r2-proxy";
|
|
|
|
// GET current user profile from session
|
|
export async function GET() {
|
|
try {
|
|
const cookieStore = await cookies();
|
|
const sessionToken = cookieStore.get("tia_session")?.value;
|
|
|
|
if (!sessionToken) {
|
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
}
|
|
|
|
// Get session and user
|
|
const sessions = await sql`
|
|
SELECT s.user_id, s.expires, u.id, u.email, u.name, u.image, u.phone, u.created_at
|
|
FROM sessions s
|
|
JOIN users u ON u.id = s.user_id
|
|
WHERE s.session_token = ${sessionToken}
|
|
AND s.expires > NOW()
|
|
`;
|
|
|
|
const session = sessions?.[0];
|
|
|
|
if (!session) {
|
|
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
|
|
}
|
|
|
|
// Get family info
|
|
const members = await sql`
|
|
SELECT fm.family_id, f.name as family_name
|
|
FROM family_members fm
|
|
JOIN families f ON f.id = fm.family_id
|
|
WHERE fm.user_id = ${session.user_id}
|
|
`;
|
|
|
|
return NextResponse.json({
|
|
user: {
|
|
id: session.id,
|
|
email: session.email,
|
|
name: session.name || "Parent",
|
|
phone: session.phone || null,
|
|
avatarUrl: toProxyUrl(session.image) || null,
|
|
familyId: members?.[0]?.family_id,
|
|
familyName: members?.[0]?.family_name,
|
|
memberSince: session.created_at,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("Profile fetch error:", error);
|
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// UPDATE user profile (name only)
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const body = await request.json();
|
|
const { name, phone } = body as { name?: string; phone?: string };
|
|
|
|
if (!name) {
|
|
return NextResponse.json({ error: "Name required" }, { status: 400 });
|
|
}
|
|
|
|
// Phone is optional. Normalise: keep a leading + then digits only.
|
|
// Empty string clears it. Light validation — 8-15 digits if provided.
|
|
let normalizedPhone: string | null | undefined; // undefined = don't touch
|
|
if (phone !== undefined) {
|
|
const trimmed = (phone || "").trim();
|
|
if (trimmed === "") {
|
|
normalizedPhone = null;
|
|
} else {
|
|
const cleaned = trimmed.replace(/[^\d+]/g, "").replace(/(?!^)\+/g, "");
|
|
const digits = cleaned.replace(/\D/g, "");
|
|
if (digits.length < 8 || digits.length > 15) {
|
|
return NextResponse.json({ error: "Enter a valid phone number" }, { status: 400 });
|
|
}
|
|
normalizedPhone = cleaned;
|
|
}
|
|
}
|
|
|
|
const cookieStore = await cookies();
|
|
const sessionToken = cookieStore.get("tia_session")?.value;
|
|
|
|
if (!sessionToken) {
|
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
}
|
|
|
|
// Get user from session
|
|
const sessions = await sql`
|
|
SELECT s.user_id
|
|
FROM sessions s
|
|
WHERE s.session_token = ${sessionToken}
|
|
AND s.expires > NOW()
|
|
`;
|
|
|
|
const session = sessions?.[0];
|
|
|
|
if (!session) {
|
|
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
|
|
}
|
|
|
|
// Update user name (+ phone only when the field was sent)
|
|
if (normalizedPhone !== undefined) {
|
|
await sql`
|
|
UPDATE users SET name = ${name}, phone = ${normalizedPhone}, updated_at = NOW()
|
|
WHERE id = ${session.user_id}
|
|
`;
|
|
} else {
|
|
await sql`
|
|
UPDATE users SET name = ${name}, updated_at = NOW()
|
|
WHERE id = ${session.user_id}
|
|
`;
|
|
}
|
|
|
|
return NextResponse.json({ success: true, name, phone: normalizedPhone ?? undefined });
|
|
} catch (error) {
|
|
console.error("Profile update error:", error);
|
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
}
|
|
} |