Implemented rbac
This commit is contained in:
@@ -1,7 +1,3 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
export interface ExportColumn<T = any> {
|
||||
header: string;
|
||||
key?: keyof T | string;
|
||||
@@ -23,7 +19,7 @@ export interface ExportOptions<T = any> {
|
||||
* Draws the official PFM Shield and Brand Header on jsPDF in landscape mode
|
||||
*/
|
||||
function drawPfmPdfHeader(
|
||||
doc: jsPDF,
|
||||
doc: any,
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
tenantName?: string,
|
||||
@@ -127,7 +123,7 @@ function drawPfmPdfHeader(
|
||||
/**
|
||||
* Export tabular data to an Excel / Spreadsheet file (.xlsx)
|
||||
*/
|
||||
export function exportToSpreadsheet<T>(options: ExportOptions<T>): void {
|
||||
export async function exportToSpreadsheet<T>(options: ExportOptions<T>): Promise<void> {
|
||||
const { title, filename, columns, data } = options;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
@@ -135,6 +131,8 @@ export function exportToSpreadsheet<T>(options: ExportOptions<T>): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const XLSX = await import('xlsx');
|
||||
|
||||
// Format table rows based on columns config
|
||||
const rows = data.map((item) => {
|
||||
const rowObj: Record<string, any> = {};
|
||||
@@ -185,7 +183,7 @@ export function exportToSpreadsheet<T>(options: ExportOptions<T>): void {
|
||||
/**
|
||||
* Export tabular data to a landscape PDF file with PFM Logo and branding
|
||||
*/
|
||||
export function exportToPdf<T>(options: ExportOptions<T>): void {
|
||||
export async function exportToPdf<T>(options: ExportOptions<T>): Promise<void> {
|
||||
const { title, subtitle, filename, columns, data, tenantName, userEmail } = options;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
@@ -193,6 +191,11 @@ export function exportToPdf<T>(options: ExportOptions<T>): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const [{ default: jsPDF }, { default: autoTable }] = await Promise.all([
|
||||
import('jspdf'),
|
||||
import('jspdf-autotable'),
|
||||
]);
|
||||
|
||||
// Landscape A4 doc: width: 841.89 pt, height: 595.28 pt
|
||||
const doc = new jsPDF({
|
||||
orientation: 'landscape',
|
||||
@@ -240,7 +243,7 @@ export function exportToPdf<T>(options: ExportOptions<T>): void {
|
||||
alternateRowStyles: {
|
||||
fillColor: [248, 250, 252], // slate-50
|
||||
},
|
||||
didDrawPage: (dataArg) => {
|
||||
didDrawPage: (dataArg: any) => {
|
||||
// Footer on all pages
|
||||
const pageCount = (doc.internal as any).getNumberOfPages();
|
||||
const currentPage = dataArg.pageNumber;
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import Hashids from 'hashids';
|
||||
|
||||
const env = (typeof import.meta !== 'undefined' && import.meta.env
|
||||
? import.meta.env
|
||||
: {}) as Record<string, string | undefined>;
|
||||
|
||||
export const HASHER_SETTINGS = {
|
||||
minHashLength: Number(env.VITE_HASHER_MIN_LENGTH) || 11,
|
||||
salt:
|
||||
(env.VITE_HASHER_SALT as string) ||
|
||||
'TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==',
|
||||
};
|
||||
|
||||
let hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength);
|
||||
|
||||
export function updateHasherSettings(salt?: string, minLength?: number | string): void {
|
||||
let updated = false;
|
||||
if (salt && typeof salt === 'string' && salt.trim() !== '') {
|
||||
HASHER_SETTINGS.salt = salt.trim();
|
||||
updated = true;
|
||||
}
|
||||
if (minLength !== undefined && minLength !== null && minLength !== '') {
|
||||
const parsed = Number(minLength);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
HASHER_SETTINGS.minHashLength = parsed;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if candidate is already a HashID generated by this hasher.
|
||||
*/
|
||||
export function isHashId(candidate: string | null | undefined): boolean {
|
||||
if (!candidate || typeof candidate !== 'string' || candidate.length < HASHER_SETTINGS.minHashLength) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const decodedNumbers = hasher.decode(candidate);
|
||||
if (Array.isArray(decodedNumbers) && decodedNumbers.length > 0) return true;
|
||||
const decodedHex = hasher.decodeHex(candidate);
|
||||
if (decodedHex && decodedHex.length > 0) return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a user idNumber using Hashids Hex encoding.
|
||||
* Supports South African national IDs (13 digits), passports, and alphanumeric keys.
|
||||
*/
|
||||
export function encodeIdNumberHex(idNumber: string | number | null | undefined): string {
|
||||
if (idNumber === undefined || idNumber === null) return '';
|
||||
const str = String(idNumber).trim();
|
||||
if (!str) return '';
|
||||
|
||||
// Prevent double-encoding if already a Hashid
|
||||
if (isHashId(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// If already pure hex digits (e.g. standard decimal SA ID 9501015800085 or hex hash)
|
||||
if (/^[0-9a-fA-F]+$/.test(str)) {
|
||||
try {
|
||||
const encoded = hasher.encodeHex(str.toLowerCase());
|
||||
if (encoded) return encoded;
|
||||
} catch {
|
||||
// Fallback below
|
||||
}
|
||||
}
|
||||
|
||||
// For arbitrary alphanumeric ID strings, encode character bytes to hex
|
||||
try {
|
||||
const hex = Array.from(str)
|
||||
.map((c) => c.charCodeAt(0).toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
const encoded = hasher.encodeHex(hex);
|
||||
return encoded || str;
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a Hashids Hex encoded string back to original idNumber.
|
||||
*/
|
||||
export function decodeIdNumberHex(hash: string | null | undefined): string {
|
||||
if (!hash || typeof hash !== 'string') return '';
|
||||
const trimmed = hash.trim();
|
||||
if (!trimmed) return '';
|
||||
|
||||
try {
|
||||
const decodedHex = hasher.decodeHex(trimmed);
|
||||
if (!decodedHex) return trimmed;
|
||||
|
||||
// Check if decoded hex represents ASCII text with 2-char hex pairs
|
||||
if (/^[0-9a-fA-F]{4,}$/.test(decodedHex) && decodedHex.length % 2 === 0) {
|
||||
let isText = true;
|
||||
let text = '';
|
||||
for (let i = 0; i < decodedHex.length; i += 2) {
|
||||
const code = parseInt(decodedHex.slice(i, i + 2), 16);
|
||||
if (code < 32 || code > 126) {
|
||||
isText = false;
|
||||
break;
|
||||
}
|
||||
text += String.fromCharCode(code);
|
||||
}
|
||||
if (isText && text.length > 0) return text;
|
||||
}
|
||||
|
||||
return decodedHex;
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts any numeric or string ID to a safe 64-bit integer string.
|
||||
* Preserves precision beyond 2^53 - 1 by avoiding standard float coercion.
|
||||
*/
|
||||
export function toSafeInt64String(val: string | number | bigint | null | undefined): string {
|
||||
if (val === undefined || val === null || val === '') return '';
|
||||
if (typeof val === 'bigint') return val.toString();
|
||||
if (typeof val === 'number') {
|
||||
if (isNaN(val) || !isFinite(val)) return '';
|
||||
return Math.trunc(val).toString();
|
||||
}
|
||||
const str = String(val).trim();
|
||||
if (/^-?\d+$/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministically encodes a numeric ID or 64-bit integer into a standard HashID string.
|
||||
*/
|
||||
export function encodeEntityId(id: string | number | bigint | null | undefined): string {
|
||||
if (id === undefined || id === null || id === '') return '';
|
||||
|
||||
const str = typeof id === 'bigint' ? id.toString() : String(id).trim();
|
||||
if (!str) return '';
|
||||
|
||||
if (isHashId(str)) return str;
|
||||
|
||||
// If safe integer range
|
||||
if (/^\d+$/.test(str)) {
|
||||
try {
|
||||
const num = Number(str);
|
||||
if (num <= Number.MAX_SAFE_INTEGER && num >= 0) {
|
||||
return hasher.encode(num);
|
||||
}
|
||||
// For integers above 2^53 - 1, encode as hex
|
||||
const bigHex = BigInt(str).toString(16);
|
||||
return hasher.encodeHex(bigHex);
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministically decodes a standard HashID string to its canonical ID representation as string.
|
||||
*/
|
||||
export function decodeEntityId(hashId: string | number | null | undefined): string {
|
||||
if (hashId === undefined || hashId === null || hashId === '') return '';
|
||||
const str = String(hashId).trim();
|
||||
if (!str) return '';
|
||||
|
||||
if (/^-?\d+$/.test(str)) return str;
|
||||
|
||||
try {
|
||||
const decoded = hasher.decode(str);
|
||||
if (Array.isArray(decoded) && decoded.length > 0) {
|
||||
return decoded[0].toString();
|
||||
}
|
||||
const decodedHex = hasher.decodeHex(str);
|
||||
if (decodedHex) {
|
||||
try {
|
||||
return BigInt('0x' + decodedHex).toString();
|
||||
} catch {
|
||||
return decodedHex;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return raw
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
Reference in New Issue
Block a user