refactor: cleans up authentication duplication between page and api routes

This commit is contained in:
Lewis Wynne 2026-03-27 18:26:40 +00:00
parent 384ca71f89
commit 5cc122bf39
5 changed files with 33 additions and 54 deletions

View file

@ -1,5 +1,3 @@
import { isAdmin } from './auth';
export function jsonResponse(data: unknown, status = 200): Response { export function jsonResponse(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), { return new Response(JSON.stringify(data), {
status, status,
@ -10,10 +8,3 @@ export function jsonResponse(data: unknown, status = 200): Response {
export function errorResponse(message: string, status: number): Response { export function errorResponse(message: string, status: number): Response {
return jsonResponse({ error: message }, status); return jsonResponse({ error: message }, status);
} }
export function requireAdmin(session: { user?: { id?: string } } | null): Response | null {
if (!session?.user?.id || !isAdmin(session.user.id)) {
return errorResponse('Unauthorized', 403);
}
return null;
}

View file

@ -1,29 +1,26 @@
import { getSession } from 'auth-astro/server'; import { getSession } from 'auth-astro/server';
type Session = { user?: { id?: string; name?: string | null } }; export type Session = { user?: { id?: string; name?: string | null } };
export function isAdmin(userId: string | undefined): boolean { export type AuthResult =
return userId === import.meta.env.ADMIN_GITHUB_ID; | { status: 'admin'; session: Session }
} | { status: 'unauthenticated' }
| { status: 'forbidden' }
| { status: 'error' };
export async function requireAdminSession(request: Request): Promise< export async function getAdminSession(request: Request): Promise<AuthResult> {
| { session: Session; error: null }
| { session: null; error: Response | null }
> {
let session: Session | null; let session: Session | null;
try { try {
session = await getSession(request); session = await getSession(request);
} catch { } catch {
return { session: null, error: new Response('Auth not configured', { status: 500 }) }; return { status: 'error' };
} }
if (!session) { if (!session) return { status: 'unauthenticated' };
return { session: null, error: null };
if (session.user?.id !== import.meta.env.ADMIN_GITHUB_ID) {
return { status: 'forbidden' };
} }
if (!isAdmin(session.user?.id)) { return { status: 'admin', session };
return { session: null, error: new Response('Forbidden', { status: 403 }) };
}
return { session, error: null };
} }

View file

@ -2,13 +2,15 @@
export const prerender = false; export const prerender = false;
import { getPendingEntries, type GuestbookEntry } from '../lib/db'; import { getPendingEntries, type GuestbookEntry } from '../lib/db';
import { requireAdminSession } from '../lib/auth'; import { getAdminSession } from '../lib/auth';
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import { formatDate } from '../lib/format'; import { formatDate } from '../lib/format';
const { session, error } = await requireAdminSession(Astro.request); const auth = await getAdminSession(Astro.request);
if (error) return error; if (auth.status === 'error') return new Response('Auth not configured', { status: 500 });
if (!session) return Astro.redirect('/api/auth/signin'); if (auth.status === 'unauthenticated') return Astro.redirect('/api/auth/signin');
if (auth.status !== 'admin') return new Response('Forbidden', { status: 403 });
const { session } = auth;
let entries: GuestbookEntry[] = []; let entries: GuestbookEntry[] = [];
try { try {

View file

@ -1,23 +1,18 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { getSession } from 'auth-astro/server'; import { jsonResponse, errorResponse } from '../../lib/api';
import { jsonResponse, errorResponse, requireAdmin } from '../../lib/api'; import { getAdminSession } from '../../lib/auth';
export const prerender = false; export const prerender = false;
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
const session = await getSession(request); const auth = await getAdminSession(request);
const authError = requireAdmin(session); if (auth.status !== 'admin') return errorResponse('Unauthorized', 403);
if (authError) return authError;
const hookUrl = import.meta.env.VERCEL_DEPLOY_HOOK; const hookUrl = import.meta.env.VERCEL_DEPLOY_HOOK;
if (!hookUrl) { if (!hookUrl) return errorResponse('Deploy hook not configured', 500);
return errorResponse('Deploy hook not configured', 500);
}
const res = await fetch(hookUrl, { method: 'POST' }); const res = await fetch(hookUrl, { method: 'POST' });
if (!res.ok) { if (!res.ok) return errorResponse('Failed to trigger deploy', 502);
return errorResponse('Failed to trigger deploy', 502);
}
return jsonResponse({ success: true }); return jsonResponse({ success: true });
}; };

View file

@ -1,33 +1,27 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { getSession } from 'auth-astro/server';
import { approveEntry, deleteEntry } from '../../../lib/db'; import { approveEntry, deleteEntry } from '../../../lib/db';
import { jsonResponse, errorResponse, requireAdmin } from '../../../lib/api'; import { jsonResponse, errorResponse } from '../../../lib/api';
import { getAdminSession } from '../../../lib/auth';
export const prerender = false; export const prerender = false;
export const PATCH: APIRoute = async ({ params, request }) => { export const PATCH: APIRoute = async ({ params, request }) => {
const session = await getSession(request); const auth = await getAdminSession(request);
const authError = requireAdmin(session); if (auth.status !== 'admin') return errorResponse('Unauthorized', 403);
if (authError) return authError;
const id = parseInt(params.id!, 10); const id = parseInt(params.id!, 10);
if (isNaN(id)) { if (isNaN(id)) return errorResponse('Invalid ID', 400);
return errorResponse('Invalid ID', 400);
}
await approveEntry(id); await approveEntry(id);
return jsonResponse({ success: true }); return jsonResponse({ success: true });
}; };
export const DELETE: APIRoute = async ({ params, request }) => { export const DELETE: APIRoute = async ({ params, request }) => {
const session = await getSession(request); const auth = await getAdminSession(request);
const authError = requireAdmin(session); if (auth.status !== 'admin') return errorResponse('Unauthorized', 403);
if (authError) return authError;
const id = parseInt(params.id!, 10); const id = parseInt(params.id!, 10);
if (isNaN(id)) { if (isNaN(id)) return errorResponse('Invalid ID', 400);
return errorResponse('Invalid ID', 400);
}
await deleteEntry(id); await deleteEntry(id);
return jsonResponse({ success: true }); return jsonResponse({ success: true });