tia/src/app/api/auth/profile/route.ts
Mannu 38bb5af01c feat: collect optional user phone number (onboarding + profile)
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>
2026-05-31 21:25:00 +05:30

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 });
}
}