- Admin -> Privacy (POPIA): edit the privacy notice (built-in text as the
start, placeholders for org/IO/operators), drafts, preview, numbered
versions; earlier versions readable on /legal/privacy?version=N
- "Significant change" versions are accepted once after login
(NoticePrompt); sign-up records the version accepted
- Operator register (seeded with the services the app uses), listed in
the notice via {operators}
- Consent log (news opt-ins, notice acceptances, parental consent) and
access log (report exports, registers, certificates, group lists, form
answers, consent/medical lists, backups, impersonation)
- Information Officer on the contact page
- Unit tests (privacyNotice.test.js), e2e (privacy-notice.spec.ts); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
55 lines
1.8 KiB
JavaScript
55 lines
1.8 KiB
JavaScript
const path = require('path');
|
|
const fs = require('fs');
|
|
const { runBackup, listBackups, BACKUP_DIR, BACKUP_FILENAME_RE } = require('../utils/backupUtils');
|
|
const { safeErrorMessage } = require('../utils/errorUtils');
|
|
|
|
// @desc List local database backups (newest first)
|
|
// @route GET /api/backups
|
|
// @access Admin
|
|
const getBackups = async (req, res) => {
|
|
try {
|
|
const backups = await listBackups();
|
|
res.json(backups);
|
|
} catch (e) {
|
|
res.status(500).json({ message: safeErrorMessage(e) });
|
|
}
|
|
};
|
|
|
|
// @desc Manually trigger a backup now
|
|
// @route POST /api/backups/run
|
|
// @access Admin
|
|
const triggerBackup = async (req, res) => {
|
|
try {
|
|
const result = await runBackup();
|
|
res.status(201).json(result);
|
|
} catch (e) {
|
|
res.status(500).json({ message: safeErrorMessage(e) });
|
|
}
|
|
};
|
|
|
|
// @desc Download a backup file
|
|
// @route GET /api/backups/:filename/download
|
|
// @access Admin
|
|
const downloadBackup = async (req, res) => {
|
|
try {
|
|
const { filename } = req.params;
|
|
// Reject anything that isn't exactly the shape this app generates, before ever
|
|
// touching the filesystem — same posture as the event-alias path-safety fix.
|
|
if (!BACKUP_FILENAME_RE.test(filename)) {
|
|
res.status(400);
|
|
throw new Error('Invalid backup filename');
|
|
}
|
|
const filePath = path.join(BACKUP_DIR, filename);
|
|
if (!fs.existsSync(filePath)) {
|
|
res.status(404);
|
|
throw new Error('Backup not found');
|
|
}
|
|
require('../utils/accessLog').logDataAccess(req, { action: 'backup_download', what: `Database backup ${filename}` });
|
|
res.download(filePath, filename);
|
|
} catch (e) {
|
|
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(e) });
|
|
}
|
|
};
|
|
|
|
module.exports = { getBackups, triggerBackup, downloadBackup };
|