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>
48 lines
1.9 KiB
TypeScript
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;
|
|
}
|