← back to Norma

app/api/library/route.ts

140 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';

/**
 * GET /api/library
 * List/search library items.
 * Optional filters: ?item_type=, ?search=, ?tags=
 * Ordered by is_pinned DESC, updated_at DESC.
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const { searchParams } = new URL(request.url);
    const itemType = searchParams.get('item_type');
    const search = searchParams.get('search');
    const tags = searchParams.get('tags');

    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
    const conditions: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    // Scope to active org
    if (orgId) {
      conditions.push(`org_id = $${paramIndex}`);
      values.push(orgId);
      paramIndex++;
    }

    if (itemType) {
      conditions.push(`item_type = $${paramIndex}`);
      values.push(itemType);
      paramIndex++;
    }

    if (search) {
      conditions.push(`(title ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`);
      values.push(`%${search}%`);
      paramIndex++;
    }

    if (tags) {
      const tagArray = tags.split(',').map((t) => t.trim()).filter(Boolean);
      conditions.push(`tags && $${paramIndex}`);
      values.push(tagArray);
      paramIndex++;
    }

    let limit = parseInt(searchParams.get('limit') || '50', 10);
    if (isNaN(limit) || limit < 1) limit = 50;
    if (limit > 200) limit = 200;

    let offset = parseInt(searchParams.get('offset') || '0', 10);
    if (isNaN(offset) || offset < 0) offset = 0;

    const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

    const countResult = await query(
      `SELECT COUNT(*)::int AS total FROM library_items ${whereClause}`,
      values
    );
    const total = countResult.rows[0]?.total ?? 0;

    const result = await query(
      `SELECT * FROM library_items ${whereClause}
       ORDER BY is_pinned DESC, updated_at DESC
       LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
      [...values, limit, offset]
    );

    return NextResponse.json({ items: result.rows, total, limit, offset });
  } catch (err) {
    console.error('[api/library] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch library items' }, { status: 500 });
  }
}

/**
 * POST /api/library
 * Add a new library item.
 * Body: { item_type, title, description?, content_html?, content_text?, tags?, email_type?, voice_mode? }
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const body = await request.json();

    if (!body.item_type || !body.title) {
      return NextResponse.json({ error: 'item_type and title are required' }, { status: 400 });
    }

    const validTypes = ['template', 'snippet', 'archived_draft'];
    if (!validTypes.includes(body.item_type)) {
      return NextResponse.json(
        { error: `item_type must be one of: ${validTypes.join(', ')}` },
        { status: 400 }
      );
    }

    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
    const result = await query(
      `INSERT INTO library_items (item_type, title, description, content_html, content_text, tags, email_type, voice_mode, org_id)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
       RETURNING *`,
      [
        body.item_type,
        body.title.trim(),
        body.description || null,
        body.content_html || null,
        body.content_text || null,
        body.tags || [],
        body.email_type || null,
        body.voice_mode || null,
        orgId || null,
      ]
    );

    const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
    await auditLog(
      'library.created',
      'library_item',
      result.rows[0].id,
      { item_type: body.item_type, title: body.title },
      ip
    );

    return NextResponse.json({ item: result.rows[0] }, { status: 201 });
  } catch (err) {
    console.error('[api/library] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to create library item' }, { status: 500 });
  }
}