DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding PCI‑DSS/ISO‑27001 Authorization Controls to the Checklist (appcheck‑uhpcm)

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:

  1. The admin checklist page (src/app/admin/page.tsx) displayed a static “Observaciones” field but had nowhere to persist the new data.
  2. When trying to add the fields directly in the UI, Prisma threw P2025: Record not found errors 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"
}
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

Running the request resulted in a PrismaClientKnownRequestError:

Invalid `prisma.checklistApp.update()` invocation:
{
  where: {
    id: 12
  },
  data: {
    authorizationStatus: 'Authorized' // <-- unknown field
  }
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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",
  },
});
Enter fullscreen mode Exit fullscreen mode

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,
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

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)