← back to Norma

app/api/drafts/[id]/export/route.ts

111 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
import { getOrgId } from '@/lib/orgId';
import { buildEml, buildHtmlDocument, htmlToText, buildJsonExport } from '@/lib/export';

type RouteContext = { params: Promise<{ id: string }> };

/**
 * GET /api/drafts/[id]/export
 * Export a draft in the specified format.
 * Query param: ?format=eml|html|txt|json
 */
export async function GET(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  if (!orgId) {
    return NextResponse.json({ error: 'Missing org context' }, { status: 400 });
  }

  const { id } = await context.params;
  const { searchParams } = new URL(request.url);
  const format = searchParams.get('format') || 'html';

  if (!['eml', 'html', 'txt', 'json'].includes(format)) {
    return NextResponse.json(
      { error: 'Invalid format. Supported: eml, html, txt, json' },
      { status: 400 }
    );
  }

  try {
    const draftResult = await query(`SELECT * FROM drafts WHERE id = $1 AND org_id = $2`, [id, orgId]);
    if (draftResult.rowCount === 0) {
      return NextResponse.json({ error: 'Draft not found' }, { status: 404 });
    }

    const draft = draftResult.rows[0];
    const safeSubject = (draft.subject || 'draft').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 60);

    const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
    await auditLog('draft.exported', 'draft', id, { format }, ip);

    switch (format) {
      case 'eml': {
        const emlContent = buildEml({
          subject: draft.subject,
          from_name: draft.from_name,
          from_email: draft.from_email,
          body_html: draft.body_html,
          body_text: draft.body_text || htmlToText(draft.body_html),
        });

        return new NextResponse(emlContent, {
          status: 200,
          headers: {
            'Content-Type': 'message/rfc822',
            'Content-Disposition': `attachment; filename="${safeSubject}.eml"`,
          },
        });
      }

      case 'html': {
        const htmlDoc = buildHtmlDocument(draft.subject, draft.body_html);

        return new NextResponse(htmlDoc, {
          status: 200,
          headers: {
            'Content-Type': 'text/html; charset=utf-8',
            'Content-Disposition': `attachment; filename="${safeSubject}.html"`,
          },
        });
      }

      case 'txt': {
        const textContent = draft.body_text || htmlToText(draft.body_html);
        const fullText = `Subject: ${draft.subject}\nFrom: ${draft.from_name} <${draft.from_email}>\n\n${textContent}`;

        return new NextResponse(fullText, {
          status: 200,
          headers: {
            'Content-Type': 'text/plain; charset=utf-8',
            'Content-Disposition': `attachment; filename="${safeSubject}.txt"`,
          },
        });
      }

      case 'json': {
        const jsonContent = buildJsonExport(draft);

        return new NextResponse(jsonContent, {
          status: 200,
          headers: {
            'Content-Type': 'application/json; charset=utf-8',
            'Content-Disposition': `attachment; filename="${safeSubject}.json"`,
          },
        });
      }

      default:
        return NextResponse.json({ error: 'Unsupported format' }, { status: 400 });
    }
  } catch (err) {
    console.error('[api/drafts/[id]/export] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to export draft' }, { status: 500 });
  }
}