← back to NationalPaperHangers
lib/utils.js
54 lines
// Tiny shared helpers used by both routes and scripts.
// Keep this file dependency-free.
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Returns a normalized http(s) URL string, or null if invalid.
function safeHttpUrl(input) {
const v = String(input || '').trim();
if (!v || v.length > 2000) return null;
let u;
try { u = new URL(v); } catch { return null; }
if (u.protocol !== 'http:' && u.protocol !== 'https:') return null;
return u.toString();
}
// Length-cap user input bound for varchar/text columns.
function clampLen(v, max) {
const s = String(v == null ? '' : v);
return s.length > max ? s.slice(0, max) : s;
}
// Parse a wall-clock time string ("H:MM", "HH:MM", or "HH:MM:SS") to its
// minute-of-day as a number, or null when malformed. Used to validate
// installer availability windows before they reach a `::time`-cast INSERT:
// a garbage value would 500 the query, and an inverted start/end pair would
// save cleanly but produce zero bookable slots (slots.js drops the invalid
// Interval), leaving the installer with no bookings and no error.
function clockMinutes(v) {
const m = /^(\d{1,2}):([0-5]\d)(?::([0-5]\d))?$/.exec(String(v == null ? '' : v).trim());
if (!m) return null;
const h = Number(m[1]);
if (h > 23) return null;
return h * 60 + Number(m[2]) + (m[3] ? Number(m[3]) / 60 : 0);
}
// Single source of truth for "is this a usable email address". Mirrors the
// shape check the booking endpoint already uses, so signup, claim, and
// booking all reject the same garbage (e.g. "@", "a@", "a@b") instead of
// letting it into the installers.email / bookings.customer_email columns.
function isValidEmail(v) {
const s = String(v == null ? '' : v).trim();
if (!s || s.length > 254) return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
}
module.exports = { escapeHtml, safeHttpUrl, clampLen, clockMinutes, isValidEmail };