← back to Norma
app/api/gmail/oauth/route.ts
167 lines
import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import { getAuthUrl, getConnectionStatus, getOAuth2Client, storeTokens } from '@/lib/gmail';
import { verifyAuth } from '@/lib/auth';
/**
* GET /api/gmail/oauth
* ?action=connect → show auth URL + code paste form for ?mailbox=<email>
* ?action=status → connection status JSON
* (default) → status
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff', 'intern');
if (auth instanceof NextResponse) return auth;
const { searchParams } = new URL(request.url);
const action = searchParams.get('action');
// Resolve the mailbox we're connecting:
// 1. explicit ?mailbox=… (admin can connect any)
// 2. session email (the logged-in user's own mailbox)
let mailboxEmail = searchParams.get('mailbox') || '';
if (!mailboxEmail && auth.username.includes('@')) {
// tier_credentials has no email column — the username IS the email.
mailboxEmail = auth.username;
}
if (!mailboxEmail) {
return NextResponse.json({ error: 'No mailbox email resolvable for this user' }, { status: 400 });
}
// Non-admins may only connect their own mailbox
if (auth.role !== 'admin') {
const ownEmail = (auth.username.includes('@') ? auth.username : '').toLowerCase();
if (mailboxEmail.toLowerCase() !== ownEmail) {
return NextResponse.json({ error: 'You can only connect your own Gmail' }, { status: 403 });
}
}
if (action === 'connect') {
const url = getAuthUrl(mailboxEmail, mailboxEmail);
return new NextResponse(
`<!DOCTYPE html>
<html><head><title>Connect Gmail — ${mailboxEmail}</title>
<style>
body { font-family: system-ui, sans-serif; background: #0C0F1A; color: #f0f0f5; padding: 40px; max-width: 640px; margin: 0 auto; }
a { color: #34d399; }
input { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #2A3048; background: #131829; color: #f0f0f5; font-size: 14px; margin: 8px 0; }
button { padding: 12px 24px; border-radius: 8px; border: none; background: #10b981; color: #fff; font-weight: 600; cursor: pointer; font-size: 14px; }
button:hover { background: #059669; }
.step { background: #131829; border: 1px solid #2A3048; border-radius: 10px; padding: 16px; margin: 16px 0; }
.step h3 { margin: 0 0 8px; color: #34d399; font-size: 14px; }
code { background: #1C2237; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
</style>
</head><body>
<h1 style="font-size:20px">Connect Gmail — ${mailboxEmail}</h1>
<div class="step">
<h3>Step 1 — Authorize</h3>
<p>Click below to open Google's consent screen. Sign in as <strong>${mailboxEmail}</strong>.</p>
<a href="${url}" target="_blank"><button type="button">Open Google Authorization</button></a>
</div>
<div class="step">
<h3>Step 2 — Copy the code</h3>
<p>After authorizing, Google will redirect to a URL that contains <code>?code=…&state=${encodeURIComponent(mailboxEmail)}</code>. Copy the FULL URL from the address bar.</p>
</div>
<div class="step">
<h3>Step 3 — Paste here</h3>
<form method="POST" action="/api/gmail/oauth">
<input name="mailbox" type="hidden" value="${mailboxEmail}" />
<input name="code_url" placeholder="Paste the full redirect URL or just the code" autofocus />
<button type="submit">Connect Gmail</button>
</form>
</div>
</body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
}
const status = await getConnectionStatus();
return NextResponse.json({ ...status, mailbox_email: mailboxEmail });
}
/**
* POST /api/gmail/oauth — Exchange auth code for tokens for the named mailbox.
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff', 'intern');
if (auth instanceof NextResponse) return auth;
const contentType = request.headers.get('content-type') || '';
let codeInput = '';
let mailboxEmail = '';
if (contentType.includes('form')) {
const form = await request.formData();
codeInput = (form.get('code_url') as string) || '';
mailboxEmail = (form.get('mailbox') as string) || '';
} else {
const body = await request.json().catch(() => ({}));
codeInput = body.code_url || body.code || '';
mailboxEmail = body.mailbox || '';
}
// Pull state= out of the URL if present — Google round-trips our mailbox email
if (codeInput.includes('state=')) {
const m = codeInput.match(/[?&]state=([^&]+)/);
if (m && !mailboxEmail) mailboxEmail = decodeURIComponent(m[1]);
}
// Extract code
let code = codeInput.trim();
if (code.includes('code=')) {
const match = code.match(/[?&]code=([^&]+)/);
code = match ? decodeURIComponent(match[1]) : '';
}
if (!code || !mailboxEmail) {
return new NextResponse(
`<html><body style="font-family:system-ui;background:#0C0F1A;color:#f0f0f5;padding:40px">
<h2 style="color:#f43f5e">Missing data</h2>
<p>code: ${code ? 'ok' : 'MISSING'}, mailbox: ${mailboxEmail || 'MISSING'}</p>
<a href="/api/gmail/oauth?action=connect&mailbox=${encodeURIComponent(mailboxEmail || '')}" style="color:#34d399">Try again</a>
</body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
}
// Non-admins still restricted to their own mailbox
if (auth.role !== 'admin') {
const ownEmail = (auth.username.includes('@') ? auth.username : '').toLowerCase();
if (mailboxEmail.toLowerCase() !== ownEmail) {
return NextResponse.json({ error: 'You can only connect your own Gmail' }, { status: 403 });
}
}
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}`);
return new NextResponse(
`<html><body style="font-family:system-ui;background:#0C0F1A;color:#f0f0f5;padding:40px;text-align:center">
<h2 style="color:#10b981">Gmail Connected!</h2>
<p>${mailboxEmail} is now linked to Norma.</p>
<p><a href="/" style="color:#34d399">← Back to Norma</a></p>
</body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
} catch (err) {
console.error('[gmail-oauth] Token exchange failed:', (err as Error).message);
return new NextResponse(
`<html><body style="font-family:system-ui;background:#0C0F1A;color:#f0f0f5;padding:40px">
<h2 style="color:#f43f5e">Token Exchange Failed</h2>
<p>${(err as Error).message}</p>
<p><a href="/api/gmail/oauth?action=connect&mailbox=${encodeURIComponent(mailboxEmail)}" style="color:#34d399">Try again</a></p>
</body></html>`,
{ headers: { 'Content-Type': 'text/html' } },
);
}
}
// silence unused
void verifyAuth;