Adding PCI‑DSS/ISO‑27001 Authorization Controls to the Checklist (appcheck‑uhpcm)
TL;DR: I extended the checklist data model with PCI‑DSS/ISO‑27001 fields, updated the admin UI, API routes, and added a daily reminder cron. The changes required a Prisma migration, type‑safe payload handling, and a small background job, all committed in a single public PR.
The Problem
Our internal compliance dashboard needed to capture authorization status for each application (PCI‑DSS 2.2.1 / ISO 27001) and the position of the collaborator who performed the validation. The existing ChecklistApp model only stored generic metadata (criticality, active, order). Without explicit fields, auditors could not query “which apps have been formally authorized?” and the UI could not surface the required information.
The symptom showed up in two ways:
- The admin checklist page (
src/app/admin/page.tsx) displayed a static “Observaciones” field but had nowhere to persist the new data. - When trying to add the fields directly in the UI, Prisma threw
P2025: Record not founderrors because the generated client still referenced the old schema.
What I Tried First
My first attempt was to hard‑code the new fields in the React form and send them as part of the PATCH payload to the existing src/app/api/admin/apps/[id]/route.ts. I added the following to the request body:
{
"authorizationStatus": "Authorized",
"authorizedBy": "jdoe@example.com",
"authorizedAt": "2026-10-01T12:00:00Z"
}
The endpoint accepted the payload (no TypeScript error), but the Prisma update call still used the old model definition:
await prisma.checklistApp.update({
where: { id: Number(id) },
data: {
// only known fields were spread here
...body,
},
});
Running the request resulted in a PrismaClientKnownRequestError:
Invalid `prisma.checklistApp.update()` invocation:
{
where: {
id: 12
},
data: {
authorizationStatus: 'Authorized' // <-- unknown field
}
}
The error proved that the database schema and the generated client were out of sync. I also realized that adding the collaborator's position field required a migration on the DeviceValidation model, which I hadn’t touched yet.
The Implementation
1. Extend the Prisma schema
I added the missing columns directly in prisma/schema.prisma. The diff looks like this:
@@ -640,6 +640,14 @@ model ChecklistApp {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+ // Formal authorization for PCI‑DSS 2.2.1 / ISO 27001
+ authorizationStatus String? // e.g. "Authorized", "Pending", "Rejected"
+ authorizedBy String? // email of the authorizer
+ authorizedAt DateTime? // timestamp of the authorization
}
@@ -674,6 +682,7 @@ model DeviceValidation {
warranty String?
deliveryStatus String?
auditDate DateTime?
+ position String? // collaborator's position (e.g., "Network Engineer")
Running npx prisma migrate dev --name add-compliance-fields generated the migration and updated the client.
2. Update the admin UI (src/app/admin/page.tsx)
I introduced three new controlled inputs in the form that edits an app:
// src/app/admin/page.tsx (excerpt)
<FormField
label="Authorization Status"
name="authorizationStatus"
type="select"
options={['Authorized', 'Pending', 'Rejected']}
/>
<FormField
label="Authorized By"
name="authorizedBy"
type="email"
/>
<FormField
label="Authorized At"
name="authorizedAt"
type="datetime-local"
/>
The component now spreads these fields into the formData state, so they are sent to the backend unchanged.
3. Adjust the PATCH endpoint (src/app/api/admin/apps/[id]/route.ts)
I typed the incoming body to include the new optional fields and passed them to Prisma:
// src/app/api/admin/apps/[id]/route.ts
export async function PATCH(req: Request, ctx: { params: Promise<{ id: string }> }) {
const { id } = await ctx.params;
const body = await req.json() as {
name?: string;
criticality?: string;
order?: number;
active?: boolean;
categoryId?: string;
authorizationStatus?: string;
authorizedBy?: string | null;
authorizedAt?: string | null;
};
const updated = await prisma.checklistApp.update({
where: { id: Number(id) },
data: {
name: body.name?.trim(),
criticality: body.criticality,
order: body.order,
active: body.active,
categoryId: body.categoryId,
authorizationStatus: body.authorizationStatus,
authorizedBy: body.authorizedBy,
authorizedAt: body.authorizedAt ? new Date(body.authorizedAt) : undefined,
},
});
return NextResponse.json(updated);
}
Notice the explicit conversion of authorizedAt to Date – Prisma expects a DateTime object, not a string.
4. Extend the POST endpoint for new apps (src/app/api/admin/apps/route.ts)
When creating a new app, we now set a default authorizationStatus of "Pending":
// src/app/api/admin/apps/route.ts
const newApp = await prisma.checklistApp.create({
data: {
name: body.name.trim(),
criticality: body.criticality ?? "Media",
order: body.order ?? 0,
active: true,
authorizationStatus: "Pending",
},
});
5. Store collaborator position in validations (src/app/api/validations/[id]/entries/[entryId]/route.ts)
The DeviceValidation model now has a position column. I added it to the PATCH handler that updates a validation entry:
// src/app/api/validations/[id]/entries/[entryId]/route.ts
export async function PATCH(req: Request, ctx) {
const { id, entryId } = await ctx.params;
const body = await req.json() as {
// ...other fields
position?: string;
};
await prisma.deviceValidationEntry.update({
where: { id: Number(entryId) },
data: {
// existing fields …
position: body.position,
},
});
}
6. Daily reminder cron (`src/app/api/
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/appcheck-uhpcm · 2026-10-10
#playadev #buildinpublic
Top comments (0)