feat(admin): live redis kill-switch on tunnel actions, sortable columns + CSV export + bulk actions, Node 24 LTS
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.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user