37 lines
1.6 KiB
TypeScript
37 lines
1.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { logger } from '@/lib/logger';
|
|
import { consumePairing } from '@/lib/auth/pairing-store';
|
|
|
|
// Phone side of the cross-device QR login. The app POSTs the pairing code it
|
|
// scanned; we hand back the OAuth token bundle the desktop stashed at
|
|
// /api/auth/pair/create. The code is the only credential required — it is
|
|
// high-entropy, single-use, and expires within ~2 minutes — so this route is
|
|
// intentionally unauthenticated (the scanning device has no webmail cookies).
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const { pairing_code: pairingCode } = await request.json().catch(() => ({}));
|
|
if (!pairingCode || typeof pairingCode !== 'string') {
|
|
return NextResponse.json({ error: 'Missing pairing code' }, { status: 400 });
|
|
}
|
|
|
|
const tokens = consumePairing(pairingCode);
|
|
if (!tokens) {
|
|
// Unknown, expired, or already redeemed — do not distinguish.
|
|
return NextResponse.json({ error: 'Invalid or expired pairing code' }, { status: 400 });
|
|
}
|
|
|
|
return NextResponse.json({
|
|
flow: 'oauth',
|
|
server_url: tokens.serverUrl,
|
|
access_token: tokens.accessToken,
|
|
...(tokens.refreshToken ? { refresh_token: tokens.refreshToken } : {}),
|
|
...(typeof tokens.expiresIn === 'number' ? { expires_in: tokens.expiresIn } : {}),
|
|
token_endpoint: tokens.tokenEndpoint,
|
|
client_id: tokens.clientId,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Pair redeem error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|