pelagia-portal/App/lib/crew-pii.ts
Hardik 06ff587024 fix(crewing): mask Aadhaar/PAN document numbers server-side
The crew profile page passed SeafarerDocument.number to the client unmasked for
all roles and all doc types, exposing full Aadhaar/PAN identity numbers to MPO /
Manager / Site staff — contradicting the field's PII annotation and §6 /
Roles-and-Permissions §3 (Aadhaar/PAN are gated to Accounts/SuperUser, same as
the bank account number).

- crew-pii.ts: add documentNumberValue(number, docType, role) — masks AADHAAR /
  PAN for non-privileged roles via the existing canViewFullBankEpf gate +
  maskTail; non-identity docs (passport, CDC, STCW…) pass through; preserves the
  string|null contract.
- crew/[id]/page.tsx: mask the number server-side before it crosses to the client.
- Tests: unit cases for the helper; an integration test that invokes the server
  component and asserts the documents prop is masked for MANAGER/SITE_STAFF/MPO
  and full for ACCOUNTS/SUPERUSER.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:29:11 +05:30

48 lines
1.9 KiB
TypeScript

import type { Role, SeafarerDocType } from "@prisma/client";
// PII visibility rules for the crew profile (Crewing-Implementation-Spec §6/§8.8).
// Bank account / EPF identity numbers are full only for Accounts (and SuperUser);
// masked for everyone else. Salary is hidden from site staff (office-only).
export function canViewFullBankEpf(role: Role): boolean {
return role === "ACCOUNTS" || role === "SUPERUSER";
}
// Identity documents whose number is itself restricted PII (Aadhaar/PAN), gated
// like bank/EPF (§6, Roles-and-Permissions §3). Other seafarer documents
// (passport, CDC, STCW, COC, medical…) are not number-restricted.
const RESTRICTED_DOC_TYPES = new Set<SeafarerDocType>(["AADHAAR", "PAN"]);
export function canViewSalary(role: Role): boolean {
// Office roles see salary; site staff see status only (§6, R7).
return role !== "SITE_STAFF";
}
// "•••• 4471" — keep only the last `visible` chars; null/short values render "—".
export function maskTail(value: string | null | undefined, visible = 4): string {
if (!value) return "—";
const v = value.trim();
if (v.length <= visible) return "••••";
return `•••• ${v.slice(-visible)}`;
}
// Show the value in full only when allowed, else mask it.
export function bankEpfValue(value: string | null | undefined, role: Role): string {
if (!value) return "—";
return canViewFullBankEpf(role) ? value : maskTail(value);
}
// A seafarer document number, masked for non-privileged roles when the document
// type is itself restricted PII (Aadhaar/PAN). Non-restricted documents pass
// through unchanged. Preserves the `string | null` contract the profile expects.
export function documentNumberValue(
value: string | null | undefined,
docType: SeafarerDocType,
role: Role
): string | null {
if (!value) return null;
if (RESTRICTED_DOC_TYPES.has(docType) && !canViewFullBankEpf(role)) {
return maskTail(value);
}
return value;
}