feat(faza-c.2): gate legacy GisFeature writes under USE_GIS_AC
Adds gateLegacyGisWrite() helper that returns 410 when the caller is on the api.gis.ac path (global USE_GIS_AC=1 or per-user GIS_AC_PILOT_USERS). Wired into 13 routes covering every entry point that touches Gis* tables on architools_postgres — directly or via parcel-sync services. Why: yesterday 4 GisFeature rows were updated on architools_postgres even though the scheduler is officially disabled. Root cause: pilot user opened the legacy /geoportal UI in a stale tab and clicked parcels; POST /api/geoportal/enrich wrote directly to the local DB. Without a write gate, Faza H (pg_dump + REVOKE + DROP) is unsafe — any stale tab in any user's browser can still trip writes between freeze and DROP. Gated routes (writes only — reads stay open for rollback ergonomics): - /api/geoportal/enrich (POST) — the writer of the 4 rows - /api/eterra/sync-rules (POST), /api/eterra/sync-rules/[id] (PATCH+DELETE) - /api/eterra/sync-rules/bulk (POST) - /api/eterra/uats (POST+PATCH) - /api/eterra/sync (POST), /api/eterra/sync-county (POST) - /api/eterra/sync-background (POST), /api/eterra/sync-all-counties (POST) - /api/eterra/auto-refresh (POST), /api/eterra/refresh-all (POST) - /api/eterra/export-layer-gpkg (POST), /api/eterra/export-bundle (POST) (last two trigger syncLayer write-first-then-export) Read-only routes intentionally NOT gated: sync-status, no-geom-scan (scanNoGeometryParcels is read-only), export-local, db-summary, counties, search, features (GET), stats, uat-dashboard, sync-rules (GET), sync-rules/scheduler. Operations: after redeploy, flip USE_GIS_AC=1 in Infisical /architools prod env and restart container. Then monitor docker logs for ~30 min: grep "deprecated" + "/api/geoportal/enrich|/api/eterra/sync*" lines indicate stale-tab clients that need a refresh. pg_stat_user_tables write count on GisFeature should hit 0 within one hour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
|
||||
import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store";
|
||||
import { gateLegacyGisWrite } from "@/core/feature-flags/gis-write-gate";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -212,6 +213,8 @@ export async function GET() {
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export async function POST() {
|
||||
const gate = await gateLegacyGisWrite("/api/eterra/uats");
|
||||
if (gate) return gate;
|
||||
try {
|
||||
|
||||
// Read uat.json from public/ directory
|
||||
@@ -288,6 +291,8 @@ export async function POST() {
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export async function PATCH() {
|
||||
const gate = await gateLegacyGisWrite("/api/eterra/uats");
|
||||
if (gate) return gate;
|
||||
try {
|
||||
// 1. Get eTerra credentials from session
|
||||
const session = getSessionCredentials();
|
||||
|
||||
Reference in New Issue
Block a user