// Session validation and family resolver for API routes // All data routes must use this to validate sessions and get familyId import { sql } from "@/db"; import { cookies } from "next/headers"; interface SessionData { userId: string; familyId: string | null; familyName: string | null; tier: string | null; } interface AuthResult { success: boolean; error?: string; status?: number; session?: SessionData; } /** * Validate session cookie and resolve family information * Use this at the start of every data API route */ export async function validateSession(): Promise { try { const cookieStore = await cookies(); const sessionToken = cookieStore.get("tia_session")?.value; if (!sessionToken) { return { success: false, error: "Not authenticated", status: 401 }; } // Verify session and get family info const sessions = await sql` SELECT s.user_id, s.expires, fm.family_id as family_id, f.name as family_name, f.tier FROM sessions s JOIN users u ON u.id::text = s.user_id::text LEFT JOIN family_members fm ON fm.user_id::text = u.id::text LEFT JOIN families f ON f.id = fm.family_id WHERE s.session_token = ${sessionToken} AND s.expires > NOW() LIMIT 1 `; if (!sessions || sessions.length === 0) { return { success: false, error: "Invalid or expired session", status: 401 }; } const session = sessions[0]; return { success: true, session: { userId: session.user_id, familyId: session.family_id, familyName: session.family_name, tier: session.tier, }, }; } catch (error) { console.error("Session validation error:", error); return { success: false, error: "Session validation failed", status: 500 }; } } /** * Require authenticated user with a family * Use for routes that require family data */ export async function requireFamily(): Promise { const auth = await validateSession(); if (!auth.success) { return auth; } if (!auth.session?.familyId) { return { success: false, error: "No family associated with session", status: 403 }; } return auth; } /** * Require authenticated user who owns a specific resource * Use to verify user has access to a resource (e.g., a specific child's data) * * @param resourceId - The ID of the resource to check * @param table - The table name to check ownership in * @param resourceLabel - Human-readable label for error messages */ export async function requireOwnership( resourceId: string, table: string, resourceLabel: string ): Promise { const auth = await requireFamily(); if (!auth.success) { return auth; } const familyId = auth.session!.familyId!; // Check if resource belongs to user's family const result = await sql.unsafe( `SELECT id FROM ${table} WHERE id = $1 AND family_id = $2 LIMIT 1`, [resourceId, familyId] ); if (!result || result.length === 0) { return { success: false, error: `${resourceLabel} not found or access denied`, status: 404, }; } return auth; } /** * Require a specific tier level * Use for premium features */ export async function requireTier(minTier: "free" | "pro"): Promise { const auth = await requireFamily(); if (!auth.success) { return auth; } const tier = auth.session?.tier || "free"; const tierLevels = { free: 0, pro: 1 }; const userLevel = tierLevels[tier as keyof typeof tierLevels] || 0; const requiredLevel = tierLevels[minTier]; if (userLevel < requiredLevel) { return { success: false, error: `Requires ${minTier} tier or higher`, status: 403, }; } return auth; }