This commit is contained in:
2026-03-20 16:30:45 +07:00
parent e51f34d7be
commit d935b7374d
11 changed files with 438 additions and 31 deletions

View File

@@ -4,6 +4,7 @@ const { Pool } = require('pg');
const session = require('express-session');
const cors = require('cors');
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const {
ALLOWED_SQL_TYPES,
canAccessFolder,
@@ -34,10 +35,17 @@ const {
createBackup,
getBackupPath,
listBackups,
pruneBackups,
} = require('./src/services/backups');
const {
notifyError,
notifyInfo,
} = require('./src/services/notifications');
const {
getSettings,
saveSettings,
validateSettings,
} = require('./src/services/runtime-config');
const app = express();
@@ -103,6 +111,45 @@ function applyRecordMetadata(structure, payload, currentUser, { isCreate = false
return data;
}
let lastAutoBackupSlot = '';
async function runScheduledBackupIfNeeded() {
const settings = getSettings();
if (!settings.backups.enabled) {
return;
}
const now = new Date();
if (now.getHours() !== settings.backups.hour || now.getMinutes() !== settings.backups.minute) {
return;
}
const slot = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}-${String(settings.backups.hour).padStart(2, '0')}-${String(settings.backups.minute).padStart(2, '0')}`;
if (lastAutoBackupSlot === slot) {
return;
}
const backup = await createBackup(pool, 'system', {
includeAppSnapshot: settings.backups.includeAppSnapshot,
keepLast: settings.backups.keepLast,
});
lastAutoBackupSlot = slot;
appendAudit('backup.auto_created', 'system', {
files: backup.files,
source: 'WEB',
});
notifyInfo('Scheduled backup completed', backup.files.map((file) => `${file.kind}: ${file.filename}`)).catch(() => {});
}
function startBackupScheduler() {
setInterval(() => {
runScheduledBackupIfNeeded().catch((error) => {
notifyError('Scheduled backup failed', error).catch(() => {});
});
}, 60000);
}
// Helper: get primary key column for a table (returns null if none)
async function getPrimaryKeyColumn(tableName) {
const result = await pool.query(`
@@ -353,8 +400,12 @@ app.post('/api/backups', requireAuth, requirePermission(
'Backup access denied'
), async (req, res) => {
try {
const backup = await createBackup(pool, req.currentUser.username);
appendAudit('backup.created', req.currentUser.username, { filename: backup.filename, source: getAuditSource(req) });
const settings = getSettings();
const backup = await createBackup(pool, req.currentUser.username, {
includeAppSnapshot: settings.backups.includeAppSnapshot,
keepLast: settings.backups.keepLast,
});
appendAudit('backup.created', req.currentUser.username, { files: backup.files, source: getAuditSource(req) });
res.json({ success: true, backup });
} catch (err) {
notifyError('Backup creation failed', err, { actor: req.currentUser.username }).catch(() => {});
@@ -362,6 +413,28 @@ app.post('/api/backups', requireAuth, requirePermission(
}
});
app.get('/api/settings', requireAuth, requirePermission(
(permissions) => permissions.canManageUsers,
'Settings access denied'
), (req, res) => {
res.json(getSettings());
});
app.put('/api/settings', requireAuth, requirePermission(
(permissions) => permissions.canManageUsers,
'Settings access denied'
), (req, res) => {
try {
const settings = validateSettings(req.body);
const saved = saveSettings(settings);
pruneBackups(saved.backups.keepLast);
appendAudit('settings.updated', req.currentUser.username, { source: getAuditSource(req) });
res.json({ success: true, settings: saved });
} catch (err) {
res.status(400).json({ success: false, error: err.message });
}
});
app.get('/api/backups/:filename/download', requireAuth, requirePermission(
(permissions) => permissions.canManageUsers,
'Backup access denied'
@@ -943,4 +1016,4 @@ app.listen(PORT, () => {
console.log('📝 Make sure to configure your database in .env file');
});
startBackupScheduler();