Files
linumiq_net-web_app/app/api/admin/tunnels/[id]/reassign/route.ts
T

86 lines
2.5 KiB
TypeScript

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 { isUuid } from '@/lib/admin/validators';
import { validateSubdomain } from '@/lib/validation';
import { isSubdomainReserved } from '@/lib/admin/reserved';
import { jsonNoStore } from '@/lib/admin/response';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } },
) {
const auth = await requireAdminApi();
if (!auth.ok) return auth.response;
const { id } = params;
if (!isUuid(id)) {
return jsonNoStore({ error: 'invalid tunnel id' }, { status: 400 });
}
let body: { subdomain?: unknown };
try {
body = (await req.json()) as { subdomain?: unknown };
} catch {
return jsonNoStore({ error: 'invalid json' }, { status: 400 });
}
// Same validation as the user-facing claim flow (format + hardcoded reserved).
const v = validateSubdomain(body.subdomain);
if (!v.ok) {
return jsonNoStore({ error: v.error }, { status: 400 });
}
const subdomain = v.value;
// Also reject anything reserved in the DB table.
if (await isSubdomainReserved(subdomain)) {
return jsonNoStore(
{ error: `'${subdomain}' is reserved` },
{ status: 400 },
);
}
const admin = getSupabaseAdmin();
// Reject if taken by a different tunnel (keyed by owner user_id).
const { data: existing } = await admin
.from('tunnels')
.select('user_id')
.eq('subdomain', subdomain)
.maybeSingle<{ user_id: string }>();
if (existing && existing.user_id !== id) {
return jsonNoStore({ error: 'subdomain taken' }, { status: 409 });
}
const { data, error } = await admin
.from('tunnels')
.update({ subdomain })
.eq('user_id', id)
.select('subdomain')
.maybeSingle<{ subdomain: string }>();
if (error) {
const code = (error as { code?: string }).code;
if (code === '23505') {
return jsonNoStore({ error: 'subdomain taken' }, { status: 409 });
}
console.error('admin tunnel.reassign failed', error);
return jsonNoStore({ error: 'internal error' }, { status: 500 });
}
if (!data) {
return jsonNoStore({ error: 'tunnel not found' }, { status: 404 });
}
await logAdminAction(auth.user, {
action: 'tunnel.reassign',
target_type: 'tunnel',
target_id: id,
details: { subdomain },
});
return jsonNoStore({ ok: true, subdomain });
}