45 lines
1.6 KiB
TypeScript
45 lines
1.6 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { createSupabaseServerClient } from '@/lib/supabase/server';
|
|
import { getSupabaseAdmin } from '@/lib/supabase/admin';
|
|
import { jsonNoStore } from '@/lib/admin/response';
|
|
import { generateRecoveryCodes, hashRecoveryCode } from '@/lib/auth/recovery';
|
|
|
|
export const runtime = 'nodejs';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
/**
|
|
* Generate a fresh set of recovery codes for the current user. Requires an
|
|
* aal2 session (the user just verified a factor). Any previously-issued codes
|
|
* are replaced. The plaintext codes are returned once and never persisted.
|
|
*/
|
|
export async function POST(): Promise<NextResponse> {
|
|
const supabase = createSupabaseServerClient();
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser();
|
|
if (!user) return jsonNoStore({ error: 'unauthorized' }, { status: 401 });
|
|
|
|
const { data: aal } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
|
|
if (aal?.currentLevel !== 'aal2') {
|
|
return jsonNoStore({ error: 'aal2_required' }, { status: 403 });
|
|
}
|
|
|
|
const codes = generateRecoveryCodes(10);
|
|
const admin = getSupabaseAdmin();
|
|
|
|
const del = await admin
|
|
.from('mfa_recovery_codes')
|
|
.delete()
|
|
.eq('user_id', user.id);
|
|
if (del.error) return jsonNoStore({ error: 'failed' }, { status: 500 });
|
|
|
|
const rows = codes.map((c) => ({
|
|
user_id: user.id,
|
|
code_hash: hashRecoveryCode(c),
|
|
}));
|
|
const ins = await admin.from('mfa_recovery_codes').insert(rows);
|
|
if (ins.error) return jsonNoStore({ error: 'failed' }, { status: 500 });
|
|
|
|
return jsonNoStore({ codes });
|
|
}
|