rcal-online/src/app/api/auth/google/callback/route.ts

90 lines
2.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { exchangeGoogleCode, fetchGoogleCalendars } from '@/lib/oauth'
import { storePendingToken } from '@/lib/pending-tokens'
import { cookies } from 'next/headers'
/**
* GET /api/auth/google/callback
* Handles the OAuth callback from Google.
* Validates CSRF state, exchanges code for tokens, stores tokens in memory
* for one-time client retrieval, then redirects to calendar page.
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const state = searchParams.get('state')
const error = searchParams.get('error')
if (error) {
return NextResponse.redirect(new URL('/rcal/calendar?connect=denied', request.url))
}
if (!code || !state) {
return NextResponse.redirect(
new URL('/rcal/calendar?connect=error&reason=missing_params', request.url)
)
}
// Validate CSRF state token
const cookieStore = await cookies()
const stateCookie = cookieStore.get('oauth_state')
if (!stateCookie?.value) {
return NextResponse.redirect(
new URL('/rcal/calendar?connect=error&reason=expired_session', request.url)
)
}
let statePayload: { csrf: string; userId: string }
try {
statePayload = JSON.parse(stateCookie.value)
} catch {
return NextResponse.redirect(
new URL('/rcal/calendar?connect=error&reason=invalid_state', request.url)
)
}
if (statePayload.csrf !== state) {
return NextResponse.redirect(
new URL('/rcal/calendar?connect=error&reason=csrf_mismatch', request.url)
)
}
// Clear the state cookie
cookieStore.delete('oauth_state')
// Exchange authorization code for tokens (server-side, uses client_secret)
const tokens = await exchangeGoogleCode(code)
// Fetch the user's calendars to find the primary one
const calendars = await fetchGoogleCalendars(tokens.access_token)
// Generate a one-time retrieval code (NOT the OAuth tokens themselves)
const retrievalCode = crypto.randomUUID()
// Store tokens in memory for one-time pickup (60 second TTL)
storePendingToken(retrievalCode, {
provider: 'google',
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_in: tokens.expires_in,
calendars: calendars.map((c) => ({
id: c.id,
name: c.summary,
color: c.backgroundColor,
primary: c.primary,
})),
})
// Redirect to client with only the retrieval code — no tokens in the URL
return NextResponse.redirect(
new URL(`/rcal/calendar?connect=pending&rc=${retrievalCode}`, request.url)
)
} catch (err) {
console.error('Google callback error:', err)
return NextResponse.redirect(
new URL('/rcal/calendar?connect=error&reason=exchange_failed', request.url)
)
}
}