feat(branding): system name/logo, horizontal logo on all pages
This commit is contained in:
@@ -616,7 +616,7 @@ async function removeEntryFiles(entryId) {
|
||||
async function sweepOrphanedUploads() {
|
||||
const dir = path.join(__dirname, 'uploads');
|
||||
if (!fs.existsSync(dir)) return;
|
||||
const [{ rows: photos }, { rows: files }, { rows: gphotos }, { rows: ephotos }, { rows: pendingJobs }, { rows: mphotos }, { rows: sphotos }] = await Promise.all([
|
||||
const [{ rows: photos }, { rows: files }, { rows: gphotos }, { rows: ephotos }, { rows: pendingJobs }, { rows: mphotos }, { rows: sphotos }, { rows: logos }] = await Promise.all([
|
||||
pool.query('SELECT photo_path AS p FROM entries WHERE photo_path IS NOT NULL'),
|
||||
pool.query('SELECT path AS p FROM project_files'),
|
||||
pool.query('SELECT photo_path AS p FROM group_photos'),
|
||||
@@ -624,9 +624,10 @@ async function sweepOrphanedUploads() {
|
||||
pool.query(`SELECT after_path AS p FROM photo_jobs WHERE status = 'done' AND applied = false AND after_path IS NOT NULL`),
|
||||
pool.query('SELECT photo_path AS p FROM modules WHERE photo_path IS NOT NULL'),
|
||||
pool.query('SELECT photo_path AS p FROM student_photos'),
|
||||
pool.query(`SELECT value AS p FROM settings WHERE key = 'system_logo' AND value IS NOT NULL AND value <> ''`),
|
||||
]);
|
||||
const refs = new Set();
|
||||
[...photos, ...files, ...gphotos, ...ephotos, ...pendingJobs, ...mphotos, ...sphotos].forEach(r => refs.add('/' + String(r.p).replace(/^\/+/, '')));
|
||||
[...photos, ...files, ...gphotos, ...ephotos, ...pendingJobs, ...mphotos, ...sphotos, ...logos].forEach(r => refs.add('/' + String(r.p).replace(/^\/+/, '')));
|
||||
for (const f of fs.readdirSync(dir)) {
|
||||
const fp = path.join(dir, f);
|
||||
if (!fs.statSync(fp).isFile()) continue;
|
||||
@@ -1037,8 +1038,8 @@ app.get('/api/settings', requireAdmin, async (_, res) => {
|
||||
|
||||
app.get('/api/public-settings', apiLimiter, async (_, res) => {
|
||||
const out = await cacheWrap('public-settings', PUBLIC_TTL_MS, async () => {
|
||||
const keys = ['footer_left', 'footer_right', 'share_show_student_message', 'share_show_entry_date', 'share_show_student_names', 'share_show_group_photos', 'cookie_notice_text', 'spam_interval_min', 'photo_capture_resolution', 'photo_capture_quality', 'photo_enhance_engine'];
|
||||
const defaults = { spam_interval_min: '30', photo_capture_resolution: '640x480', photo_capture_quality: '0.92', photo_enhance_engine: 'auto' };
|
||||
const keys = ['system_name', 'system_logo', 'footer_left', 'footer_right', 'share_show_student_message', 'share_show_entry_date', 'share_show_student_names', 'share_show_group_photos', 'cookie_notice_text', 'spam_interval_min', 'photo_capture_resolution', 'photo_capture_quality', 'photo_enhance_engine'];
|
||||
const defaults = { system_name: 'WhatIDo', system_logo: '', spam_interval_min: '30', photo_capture_resolution: '640x480', photo_capture_quality: '0.92', photo_enhance_engine: 'auto' };
|
||||
const result = {};
|
||||
for (const k of keys) result[k] = await getSetting(k, defaults[k] || '');
|
||||
const rm = /^(\d{2,5})x(\d{2,5})$/.exec(result.photo_capture_resolution);
|
||||
@@ -1081,6 +1082,12 @@ app.put('/api/settings', requireAdmin, async (req, res) => {
|
||||
if (key === 'photo_enhance_engine' && !['auto', 'server', 'client'].includes(String(value))) {
|
||||
return res.status(400).json({ error: 'photo_enhance_engine должен быть auto, server или client' });
|
||||
}
|
||||
if (key === 'system_name' && String(value).trim().length > 60) {
|
||||
return res.status(400).json({ error: 'system_name не может быть длиннее 60 символов' });
|
||||
}
|
||||
if (key === 'system_logo' && String(value) !== '' && !isSafeUploadPath(String(value))) {
|
||||
return res.status(400).json({ error: 'system_logo — некорректный путь' });
|
||||
}
|
||||
}
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
@@ -1107,6 +1114,54 @@ app.put('/api/settings', requireAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const logoUpload = upload.single('logo');
|
||||
app.post('/api/settings/logo', requireAdmin, (req, res, next) => {
|
||||
logoUpload(req, res, (err) => {
|
||||
if (err) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') return res.status(400).json({ error: 'Файл слишком большой (макс. 10 МБ)' });
|
||||
if (err.message === 'Only images') return res.status(400).json({ error: 'Логотип: допустимы только изображения (jpg, png, gif, webp, bmp, avif, ico, heic, heif)' });
|
||||
if (err.message === 'Not allowed extension') return res.status(400).json({ error: 'Недопустимый тип файла' });
|
||||
return res.status(400).json({ error: 'Недопустимый файл' });
|
||||
}
|
||||
next();
|
||||
});
|
||||
}, async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'Файл обязателен' });
|
||||
const ext = (path.extname(req.file.originalname || '') || '').toLowerCase();
|
||||
if (!ALLOWED_IMAGE_EXT.has(ext)) {
|
||||
removeUpload(req.file);
|
||||
return res.status(400).json({ error: 'Логотип: допустимы только изображения (jpg, png, gif, webp, bmp, avif, ico, heic, heif, jfif)' });
|
||||
}
|
||||
try {
|
||||
await convertPhoto(req.file);
|
||||
const finalPath = `/uploads/${req.file.filename}`;
|
||||
const old = await getSetting('system_logo', '');
|
||||
if (old && isSafeUploadPath(old) && old !== finalPath) safeUnlink(old);
|
||||
await pool.query(
|
||||
`INSERT INTO settings (key, value) VALUES ('system_logo', $1)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`,
|
||||
[finalPath]
|
||||
);
|
||||
await logAudit(req, 'settings.logo.upload', { path: finalPath });
|
||||
invalidateSettings();
|
||||
res.json({ system_logo: finalPath });
|
||||
} catch (e) {
|
||||
removeUpload(req.file);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/settings/logo', requireAdmin, async (req, res) => {
|
||||
const old = await getSetting('system_logo', '');
|
||||
if (old) {
|
||||
await pool.query(`UPDATE settings SET value = '' WHERE key = 'system_logo'`);
|
||||
if (isSafeUploadPath(old)) safeUnlink(old);
|
||||
}
|
||||
await logAudit(req, 'settings.logo.remove', {});
|
||||
invalidateSettings();
|
||||
res.json({ ok: true, system_logo: '' });
|
||||
});
|
||||
|
||||
app.get('/api/audit', requireAdmin, async (req, res) => {
|
||||
const limit = Math.min(parseInt(req.query.limit, 10) || 100, 1000);
|
||||
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
|
||||
|
||||
Reference in New Issue
Block a user