← back to Norma

app/api/sessions/[id]/route.ts

152 lines

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

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

/**
 * GET /api/sessions/[id]
 * Return a single session with its drafts.
 */
export async function GET(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { id } = await context.params;
  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

  try {
    const orgFilter = orgId ? ' AND org_id = $2' : '';
    const sessionResult = await query(
      `SELECT * FROM sessions WHERE id = $1${orgFilter}`,
      orgId ? [id, orgId] : [id]
    );

    if (sessionResult.rowCount === 0) {
      return NextResponse.json({ error: 'Session not found' }, { status: 404 });
    }

    const draftsResult = await query(
      `SELECT * FROM drafts WHERE session_id = $1 ORDER BY lane`,
      [id]
    );

    return NextResponse.json({
      session: {
        ...sessionResult.rows[0],
        drafts: draftsResult.rows,
      },
    });
  } catch (err) {
    console.error('[api/sessions/[id]] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch session' }, { status: 500 });
  }
}

/**
 * PATCH /api/sessions/[id]
 * Update session fields: title, status, planning_checklist, planning_notes, news_item_ids, x_post_ids.
 */
export async function PATCH(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { id } = await context.params;

  try {
    const body = await request.json();
    const allowedFields = ['title', 'status', 'planning_checklist', 'planning_notes', 'news_item_ids', 'x_post_ids'];
    const setClauses: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    for (const field of allowedFields) {
      if (field in body) {
        let value = body[field];
        // JSON fields need to be stringified
        if (field === 'planning_checklist' && typeof value === 'object') {
          value = JSON.stringify(value);
        }
        setClauses.push(`${field} = $${paramIndex}`);
        values.push(value);
        paramIndex++;
      }
    }

    if (setClauses.length === 0) {
      return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 });
    }

    values.push(id);
    paramIndex++;
    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
    let sessOrgFilter = '';
    if (orgId) {
      values.push(orgId);
      sessOrgFilter = ` AND org_id = $${paramIndex}`;
    }
    const result = await query(
      `UPDATE sessions SET ${setClauses.join(', ')} WHERE id = $${paramIndex - 1}${sessOrgFilter} RETURNING *`,
      values
    );

    if (result.rowCount === 0) {
      return NextResponse.json({ error: 'Session not found' }, { status: 404 });
    }

    const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
    await auditLog('session.updated', 'session', id, { fields: Object.keys(body).filter((k) => allowedFields.includes(k)) }, ip);

    return NextResponse.json({ session: result.rows[0] });
  } catch (err) {
    console.error('[api/sessions/[id]] PATCH error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to update session' }, { status: 500 });
  }
}

/**
 * DELETE /api/sessions/[id]
 * Delete a session and its drafts in a transaction to prevent orphaned records.
 */
export async function DELETE(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { id } = await context.params;
  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  const client = await getClient();

  try {
    await client.query('BEGIN');

    // Delete associated drafts first (their versions cascade via ON DELETE CASCADE)
    const orgFilter = orgId ? ' AND org_id = $2' : '';
    await client.query(`DELETE FROM drafts WHERE session_id = $1${orgFilter}`, orgId ? [id, orgId] : [id]);

    const result = await client.query(
      `DELETE FROM sessions WHERE id = $1${orgFilter} RETURNING id, title`,
      orgId ? [id, orgId] : [id]
    );

    if (result.rowCount === 0) {
      await client.query('ROLLBACK');
      return NextResponse.json({ error: 'Session not found' }, { status: 404 });
    }

    await client.query('COMMIT');

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

    return NextResponse.json({ success: true, deleted: result.rows[0] });
  } catch (err) {
    await client.query('ROLLBACK');
    console.error('[api/sessions/[id]] DELETE error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to delete session' }, { status: 500 });
  } finally {
    client.release();
  }
}