d317e8c758
WS1: pin all Docker stages to node:24.16.0-alpine; add engines node>=20. WS2: lib/redis.ts gains TTL-backed redisSet, redisDel, setTunnelActive (writes tunnel:active:<sub>=1/0 EX 30, TUNNEL_ACTIVE_TTL override, no-op without REDIS_URL); wired into tunnel active/delete/reassign routes. WS3: sortable columns, CSV export routes (token excluded), and bulk actions (self-account guard) across users/tunnels/audit admin tables.
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
/**
|
|
* Minimal, dependency-free CSV serialization for the admin export endpoints.
|
|
* RFC-4180 style: fields containing a comma, double-quote, CR or LF are wrapped
|
|
* in double-quotes with embedded quotes doubled. Everything is coerced to a
|
|
* string first; null/undefined become empty fields.
|
|
*/
|
|
|
|
export function csvField(v: unknown): string {
|
|
let s: string;
|
|
if (v === null || v === undefined) s = '';
|
|
else if (typeof v === 'string') s = v;
|
|
else if (typeof v === 'object') s = JSON.stringify(v);
|
|
else s = String(v);
|
|
if (/[",\r\n]/.test(s)) {
|
|
return `"${s.replace(/"/g, '""')}"`;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
export function csvRow(values: unknown[]): string {
|
|
return values.map(csvField).join(',');
|
|
}
|
|
|
|
/**
|
|
* Build a full CSV document (header + rows). A leading row is always the
|
|
* provided header. Lines are CRLF-terminated for maximal spreadsheet
|
|
* compatibility.
|
|
*/
|
|
export function toCsv(header: string[], rows: unknown[][]): string {
|
|
const lines = [csvRow(header), ...rows.map(csvRow)];
|
|
return lines.join('\r\n') + '\r\n';
|
|
}
|
|
|
|
/**
|
|
* Max rows any single export will emit. Keeps a runaway export bounded; the
|
|
* caller notes the cap in the report when hit.
|
|
*/
|
|
export const EXPORT_MAX_ROWS = 10000;
|