feat(tenancies-p1): module-enabled gate + admin toggle endpoints
Part of the locked Tenancies module design (docs/tenancies-design.md).
This PR is the gating infrastructure — the actual table rename
(berth_reservations -> tenancies) + self-FKs + perm-rename + sidebar
entry land in subsequent PRs.
What ships:
- `system_settings.tenancies_module_enabled` registry entry (port-scoped
boolean, default false). Surfaces in the registry-driven admin form
+ the resolveForAdminAPI chain.
- `src/lib/services/tenancies-module.service.ts` with:
* isTenanciesModuleEnabled(portId) — checks the admin setting AND
the lazy "any berth_reservations row exists" sentinel
* enableTenanciesModule / disableTenanciesModule — idempotent
upserts on the system_settings row
* assertTenanciesModuleEnabled — throw-on-disabled helper for
route handlers (NotFoundError -> 404)
- Three admin endpoints under /api/v1/admin/tenancies-module/
(status / enable / disable), all gated on admin.manage_settings.
Behaviour today: with the module off (default), nothing changes.
Sidebar, entity tabs, top-level page, webhook auto-create branch,
and dashboard widgets all continue to read the same flag and stay
hidden until either an admin toggles it ON or the first auto-create
flips it via the lazy "row exists" sentinel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
21
src/app/api/v1/admin/tenancies-module/disable/route.ts
Normal file
21
src/app/api/v1/admin/tenancies-module/disable/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { disableTenanciesModule } from '@/lib/services/tenancies-module.service';
|
||||
|
||||
/**
|
||||
* POST /api/v1/admin/tenancies-module/disable — admin-driven disable.
|
||||
* Data is preserved; only the rendering surfaces hide. Frontend warns
|
||||
* the operator about the row count before calling this.
|
||||
*/
|
||||
export const POST = withAuth(
|
||||
withPermission('admin', 'manage_settings', async (_req, ctx) => {
|
||||
try {
|
||||
await disableTenanciesModule(ctx.portId);
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
20
src/app/api/v1/admin/tenancies-module/enable/route.ts
Normal file
20
src/app/api/v1/admin/tenancies-module/enable/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { enableTenanciesModule } from '@/lib/services/tenancies-module.service';
|
||||
|
||||
/**
|
||||
* POST /api/v1/admin/tenancies-module/enable — admin-driven enable.
|
||||
* Idempotent; flipping an already-enabled module is a no-op.
|
||||
*/
|
||||
export const POST = withAuth(
|
||||
withPermission('admin', 'manage_settings', async (_req, ctx) => {
|
||||
try {
|
||||
await enableTenanciesModule(ctx.portId);
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
21
src/app/api/v1/admin/tenancies-module/status/route.ts
Normal file
21
src/app/api/v1/admin/tenancies-module/status/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
|
||||
/**
|
||||
* GET /api/v1/admin/tenancies-module/status — surface whether the module
|
||||
* is currently enabled (via setting OR lazy "any row exists" sentinel)
|
||||
* so the admin Operations page can render the toggle in the correct state.
|
||||
*/
|
||||
export const GET = withAuth(
|
||||
withPermission('admin', 'manage_settings', async (_req, ctx) => {
|
||||
try {
|
||||
const enabled = await isTenanciesModuleEnabled(ctx.portId);
|
||||
return NextResponse.json({ data: { enabled } });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user