api: API key authentication with rotation and grace period

- Add ApiKey Prisma model (keyHash, expiresAt, revokedAt)
- Protect /api/v1/* routes with Authorization: Bearer header check in hooks
- create rotateApiKey() utility: normal (48h grace) and emergency (immediate)
- Add Super-only /admin/api-keys page for key rotation with one-time reveal
- Fix roleId check by using parent layout's DB-fetched adminUser
- Add API Keys link to admin sidebar
This commit is contained in:
NeilClack
2026-07-08 13:18:19 -05:00
parent 6266be0edf
commit 46b2cdf53d
14 changed files with 1658 additions and 7 deletions
+22 -2
View File
@@ -1,14 +1,34 @@
import { auth } from '$lib/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';
import { validateApiKey } from '$lib/server/api-key';
export async function handle({ event, resolve }) {
// Fetch current session from Better Auth
if (event.url.pathname.startsWith('/api/v1/')) {
const authHeader = event.request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response(JSON.stringify({ error: 'API key required' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
const key = authHeader.slice(7);
const record = await validateApiKey(key);
if (!record) {
return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
return resolve(event);
}
const session = await auth.api.getSession({
headers: event.request.headers
});
// Make session and user available on server
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;