← back to Norma
app/api/gmail/oauth/callback/route.ts
67 lines
import { NextRequest, NextResponse } from 'next/server';
import { getOAuth2Client, storeTokens, verifyOAuthState } from '@/lib/gmail';
/**
* GET /api/gmail/oauth/callback — Google OAuth2 redirect target.
* Exchanges the authorization code for tokens and stores them on the mailbox
* named by the `state` parameter (we set state=mailboxEmail in /api/gmail/oauth).
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const stateValue = searchParams.get('state') || '';
const error = searchParams.get('error');
if (error) {
return new NextResponse(
`<html><body><h2>OAuth Error</h2><p>${error}</p><p><a href="/">Back to Norma</a></p></body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
}
if (!code) {
return new NextResponse(
`<html><body><h2>Missing code</h2><p>No authorization code received.</p><p><a href="/">Back to Norma</a></p></body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
}
const cookieState = request.cookies.get('gmail-oauth-state')?.value;
const state = cookieState && decodeURIComponent(cookieState) === stateValue
? verifyOAuthState(stateValue)
: null;
if (!state) {
return new NextResponse(
`<html><body><h2>Expired connection</h2><p>The Gmail connection request is invalid or expired. Restart the connect flow from inside Norma.</p><p><a href="/">Back to Norma</a></p></body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
}
const mailboxEmail = state.mailboxEmail;
try {
const client = getOAuth2Client();
const { tokens } = await client.getToken(code);
await storeTokens(tokens as unknown as Record<string, unknown>, mailboxEmail);
console.log(`[gmail-oauth] Tokens stored for ${mailboxEmail}`);
const response = new NextResponse(
`<html><body style="font-family:system-ui;padding:40px;text-align:center">
<h2 style="color:#10b981">Gmail Connected!</h2>
<p>${mailboxEmail} is now linked to Norma.</p>
<p>You can close this tab and return to Norma. Inbox will populate after the next sync.</p>
<script>setTimeout(()=>window.close(),3000)</script>
</body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
response.headers.append('Set-Cookie', 'gmail-oauth-state=; Path=/api/gmail/oauth; HttpOnly; SameSite=Lax; Max-Age=0');
return response;
} catch (err) {
console.error('[gmail-oauth] Token exchange failed:', (err as Error).message);
return new NextResponse(
`<html><body><h2>Token Exchange Failed</h2><p>${(err as Error).message}</p><p><a href="/">Back to Norma</a></p></body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
}
}