Verified via psql that the user table has quoted camelCase columns from
Better Auth ("updatedAt", "createdAt", "emailVerified"). The original
route comment claimed app/api/settings/profile as precedent — that route
is ALSO broken with the same bug; only app/api/me/timezone got it right.
Aligning theme route with the timezone precedent.
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { requireAuth } from '@/lib/auth-utils';
|
|
import { postgresClient } from '@/lib/services/postgres-client';
|
|
|
|
// GET /api/me/theme -> { theme: 'light' | 'dark' | 'system', source: 'user' | 'default' }
|
|
// PUT /api/me/theme -> body { theme: 'light' | 'dark' | 'system' } -> { theme: string }
|
|
//
|
|
// THEME-02. Authentication: requireAuth(). The PUT handler updates ONLY the
|
|
// calling user's row — there is no `userId` query param or body field. The
|
|
// write target is always `session.user.id`.
|
|
//
|
|
// Validation: the input theme must be one of: 'light', 'dark', 'system'.
|
|
// Anything else is rejected with 400 before touching the database.
|
|
//
|
|
// NOTE: The UPDATE statement uses `"updatedAt"` (quoted camelCase) — that is
|
|
// the actual column name in the Better Auth `"user"` table (verified via psql).
|
|
// Matches the working precedent in app/api/me/timezone/route.ts.
|
|
|
|
const ALLOWED_THEMES = new Set(['light', 'dark', 'system'] as const);
|
|
|
|
function isValidTheme(t: unknown): t is 'light' | 'dark' | 'system' {
|
|
return typeof t === 'string' && ALLOWED_THEMES.has(t as 'light' | 'dark' | 'system');
|
|
}
|
|
|
|
export async function GET(): Promise<NextResponse> {
|
|
const { session, error } = await requireAuth();
|
|
if (error) return error;
|
|
|
|
try {
|
|
const result = await postgresClient.query<{ theme: string | null }>(
|
|
'SELECT theme FROM "user" WHERE id = $1',
|
|
[session!.user.id],
|
|
);
|
|
const stored = result.rows[0]?.theme;
|
|
// source is 'user' when a non-default value is explicitly stored;
|
|
// 'default' when value equals fallback ('system') or no row found.
|
|
const theme = stored ?? 'system';
|
|
const source: 'user' | 'default' = stored && stored !== 'system' ? 'user' : 'default';
|
|
return NextResponse.json({ theme, source });
|
|
} catch (e) {
|
|
console.error('GET /api/me/theme failed:', e);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to read theme', message: e instanceof Error ? e.message : 'unknown' },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function PUT(request: NextRequest): Promise<NextResponse> {
|
|
const { session, error } = await requireAuth();
|
|
if (error) return error;
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid JSON', message: 'Request body must be JSON' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const candidate =
|
|
body && typeof body === 'object' && 'theme' in body
|
|
? (body as { theme: unknown }).theme
|
|
: undefined;
|
|
|
|
if (!isValidTheme(candidate)) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid theme', message: 'theme must be one of: light, dark, system' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
try {
|
|
// Authoritative write target: session.user.id. NO userId from body.
|
|
// "updatedAt" is quoted camelCase — matches Better Auth schema (verified).
|
|
const result = await postgresClient.query<{ theme: string }>(
|
|
'UPDATE "user" SET theme = $1, "updatedAt" = NOW() WHERE id = $2 RETURNING theme',
|
|
[candidate, session!.user.id],
|
|
);
|
|
if (result.rowCount === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'User not found', message: 'No user row matched the session' },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
return NextResponse.json({ theme: result.rows[0].theme });
|
|
} catch (e) {
|
|
console.error('PUT /api/me/theme failed:', e);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to update theme', message: e instanceof Error ? e.message : 'unknown' },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|