← back to Norma

app/api/settings/tier-credentials/[id]/route.ts

122 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { hashPassword, ALL_ROLES, type UserRole } from '@/lib/auth';
import { requireRole } from '@/lib/require-role';

interface RouteContext {
  params: Promise<{ id: string }>;
}

/**
 * PUT /api/settings/tier-credentials/[id]
 * Update display_name, role, org_id. Optionally reset password.
 */
export async function PUT(request: NextRequest, context: RouteContext) {
  const result = requireRole(request, 'admin');
  if (result instanceof NextResponse) return result;

  const { id } = await context.params;

  try {
    const body = await request.json();
    const { display_name, role, org_id, password } = body;

    // Verify credential exists
    const existing = await query(
      'SELECT id, username FROM tier_credentials WHERE id = $1',
      [id],
    );
    if (existing.rows.length === 0) {
      return NextResponse.json({ error: 'Credential not found' }, { status: 404 });
    }

    if (role !== undefined && !ALL_ROLES.includes(role as UserRole)) {
      return NextResponse.json(
        { error: `role must be one of ${ALL_ROLES.join(', ')}` },
        { status: 400 },
      );
    }

    // Hard guard: 'admin' username cannot be demoted, otherwise the tenant can
    // lock itself out of user management.
    if (existing.rows[0].username === 'admin' && role !== undefined && role !== 'admin') {
      return NextResponse.json(
        { error: 'Cannot demote the admin account' },
        { status: 403 },
      );
    }

    // Build dynamic update
    const updates: string[] = [];
    const values: unknown[] = [];
    let paramIdx = 1;

    if (display_name !== undefined) {
      updates.push(`display_name = $${paramIdx++}`);
      values.push(display_name || null);
    }
    if (role !== undefined) {
      updates.push(`role = $${paramIdx++}`);
      values.push(role);
    }
    if (org_id !== undefined) {
      updates.push(`org_id = $${paramIdx++}`);
      values.push(org_id || null);
    }
    if (password) {
      updates.push(`password_hash = $${paramIdx++}`);
      values.push(await hashPassword(password));
    }

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

    values.push(id);
    const { rows } = await query(
      `UPDATE tier_credentials SET ${updates.join(', ')} WHERE id = $${paramIdx}
       RETURNING id, username, role, org_id, display_name,
                 created_at, updated_at`,
      values,
    );

    return NextResponse.json({ credential: rows[0] });
  } catch (err) {
    console.error('[tier-credentials] PUT error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to update credential' }, { status: 500 });
  }
}

/**
 * DELETE /api/settings/tier-credentials/[id]
 * Delete a credential. Cannot delete the admin user.
 */
export async function DELETE(request: NextRequest, context: RouteContext) {
  const result = requireRole(request, 'admin');
  if (result instanceof NextResponse) return result;

  const { id } = await context.params;

  try {
    const existing = await query(
      'SELECT username FROM tier_credentials WHERE id = $1',
      [id],
    );
    if (existing.rows.length === 0) {
      return NextResponse.json({ error: 'Credential not found' }, { status: 404 });
    }
    if (existing.rows[0].username === 'admin') {
      return NextResponse.json(
        { error: 'Cannot delete the admin account' },
        { status: 403 },
      );
    }

    await query('DELETE FROM tier_credentials WHERE id = $1', [id]);
    return NextResponse.json({ success: true });
  } catch (err) {
    console.error('[tier-credentials] DELETE error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to delete credential' }, { status: 500 });
  }
}