tia/src/lib/auth.ts
2026-05-17 00:35:49 +05:30

148 lines
No EOL
3.7 KiB
TypeScript

// 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<AuthResult> {
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<AuthResult> {
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<AuthResult> {
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<AuthResult> {
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;
}