import { NextRequest, NextResponse } from 'next/server'; import { logger } from '@/lib/logger'; import { JmapAuthVerificationError, verifyJmapAuth } from '@/lib/auth/verify-jmap-auth'; import { setStalwartAuthContext } from '@/lib/stalwart/auth-context'; import { configManager } from '@/lib/admin/config-manager'; import { isPublicHttpUrl } from '@/lib/security/url-guard'; import { recordLogin } from '@/lib/telemetry/login-tracker'; import { parseJmapServers, resolveTrustedJmapUrl } from '@/lib/admin/jmap-servers'; import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils'; function getSlot(request: NextRequest, bodySlot: unknown): number { if (typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot < MAX_ACCOUNT_SLOTS) { return bodySlot; } const raw = request.nextUrl.searchParams.get('slot'); if (raw === null) return 0; const slot = parseInt(raw, 10); return Number.isNaN(slot) || slot < 0 || slot >= MAX_ACCOUNT_SLOTS ? 0 : slot; } export async function POST(request: NextRequest) { try { const { serverUrl, username, authHeader, slot: bodySlot } = await request.json(); if (!serverUrl || !username || !authHeader) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } // Pin the upstream URL to a configured JMAP server (single `jmapServerUrl` // or any entry in `jmapServers`). Falls back to the request URL only when // `allowCustomJmapEndpoint` is enabled, and even then it must be public. await configManager.ensureLoaded(); const configuredServerUrl = configManager.get('jmapServerUrl', '') || process.env.JMAP_SERVER_URL || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || ''; const allowCustomEndpoint = configManager.get('allowCustomJmapEndpoint', false); const serverList = parseJmapServers(configManager.get('jmapServers', [])); const trustedUrl = resolveTrustedJmapUrl(serverUrl, configuredServerUrl, serverList); let upstreamUrl: string; let upstreamTrusted: boolean; if (trustedUrl) { upstreamUrl = trustedUrl; upstreamTrusted = true; } else if (allowCustomEndpoint) { if (!(await isPublicHttpUrl(serverUrl))) { return NextResponse.json({ error: 'Server URL is not allowed' }, { status: 400 }); } upstreamUrl = serverUrl; upstreamTrusted = false; } else { return NextResponse.json({ error: 'JMAP server not configured' }, { status: 500 }); } const slot = getSlot(request, bodySlot); const normalizedServerUrl = await verifyJmapAuth(upstreamUrl, authHeader, { trusted: upstreamTrusted }); await setStalwartAuthContext(slot, { serverUrl: normalizedServerUrl, username, authHeader, }); void recordLogin(username, normalizedServerUrl); return NextResponse.json({ ok: true }); } catch (error) { if (error instanceof JmapAuthVerificationError) { return NextResponse.json({ error: error.message }, { status: error.status }); } logger.error('Failed to store Stalwart auth context', { error: error instanceof Error ? error.message : 'Unknown error', }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } }