Files
TREK/server/src/nest/admin/admin.helpers.ts
T
Maurice a926990e4a fix(db): timestamp columns are written and stored in one canonical spelling
A DbTimestamp brand, produced only by dbNow(), types the expiry writes of OAuth tokens and password reset links, and collection edits stamp updated_at the same way. parseDbTimestamp() reads the stored text as UTC and replaces the oauth-local parser; utcSuffix lives once beside it. A data migration rewrites the ISO values those writers left behind, and the OAuth session lists keep answering the expiries in ISO.

Two outputs change on purpose. collections.updated_at used to come back in two spellings: a collection never edited answered the column default (2026-01-02 03:04:05), an edited one the ISO text its writer stored (2026-01-02T03:04:05.678Z). It now answers the canonical text in every case, in the PUT and cover responses and, once the migration has run, in every read; the shared schema types it as a plain string and the client does not read it. The ISO expiries of the OAuth session lists are rebuilt from the stored second-precision text, so they end in .000Z instead of carrying the milliseconds of the write.
2026-10-10 20:15:55 +02:00

93 lines
3.3 KiB
TypeScript

import fs from 'fs';
/**
* Pure + module-scoped half of the admin domain, relocated verbatim from
* services/adminService.ts with the 2026-08 fold (auth.helpers / maps.helpers /
* atlas-geo precedent).
*
* The version cache is module-scoped **on purpose** (the permissions-cache
* precedent, permissions.bridge.ts): the daily version-check cron
* (VersionCheckJob) and GET /api/admin/version-check both run on the container
* singleton now, but the module scope keeps the one-5-minute-cache guarantee
* independent of how many AdminService instances exist (the legacy module and
* the retired admin bridge relied on exactly that). Instance state would double
* the GitHub traffic and let the cron notify on a version the UI has not seen
* yet.
*/
/** bcrypt cost factor for user passwords — kept in sync with authService. */
export const BCRYPT_COST = 12;
export function compareVersions(a: string, b: string): number {
const parse = (v: string) => {
const [base, pre] = v.split('-pre.');
const parts = base.split('.').map(Number);
const n = pre !== undefined ? Number.parseInt(pre, 10) : null;
const preN = n !== null && Number.isFinite(n) ? n : null;
return { parts, preN };
};
const pa = parse(a),
pb = parse(b);
for (let i = 0; i < Math.max(pa.parts.length, pb.parts.length); i++) {
const na = pa.parts[i] || 0,
nb = pb.parts[i] || 0;
if (na > nb) return 1;
if (na < nb) return -1;
}
// Equal base: stable > prerelease; higher preN wins among prereleases
if (pa.preN === null && pb.preN !== null) return 1;
if (pa.preN !== null && pb.preN === null) return -1;
if (pa.preN !== null && pb.preN !== null) {
if (pa.preN > pb.preN) return 1;
if (pa.preN < pb.preN) return -1;
}
return 0;
}
/**
* Documented parity exception to the "modules must be importable without side
* effects" rule (auth.helpers.ts's import-time DUMMY_PASSWORD_HASH precedent):
* this probes the filesystem at module evaluation, exactly as the legacy module
* did. Making it lazy would change *when* `fs` is consulted relative to a
* suite's vi.mock setup, and so could shift `is_docker` in checkVersion
* payloads.
*/
export const isDocker = (() => {
try {
return (
fs.existsSync('/.dockerenv') ||
(fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf8').includes('docker'))
);
} catch {
return false;
}
})();
export interface VersionInfo {
current: string;
latest: string;
update_available: boolean;
release_url?: string;
is_docker: boolean;
is_prerelease: boolean;
}
const VERSION_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
let _versionCache: { data: VersionInfo; expiresAt: number } | null = null;
/** Cached payload while inside the TTL, otherwise null. */
export function readVersionCache(): VersionInfo | null {
if (_versionCache && Date.now() < _versionCache.expiresAt) return _versionCache.data;
return null;
}
/** `ttl` overrides the 5-minute default — failed checks cache on a shorter one. */
export function writeVersionCache(data: VersionInfo, ttl: number = VERSION_CACHE_TTL): void {
_versionCache = { data, expiresAt: Date.now() + ttl };
}
/** Test-only: clear the in-memory version cache. */
export function __clearVersionCacheForTests(): void {
_versionCache = null;
}