← back to Norma
app/api/ingest/gmail/route.ts
133 lines
import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
import { syncGmail, type GmailAccount, type SyncOptions, type SyncResult } from '@/lib/ingest-gmail';
/**
* POST /api/ingest/gmail
*
* Trigger Gmail ingestion for one or both accounts.
*
* Query params:
* ?account=steve — sync only the steve@ account
* ?account=info — sync only the info@ account
* (omit account) — sync both accounts sequentially
*
* Optional JSON body:
* {
* "afterDate": "2026-01-01", // ISO date — only fetch messages after this date
* "query": "label:INBOX", // extra Gmail search terms
* "maxPages": 20 // max list pages per account (default 10)
* }
*
* Response 200:
* {
* "results": [
* { "account": "steve", "mailboxId": "...", "fetched": 50,
* "inserted": 12, "skipped": 38, "errors": 0, "newCursor": "2026-02-26" },
* { "account": "info", ... }
* ],
* "totalInserted": 12,
* "totalFetched": 100,
* "durationMs": 4823
* }
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const startMs = Date.now();
const ip =
request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
undefined;
// Parse optional JSON body — gracefully ignore empty or non-JSON bodies
let bodyOptions: SyncOptions = {};
try {
const text = await request.text();
if (text.trim()) {
const parsed = JSON.parse(text);
bodyOptions = {
afterDate: typeof parsed.afterDate === 'string' ? parsed.afterDate : undefined,
query: typeof parsed.query === 'string' ? parsed.query : undefined,
maxPages: typeof parsed.maxPages === 'number' ? parsed.maxPages : undefined,
};
}
} catch {
// Non-JSON body is fine — just use defaults
}
// Determine which accounts to sync
const { searchParams } = new URL(request.url);
const accountParam = searchParams.get('account');
let accounts: GmailAccount[];
if (accountParam === 'steve') {
accounts = ['steve'];
} else if (accountParam === 'info') {
accounts = ['info'];
} else if (accountParam === null) {
accounts = ['steve', 'info'];
} else {
return NextResponse.json(
{ error: `Invalid account parameter "${accountParam}". Must be "steve" or "info".` },
{ status: 400 },
);
}
// Run syncs sequentially so DB load stays manageable
const results: SyncResult[] = [];
for (const account of accounts) {
try {
const result = await syncGmail(account, bodyOptions);
results.push(result);
} catch (err) {
const message = (err as Error).message;
console.error(`[api/ingest/gmail] syncGmail(${account}) threw:`, message);
// Push a partial failure result rather than aborting the whole response
results.push({
account,
mailboxId: '',
fetched: 0,
inserted: 0,
skipped: 0,
errors: 1,
newCursor: null,
});
}
}
const durationMs = Date.now() - startMs;
const totalFetched = results.reduce((sum, r) => sum + r.fetched, 0);
const totalInserted = results.reduce((sum, r) => sum + r.inserted, 0);
const totalErrors = results.reduce((sum, r) => sum + r.errors, 0);
// Audit log the completed sync
await auditLog(
'gmail.ingest',
'mailbox',
accounts.join(','),
{
accounts,
totalFetched,
totalInserted,
totalErrors,
durationMs,
options: bodyOptions,
},
ip,
);
return NextResponse.json(
{
results,
totalFetched,
totalInserted,
durationMs,
},
{ status: 200 },
);
}