alotta good shit

This commit is contained in:
2026-09-19 06:20:37 +02:00
parent 74f7884525
commit 1477d56658
45 changed files with 4363 additions and 2069 deletions
+201
View File
@@ -0,0 +1,201 @@
import cfg from "./inc/config.mjs";
import path from "path";
import { promises as fs } from "fs";
import db from "./inc/sql.mjs";
import lib from "./inc/lib.mjs";
import { parseMultipart, collectBody } from "./inc/multipart.mjs";
import { execFile as _execFile } from "child_process";
import { promisify } from "util";
import audit from "./inc/audit.mjs";
import { getBrandImageUrl, setBrandImageUrl } from "./inc/settings.mjs";
const execFile = promisify(_execFile);
const sendJson = (res, data, code = 200) => {
const body = JSON.stringify(data);
res.writeHead(code, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }).end(body);
};
/** Shared admin session + CSRF lookup */
async function getAdminSession(req, res) {
if (!req.cookies || !req.cookies.session) {
sendJson(res, { success: false, msg: 'Unauthorized' }, 401);
return null;
}
const user = await db`
SELECT "user".id, "user".login, "user".user, "user".admin,
"user_sessions".id AS sess_id, "user_sessions".csrf_token
FROM "user_sessions"
LEFT JOIN "user" ON "user".id = "user_sessions".user_id
WHERE "user_sessions".session = ${lib.sha256(req.cookies.session)}
LIMIT 1
`;
if (user.length === 0 || !user[0].admin) {
sendJson(res, { success: false, msg: 'Unauthorized' }, 401);
return null;
}
const session = user[0];
// CSRF validation via header
if (session.csrf_token) {
const csrfToken = req.headers['x-csrf-token'];
if (!csrfToken || csrfToken !== session.csrf_token) {
console.warn(`[CSRF] Blocked brand image request for user ${session.user}. Invalid token.`);
sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
return null;
}
}
return session;
}
/** Delete the physical file for a stored brand image URL (if any) */
async function deleteOldBrandFile() {
const current = getBrandImageUrl();
if (!current) return;
// Strip query string to get the bare filename
const urlPath = current.split('?')[0];
// Only delete files that live under our navbar img dir
if (!urlPath.startsWith('/s/img/navbar/brand.')) return;
const filename = path.basename(urlPath);
const filePath = path.join(cfg.paths.s, 'img', 'navbar', filename);
await fs.unlink(filePath).catch(() => {});
}
/** Persist brand image URL to site_settings DB and in-memory setting */
async function persistBrandImage(url) {
setBrandImageUrl(url);
await db`
INSERT INTO site_settings (key, value)
VALUES ('brand_image_url', ${url})
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
`;
}
// ── Upload ─────────────────────────────────────────────────────────────────
export const handleBrandImageUpload = async (req, res) => {
console.log('[BRAND UPLOAD] Started');
const session = await getAdminSession(req, res);
if (!session) return;
try {
const contentType = req.headers['content-type'] || '';
const boundaryMatch = contentType.match(/boundary=(.+)$/);
if (!boundaryMatch) {
return sendJson(res, { success: false, msg: 'Invalid content type — multipart boundary missing' }, 400);
}
const body = await collectBody(req, 5 * 1024 * 1024); // 5 MB hard cap
const parts = parseMultipart(body, boundaryMatch[1]);
const file = parts.file;
if (!file || !file.data || file.data.length === 0) {
return sendJson(res, { success: false, msg: 'No file provided' }, 400);
}
// 2 MB soft cap
const maxSize = 2 * 1024 * 1024;
if (file.data.length > maxSize) {
return sendJson(res, {
success: false,
msg: `File too large. Maximum is 2 MB, got ${(file.data.length / 1024 / 1024).toFixed(2)} MB`
}, 400);
}
const allowedMimes = ['image/gif', 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/svg+xml'];
const mime = (file.contentType || '').toLowerCase().split(';')[0].trim();
if (!allowedMimes.includes(mime)) {
return sendJson(res, {
success: false,
msg: `Invalid file type. Allowed: gif, jpg, png, webp, svg. Got: ${mime}`
}, 400);
}
const imgDir = path.join(cfg.paths.s, 'img', 'navbar');
await fs.mkdir(imgDir, { recursive: true });
await fs.mkdir(cfg.paths.tmp, { recursive: true });
const isSvg = mime === 'image/svg+xml';
const ts = Date.now();
// Timestamp baked into the filename — every upload is a unique file
const outFilename = isSvg ? `brand.${ts}.svg` : `brand.${ts}.webp`;
const tmpPath = path.join(cfg.paths.tmp, `brand_tmp_${ts}`);
const finalPath = path.join(imgDir, outFilename);
await fs.writeFile(tmpPath, file.data);
if (!isSvg) {
// Verify actual MIME with file(1)
try {
const { stdout: actualMime } = await execFile('file', ['--mime-type', '-b', tmpPath]);
const safeActual = ['image/gif', 'image/jpeg', 'image/png', 'image/webp'];
if (!safeActual.includes(actualMime.trim())) {
await fs.unlink(tmpPath).catch(() => {});
return sendJson(res, { success: false, msg: `Invalid file type detected: ${actualMime.trim()}` }, 400);
}
} catch (_) {
// file(1) not available — skip magic check
}
// Convert to WebP via ImageMagick
try {
await execFile('magick', [tmpPath, '-coalesce', '-quality', '85', finalPath]);
} catch (err) {
console.error('[BRAND UPLOAD] ImageMagick error:', err);
await fs.unlink(tmpPath).catch(() => {});
return sendJson(res, { success: false, msg: 'Failed to process image (ImageMagick required)' }, 500);
}
} else {
// SVG: copy as-is
await fs.copyFile(tmpPath, finalPath);
}
await fs.unlink(tmpPath).catch(() => {});
// Delete the previous brand file from disk
await deleteOldBrandFile();
const publicUrl = `/s/img/navbar/${outFilename}`;
await persistBrandImage(publicUrl);
await db`SELECT pg_notify('brand_image', ${JSON.stringify({ url: publicUrl })})`.catch(() => {});
await audit.log(session.id, 'update_brand_image', 'system', 0, { url: publicUrl });
console.log('[BRAND UPLOAD] Done:', publicUrl);
return sendJson(res, { success: true, url: publicUrl, msg: 'Brand image updated' });
} catch (err) {
if (err.code === 'BODY_TOO_LARGE') {
return sendJson(res, { success: false, msg: 'File too large (5 MB max)' }, 413);
}
console.error('[BRAND UPLOAD ERROR]', err);
return sendJson(res, { success: false, msg: 'Upload failed: ' + err.message }, 500);
}
};
// ── Delete ─────────────────────────────────────────────────────────────────
export const handleBrandImageDelete = async (req, res) => {
console.log('[BRAND DELETE] Started');
const session = await getAdminSession(req, res);
if (!session) return;
try {
// Delete the physical file before clearing the setting
await deleteOldBrandFile();
await persistBrandImage('');
await db`SELECT pg_notify('brand_image', ${JSON.stringify({ url: null })})`.catch(() => {});
await audit.log(session.id, 'delete_brand_image', 'system', 0, {});
console.log('[BRAND DELETE] Done');
return sendJson(res, { success: true, msg: 'Brand image removed' });
} catch (err) {
console.error('[BRAND DELETE ERROR]', err);
return sendJson(res, { success: false, msg: 'Delete failed: ' + err.message }, 500);
}
};