// Workspace (white-label): the agency's name, logo, brand colour and support email. // Used in the app (sidebar, sign-in), client approval pages, emails and the PDF reports. import fs from "node:fs"; import path from "node:path"; import { DATA_DIR, AGENCY_NAME } from "./config.js"; const FILE = path.join(DATA_DIR, "workspace.json"); const LOGO = path.join(DATA_DIR, "workspace-logo"); const HEX = /^#[0-9a-f]{6}$/i; const err = (msg, status = 400) => Object.assign(new Error(msg), { status }); const read = () => { try { return JSON.parse(fs.readFileSync(FILE, "utf8")); } catch { return {}; } }; export function get() { const w = read(); return { name: w.name || "", accent: w.accent || "", supportEmail: w.supportEmail || "", website: w.website || "", hasLogo: Boolean(logoPath()), updatedAt: w.updatedAt || null }; } /** The name shown to people: the workspace name, else AGENCY_NAME from .env. */ export const agencyName = () => read().name || AGENCY_NAME; export function save(patch, by) { const cur = read(); const next = { ...cur }; if (patch.name !== undefined) next.name = String(patch.name).trim().slice(0, 60); if (patch.accent !== undefined) { if (patch.accent && !HEX.test(patch.accent)) throw err("Brand colour must be like #242142."); next.accent = patch.accent || ""; } if (patch.supportEmail !== undefined) { const e = String(patch.supportEmail).trim(); if (e && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(e)) throw err("That email doesn't look right."); next.supportEmail = e; } if (patch.website !== undefined) { const u = String(patch.website).trim(); if (u && !/^https?:\/\//i.test(u)) throw err("Website must start with https://"); next.website = u; } next.updatedAt = new Date().toISOString(); next.updatedBy = by; fs.writeFileSync(FILE, JSON.stringify(next, null, 1)); return get(); } export function logoPath() { for (const ext of [".png", ".jpg"]) if (fs.existsSync(LOGO + ext)) return LOGO + ext; return null; } /** Save an uploaded logo (PNG or JPG, up to 2 MB). */ export function saveLogo(buf, filename) { if (!Buffer.isBuffer(buf) || !buf.length) throw err("Choose an image."); if (buf.length > 2 * 1024 * 1024) throw err("Logo must be 2 MB or smaller."); const png = buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])); const jpg = buf[0] === 0xff && buf[1] === 0xd8; if (!png && !jpg) throw err("Logo must be a PNG or JPG."); removeLogo(); fs.writeFileSync(LOGO + (png ? ".png" : ".jpg"), buf); return get(); } export function removeLogo() { for (const ext of [".png", ".jpg"]) fs.rmSync(LOGO + ext, { force: true }); } /** Options for tools/report-pdf toPdf(): the agency's logo when one is set. */ export const reportOptions = () => (logoPath() ? { logoPath: logoPath(), logoAlt: agencyName() } : {});