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 {
return new Response(JSON.stringify(data), {
status,
@ -10,10 +8,3 @@ export function jsonResponse(data: unknown, status = 200): Response {
export function errorResponse(message: string, status: number): Response {
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';
type Session = { user?: { id?: string; name?: string | null } };
export type Session = { user?: { id?: string; name?: string | null } };
export function isAdmin(userId: string | undefined): boolean {
return userId === import.meta.env.ADMIN_GITHUB_ID;
}
export type AuthResult =
| { status: 'admin'; session: Session }
| { status: 'unauthenticated' }
| { status: 'forbidden' }
| { status: 'error' };
export async function requireAdminSession(request: Request): Promise<
| { session: Session; error: null }
| { session: null; error: Response | null }
> {
export async function getAdminSession(request: Request): Promise<AuthResult> {
let session: Session | null;
try {
session = await getSession(request);
} catch {
return { session: null, error: new Response('Auth not configured', { status: 500 }) };
return { status: 'error' };
}
if (!session) {
return { session: null, error: null };
if (!session) return { status: 'unauthenticated' };
if (session.user?.id !== import.meta.env.ADMIN_GITHUB_ID) {
return { status: 'forbidden' };
}
if (!isAdmin(session.user?.id)) {
return { session: null, error: new Response('Forbidden', { status: 403 }) };
}
return { session, error: null };
return { status: 'admin', session };
}

View file

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

View file

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

View file

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