c6ec93eef9
The audit #485 called for: on SQLite (native installs), timestamp columns written with a raw `new Date()` through knex store epoch-ms numbers; Postgres returns ISO strings. Frontend code written against Postgres calls parseISO() on them — parseISO(number) throws and crashes the page. #485 fixed admin Users and listed api tokens / photos / activity as out-of-scope follow-ups. Verified crash on main: Timeline gallery layout parseISO(uploaded_at) against photos written by the archive-RESTORE path (raw Date). Other raw-write surfaces (api_tokens last_used_at/revoked_at, email_queue) degrade rather than crash but violate the ISO contract. - extract toIso() from adminUsers.js into utils/dateNormalize.js (contract unchanged — the 10 existing #485 tests still pin it) - write-side: archive-restore uploaded_at, api-token last_used_at / revoked_at, email_queue created_at/sent_at now write ISO strings - read-side (heals existing corrupted rows): gallery /photos normalizes uploaded_at/captured_at; api-tokens list normalizes all four timestamp fields - frontend defence-in-depth: Timeline layout parses uploaded_at tolerantly (typeof guard) for stale caches / old backends - 2 regression tests seed literal epoch numbers and assert the API serves ISO strings activity_logs turned out safe (created_at comes from the DB default, not a raw Date) — left untouched. Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
37 lines
1.6 KiB
JavaScript
37 lines
1.6 KiB
JavaScript
/**
|
|
* Coerce any of the shapes a TIMESTAMP column produces across our
|
|
* supported drivers into a single ISO 8601 string the frontend (and
|
|
* any external API consumer) can safely pass to date-fns / new Date.
|
|
*
|
|
* Postgres → Date object (becomes ISO via JSON.stringify anyway, but
|
|
* pinning the format defends against driver-side surprises).
|
|
* SQLite → integer milliseconds since epoch when a raw `new Date()` was
|
|
* written through knex (the surface that crashed the admin Users page
|
|
* in #485 — `parseISO(123456789)` blows up with "e.split is not a
|
|
* function"). Native installs default to SQLite, so this path matters
|
|
* every release.
|
|
* Already a string → assume it's a parseable ISO/RFC3339 (Postgres
|
|
* driver may stringify under JSON serialization mid-pipeline).
|
|
*
|
|
* Returns null/undefined unchanged so an unset value surfaces as
|
|
* "Never" in the UI rather than 1970-01-01T00:00:00Z.
|
|
*
|
|
* Extracted from routes/adminUsers.js (#485) so every route that
|
|
* serializes timestamps can share one contract.
|
|
*/
|
|
function toIso(value) {
|
|
if (value === null || value === undefined || value === '') return value;
|
|
if (value instanceof Date) return value.toISOString();
|
|
if (typeof value === 'number') return new Date(value).toISOString();
|
|
if (typeof value === 'string') {
|
|
// Numeric-as-string ("1778752458666") happens when the SQLite
|
|
// driver stringifies large integers — re-coerce so the frontend
|
|
// doesn't try to parseISO('1778752458666').
|
|
if (/^\d{10,}$/.test(value)) return new Date(Number(value)).toISOString();
|
|
return value;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
module.exports = { toIso };
|