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 <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
2f05fcc39d
commit
c6ec93eef9
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user