169 lines
4.4 KiB
JavaScript
169 lines
4.4 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const BRANDING_DIR = path.join(__dirname, '../Data/branding');
|
|
const BRANDING_FILE = path.join(BRANDING_DIR, 'branding.json');
|
|
const FAVICON_BASENAME = 'favicon';
|
|
|
|
const DEFAULTS = {
|
|
title: 'SupportBot',
|
|
pageTitle: 'SupportBot Dashboard',
|
|
faviconFile: null,
|
|
faviconMime: null,
|
|
updatedAt: null,
|
|
};
|
|
|
|
const ALLOWED_MIME = new Set([
|
|
'image/png',
|
|
'image/jpeg',
|
|
'image/x-icon',
|
|
'image/vnd.microsoft.icon',
|
|
'image/svg+xml',
|
|
'image/webp',
|
|
]);
|
|
|
|
const MIME_EXT = {
|
|
'image/png': '.png',
|
|
'image/jpeg': '.jpg',
|
|
'image/x-icon': '.ico',
|
|
'image/vnd.microsoft.icon': '.ico',
|
|
'image/svg+xml': '.svg',
|
|
'image/webp': '.webp',
|
|
};
|
|
|
|
function ensureDir() {
|
|
if (!fs.existsSync(BRANDING_DIR)) {
|
|
fs.mkdirSync(BRANDING_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
function readBranding() {
|
|
ensureDir();
|
|
if (!fs.existsSync(BRANDING_FILE)) {
|
|
return { ...DEFAULTS };
|
|
}
|
|
try {
|
|
const raw = JSON.parse(fs.readFileSync(BRANDING_FILE, 'utf8'));
|
|
return {
|
|
title: String(raw.title || DEFAULTS.title).slice(0, 64),
|
|
pageTitle: String(raw.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
|
faviconFile: raw.faviconFile || null,
|
|
faviconMime: raw.faviconMime || null,
|
|
updatedAt: raw.updatedAt || null,
|
|
};
|
|
} catch {
|
|
return { ...DEFAULTS };
|
|
}
|
|
}
|
|
|
|
function writeBranding(data) {
|
|
ensureDir();
|
|
const next = {
|
|
title: String(data.title || DEFAULTS.title).slice(0, 64),
|
|
pageTitle: String(data.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
|
faviconFile: data.faviconFile ?? null,
|
|
faviconMime: data.faviconMime ?? null,
|
|
updatedAt: Date.now(),
|
|
};
|
|
fs.writeFileSync(BRANDING_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
return next;
|
|
}
|
|
|
|
function publicPayload(branding) {
|
|
const hasFavicon = Boolean(branding.faviconFile);
|
|
return {
|
|
title: branding.title,
|
|
pageTitle: branding.pageTitle,
|
|
hasFavicon,
|
|
faviconUrl: hasFavicon ? '/api/branding/favicon' : null,
|
|
updatedAt: branding.updatedAt,
|
|
};
|
|
}
|
|
|
|
function updateBranding({ title, pageTitle }) {
|
|
const current = readBranding();
|
|
return writeBranding({
|
|
...current,
|
|
title: title !== undefined ? title : current.title,
|
|
pageTitle: pageTitle !== undefined ? pageTitle : current.pageTitle,
|
|
});
|
|
}
|
|
|
|
function removeExistingFavicons() {
|
|
if (!fs.existsSync(BRANDING_DIR)) return;
|
|
for (const file of fs.readdirSync(BRANDING_DIR)) {
|
|
if (file.startsWith(`${FAVICON_BASENAME}.`)) {
|
|
fs.unlinkSync(path.join(BRANDING_DIR, file));
|
|
}
|
|
}
|
|
}
|
|
|
|
function saveFaviconFromBase64(dataUrl) {
|
|
const match = String(dataUrl || '').match(
|
|
/^data:([\w/+.-]+);base64,([A-Za-z0-9+/=]+)$/,
|
|
);
|
|
if (!match) {
|
|
throw new Error('Invalid image data. Upload a PNG, ICO, SVG, or WebP file.');
|
|
}
|
|
|
|
const mime = match[1];
|
|
if (!ALLOWED_MIME.has(mime)) {
|
|
throw new Error('Unsupported image type. Use PNG, ICO, SVG, JPEG, or WebP.');
|
|
}
|
|
|
|
const buffer = Buffer.from(match[2], 'base64');
|
|
if (buffer.length > 512 * 1024) {
|
|
throw new Error('Favicon must be 512 KB or smaller.');
|
|
}
|
|
|
|
ensureDir();
|
|
removeExistingFavicons();
|
|
|
|
const ext = MIME_EXT[mime] || '.png';
|
|
const filename = `${FAVICON_BASENAME}${ext}`;
|
|
const filePath = path.join(BRANDING_DIR, filename);
|
|
fs.writeFileSync(filePath, buffer);
|
|
|
|
const current = readBranding();
|
|
return writeBranding({
|
|
...current,
|
|
faviconFile: filename,
|
|
faviconMime: mime,
|
|
});
|
|
}
|
|
|
|
function clearFavicon() {
|
|
removeExistingFavicons();
|
|
const current = readBranding();
|
|
return writeBranding({
|
|
...current,
|
|
faviconFile: null,
|
|
faviconMime: null,
|
|
});
|
|
}
|
|
|
|
function getFaviconPath() {
|
|
const branding = readBranding();
|
|
if (!branding.faviconFile) return null;
|
|
const filePath = path.join(BRANDING_DIR, branding.faviconFile);
|
|
if (!fs.existsSync(filePath)) return null;
|
|
return { filePath, mime: branding.faviconMime || 'image/png' };
|
|
}
|
|
|
|
function resetBranding() {
|
|
clearFavicon();
|
|
return writeBranding({ ...DEFAULTS });
|
|
}
|
|
|
|
module.exports = {
|
|
readBranding,
|
|
writeBranding,
|
|
publicPayload,
|
|
updateBranding,
|
|
saveFaviconFromBase64,
|
|
clearFavicon,
|
|
getFaviconPath,
|
|
resetBranding,
|
|
DEFAULTS,
|
|
};
|