Files
pn-new-crm/src/app/api/v1/residential/assignable-users/route.ts
Matt 3c9310f81c fix(audit): critical C3 — enforce residential module gate on all v1 API routes
Adds assertResidentialModuleEnabled(ctx.portId) as the first statement in
every residential v1 handler (24 handlers across 13 files), mirroring the
Tenancies pattern. Previously the disabled-module state was enforced only
in the page layout, so a disabled module still accepted API writes
(including partner-forward emails on residential interest creation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:59:52 +02:00

52 lines
1.9 KiB
TypeScript

import { NextResponse } from 'next/server';
import { and, eq, or, sql } from 'drizzle-orm';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { db } from '@/lib/db';
import { roles, user, userPortRoles } from '@/lib/db/schema/users';
import { errorResponse } from '@/lib/errors';
import { assertResidentialModuleEnabled } from '@/lib/services/residential-module.service';
/**
* Returns the set of users in the current port who can be assigned a
* residential interest. A user qualifies when ANY of their port-role
* grants either:
* - role.permissions.residential_interests.view = true, OR
* - role.permissions.residential_clients.view = true, OR
* - the per-user `residentialAccess` toggle is set on this port
*
* Used by the residential-interest detail page's "Assigned to" picker.
* Returns minimal `{ id, name, email }` rows so the dropdown stays
* fast and the JSON payload doesn't leak more than the picker needs.
*/
export const GET = withAuth(
withPermission('residential_interests', 'view', async (_req, ctx) => {
try {
await assertResidentialModuleEnabled(ctx.portId);
const rows = await db
.selectDistinct({
id: user.id,
name: user.name,
email: user.email,
})
.from(userPortRoles)
.innerJoin(roles, eq(roles.id, userPortRoles.roleId))
.innerJoin(user, eq(user.id, userPortRoles.userId))
.where(
and(
eq(userPortRoles.portId, ctx.portId),
or(
eq(userPortRoles.residentialAccess, true),
sql`${roles.permissions}->'residential_interests'->>'view' = 'true'`,
sql`${roles.permissions}->'residential_clients'->>'view' = 'true'`,
)!,
),
)
.orderBy(user.name);
return NextResponse.json({ data: rows });
} catch (error) {
return errorResponse(error);
}
}),
);