Files
linumiq_net-web_app/app/api/admin/reserved/route.ts
T

100 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type NextRequest } from 'next/server';
import { requireAdminApi } from '@/lib/auth/admin-guard';
import { getSupabaseAdmin } from '@/lib/supabase/admin';
import { logAdminAction } from '@/lib/auth/audit';
import { RESERVED_SUBDOMAINS } from '@/lib/validation';
import { jsonNoStore } from '@/lib/admin/response';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
const NAME_RE = /^[a-z0-9-]{1,63}$/;
export async function GET() {
const auth = await requireAdminApi();
if (!auth.ok) return auth.response;
const admin = getSupabaseAdmin();
const { data, error } = await admin
.from('reserved_subdomains')
.select('name, created_at')
.order('name', { ascending: true });
if (error) {
console.error('admin reserved list failed', error);
return jsonNoStore({ error: 'internal error' }, { status: 500 });
}
return jsonNoStore({
reserved: data ?? [],
hardcoded: Array.from(RESERVED_SUBDOMAINS).sort(),
});
}
export async function POST(req: NextRequest) {
const auth = await requireAdminApi();
if (!auth.ok) return auth.response;
let body: { name?: unknown };
try {
body = (await req.json()) as { name?: unknown };
} catch {
return jsonNoStore({ error: 'invalid json' }, { status: 400 });
}
if (typeof body.name !== 'string') {
return jsonNoStore({ error: 'name must be a string' }, { status: 400 });
}
const name = body.name.trim().toLowerCase();
if (!NAME_RE.test(name)) {
return jsonNoStore(
{ error: 'name must be 163 chars, lowercase az, 09, hyphen' },
{ status: 400 },
);
}
const admin = getSupabaseAdmin();
const { error } = await admin
.from('reserved_subdomains')
.upsert({ name }, { onConflict: 'name' });
if (error) {
console.error('admin reserved add failed', error);
return jsonNoStore({ error: 'internal error' }, { status: 500 });
}
await logAdminAction(auth.user, {
action: 'reserved.add',
target_type: 'reserved_subdomain',
target_id: name,
});
return jsonNoStore({ ok: true, name });
}
export async function DELETE(req: NextRequest) {
const auth = await requireAdminApi();
if (!auth.ok) return auth.response;
const url = new URL(req.url);
const name = (url.searchParams.get('name') ?? '').trim().toLowerCase();
if (!name) {
return jsonNoStore({ error: 'name is required' }, { status: 400 });
}
const admin = getSupabaseAdmin();
const { error } = await admin
.from('reserved_subdomains')
.delete()
.eq('name', name);
if (error) {
console.error('admin reserved remove failed', error);
return jsonNoStore({ error: 'internal error' }, { status: 500 });
}
await logAdminAction(auth.user, {
action: 'reserved.remove',
target_type: 'reserved_subdomain',
target_id: name,
});
return jsonNoStore({ ok: true });
}