← 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=…&amp;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;