fix(shorts): плановая очистка, бэкап, часовой пояс и экранирование
- purgeScheduledDeletions() теперь чистит short_messages по purge_at - short_messages добавлена в BACKUP_TABLES/BACKUP_SEQUENCE_TABLES, normalizeRestoreData и restore (поле автора проверяется на живость) - сид short_messages_retention_days перенесён и в db/init.sql - used_at по умолчанию берётся в зоне приложения (appTodayIso), а не по UTC - поле «Использовано» скрыто при создании, открыто при правке - удалён пустой escInline и мёртвые data-message/canEdit: сырой текст сообщения попадал в атрибут неэкранированным
This commit is contained in:
@@ -1329,6 +1329,12 @@ async function appTimezone() {
|
||||
return validTimezone(v) ? v : DEFAULT_TIMEZONE;
|
||||
}
|
||||
|
||||
async function appTodayIso() {
|
||||
const tz = await appTimezone();
|
||||
const { rows } = await pool.query('SELECT to_char(now() AT TIME ZONE $1, \'YYYY-MM-DD\') AS d', [tz]);
|
||||
return rows[0].d;
|
||||
}
|
||||
|
||||
async function appHour12() {
|
||||
return (await getSetting('time_format', '24h')) === '12h';
|
||||
}
|
||||
@@ -1362,12 +1368,15 @@ async function purgeScheduledDeletions() {
|
||||
publishChat({ type: 'thread_update', thread_id: c.id, tutor_id: null, deleted: true });
|
||||
}
|
||||
}
|
||||
if (erefs.rowCount || gcount || ccount) {
|
||||
const sres = await pool.query('DELETE FROM short_messages WHERE purge_at IS NOT NULL AND purge_at <= now()');
|
||||
const scount = sres.rowCount;
|
||||
if (erefs.rowCount || gcount || ccount || scount) {
|
||||
invalidateEntries();
|
||||
invalidateGroups();
|
||||
invalidateStats();
|
||||
}
|
||||
return { entries: erefs.rowCount, groups: gcount, chat_threads: ccount };
|
||||
if (scount) invalidateShorts();
|
||||
return { entries: erefs.rowCount, groups: gcount, chat_threads: ccount, short_messages: scount };
|
||||
}
|
||||
|
||||
function safeUnlink(relPath) {
|
||||
@@ -2667,7 +2676,7 @@ app.put('/api/shorts/:id', requireAuth, async (req, res) => {
|
||||
if (usedAt && !/^\d{4}-\d{2}-\d{2}$/.test(usedAt)) {
|
||||
return res.status(400).json({ error: 'used_at должен быть в формате YYYY-MM-DD' });
|
||||
}
|
||||
if (used && !usedAt) usedAt = new Date().toISOString().slice(0, 10);
|
||||
if (used && !usedAt) usedAt = await appTodayIso();
|
||||
if (usedAt) used = true;
|
||||
if (!used) usedAt = null;
|
||||
if (lessonDate && !/^\d{4}-\d{2}-\d{2}$/.test(lessonDate)) {
|
||||
@@ -3443,7 +3452,7 @@ function sweepBackupStorage() {
|
||||
async function buildBackupArchive() {
|
||||
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'wido-bk-'));
|
||||
try {
|
||||
const [g, s, e, st, pf, br, us, ub, gp, ep, md, sp, sl, pj, lr, lrv, al, nt, nr, bi, ct, cm, ca] = await Promise.all([
|
||||
const [g, s, e, st, pf, br, us, ub, gp, ep, md, sp, sl, pj, lr, lrv, al, nt, nr, bi, ct, cm, ca, sm] = await Promise.all([
|
||||
pool.query('SELECT * FROM groups ORDER BY id'),
|
||||
pool.query('SELECT * FROM students ORDER BY id'),
|
||||
pool.query('SELECT * FROM entries ORDER BY id'),
|
||||
@@ -3467,6 +3476,7 @@ async function buildBackupArchive() {
|
||||
pool.query('SELECT * FROM chat_threads ORDER BY id'),
|
||||
pool.query('SELECT * FROM chat_messages ORDER BY id'),
|
||||
pool.query('SELECT * FROM chat_attachments ORDER BY id'),
|
||||
pool.query('SELECT * FROM short_messages ORDER BY id'),
|
||||
]);
|
||||
const settings = {};
|
||||
st.rows.forEach(r => { settings[r.key] = r.value; });
|
||||
@@ -3482,8 +3492,9 @@ async function buildBackupArchive() {
|
||||
lesson_report_versions: lrv.rowCount, audit_log: al.rowCount, notifications: nt.rowCount,
|
||||
notification_reads: nr.rowCount, banned_ips: bi.rowCount,
|
||||
chat_threads: ct.rowCount, chat_messages: cm.rowCount, chat_attachments: ca.rowCount,
|
||||
short_messages: sm.rowCount,
|
||||
},
|
||||
groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows, modules: md.rows, student_photos: sp.rows, share_links: sl.rows, photo_jobs: pj.rows, lesson_reports: lr.rows, lesson_report_versions: lrv.rows, audit_log: al.rows, notifications: nt.rows, notification_reads: nr.rows, banned_ips: bi.rows, chat_threads: ct.rows, chat_messages: cm.rows, chat_attachments: ca.rows,
|
||||
groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows, modules: md.rows, student_photos: sp.rows, share_links: sl.rows, photo_jobs: pj.rows, lesson_reports: lr.rows, lesson_report_versions: lrv.rows, audit_log: al.rows, notifications: nt.rows, notification_reads: nr.rows, banned_ips: bi.rows, chat_threads: ct.rows, chat_messages: cm.rows, chat_attachments: ca.rows, short_messages: sm.rows,
|
||||
};
|
||||
fs.writeFileSync(path.join(staging, 'data.json'), JSON.stringify(payload));
|
||||
const files = await storage.downloadAll(path.join(staging, 'uploads'));
|
||||
@@ -3649,6 +3660,7 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
||||
await client.query('DELETE FROM chat_attachments');
|
||||
await client.query('DELETE FROM chat_messages');
|
||||
await client.query('DELETE FROM chat_threads');
|
||||
await client.query('DELETE FROM short_messages');
|
||||
await client.query('DELETE FROM project_files');
|
||||
await client.query('DELETE FROM lesson_report_versions');
|
||||
await client.query('DELETE FROM lesson_reports');
|
||||
@@ -3802,6 +3814,13 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
||||
[x.id, x.message_id, x.token, x.path, x.name, x.size || 0, x.created_at]
|
||||
);
|
||||
}
|
||||
for (const x of ndata.short_messages) {
|
||||
const okUser = x.author_id == null || (await client.query('SELECT 1 FROM users WHERE id = $1', [x.author_id])).rowCount;
|
||||
await client.query(
|
||||
'INSERT INTO short_messages (id, resident, lesson_date, topic, message, used, used_at, author_id, deleted_at, purge_at, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)',
|
||||
[x.id, x.resident, x.lesson_date, x.topic, x.message, x.used, x.used_at, okUser ? x.author_id : null, x.deleted_at, x.purge_at, x.created_at, x.updated_at]
|
||||
);
|
||||
}
|
||||
for (const x of ndata.audit_log) {
|
||||
const okUser = x.user_id == null || (await client.query('SELECT 1 FROM users WHERE id = $1', [x.user_id])).rowCount;
|
||||
await client.query(
|
||||
|
||||
Reference in New Issue
Block a user