- App-layer AES-256-GCM crypto helper (src/utils/crypto.js) - Nextcloud tokens encrypted at rest; transparent migration on next use - Audio backups encrypted at rest (version byte 0x01 envelope); legacy rows still decrypt as-is until overwritten - argon2id password hashing via src/utils/passwords.js with bcrypt fallback; bcrypt hashes rehashed to argon2id on next successful login. argon2 package is optional — server keeps running with bcrypt only until npm install adds the native dep - PHI redactor for audit log details (src/utils/redact.js) — strips SSN, phone, email, DoB, long IDs; caps at 500 chars; detects note bodies - DOMPurify (cdnjs, SRI-pinned) replaces custom regex sanitizer in Learning Hub content rendering - SRI integrity hashes added for Font Awesome CSS and Chart.js - Magic-byte file-type verification on document uploads (src/utils/fileType.js) - Generic 500 error responses via src/utils/errors.js applied to nextcloud and audioBackups; full detail still logged server-side - DATA_ENCRYPTION_KEY env documented in .env.example Deploy: requires rebuild of the container image to pick up the new files and `npm install` (adds argon2). Existing users keep working because bcrypt stays available and crypto helpers pass through plaintext when the key is not yet set in dev.
84 lines
4.5 KiB
JavaScript
84 lines
4.5 KiB
JavaScript
var express = require('express');
|
|
var router = express.Router();
|
|
var axios = require('axios');
|
|
var { authMiddleware } = require('../middleware/auth');
|
|
var db = require('../db/database');
|
|
var logger = require('../utils/logger');
|
|
var cryptoUtil = require('../utils/crypto');
|
|
var { serverError } = require('../utils/errors');
|
|
|
|
router.post('/nextcloud/connect', authMiddleware, async function(req, res) {
|
|
try {
|
|
var { nextcloudUrl, username, appPassword, folder } = req.body;
|
|
if (!nextcloudUrl || !username || !appPassword) return res.status(400).json({ error: 'All fields required' });
|
|
|
|
var cleanUrl = nextcloudUrl.replace(/\/+$/, '');
|
|
var targetFolder = (folder || '/PediatricScribe').replace(/\/+$/, '');
|
|
|
|
try {
|
|
await axios({ method: 'PROPFIND', url: cleanUrl + '/remote.php/dav/files/' + username + '/', auth: { username: username, password: appPassword }, headers: { Depth: '0' } });
|
|
} catch (e) {
|
|
return res.status(400).json({ error: 'Cannot connect. Check credentials.' });
|
|
}
|
|
|
|
var parts = targetFolder.split('/').filter(Boolean);
|
|
var current = '';
|
|
for (var i = 0; i < parts.length; i++) {
|
|
current += '/' + parts[i];
|
|
try { await axios({ method: 'MKCOL', url: cleanUrl + '/remote.php/dav/files/' + username + current + '/', auth: { username: username, password: appPassword } }); } catch (e) {}
|
|
}
|
|
|
|
await db.run('UPDATE users SET nextcloud_url = ?, nextcloud_user = ?, nextcloud_token = ?, nextcloud_folder = ? WHERE id = ?',
|
|
[cleanUrl, username, cryptoUtil.encryptString(appPassword), targetFolder, req.user.id]);
|
|
|
|
res.json({ success: true, message: 'Connected! Files saved to ' + targetFolder + '/' });
|
|
logger.audit(req.user.id, 'nextcloud_connect', 'Connected Nextcloud', req, { category: 'integration' });
|
|
} catch (err) { return serverError(res, 'Nextcloud connect', err, 'Could not connect to Nextcloud'); }
|
|
});
|
|
|
|
router.post('/nextcloud/export', authMiddleware, async function(req, res) {
|
|
try {
|
|
var { content, filename, type } = req.body;
|
|
var user = await db.get('SELECT nextcloud_url, nextcloud_user, nextcloud_token, nextcloud_folder FROM users WHERE id = ?', [req.user.id]);
|
|
if (!user || !user.nextcloud_url) return res.status(400).json({ error: 'Nextcloud not connected. Go to Settings.' });
|
|
|
|
var baseFolder = user.nextcloud_folder || '/PediatricScribe';
|
|
var today = new Date().toISOString().split('T')[0];
|
|
var targetPath = baseFolder + '/' + today;
|
|
|
|
var pw;
|
|
try { pw = cryptoUtil.decryptString(user.nextcloud_token); }
|
|
catch (decErr) { return res.status(400).json({ error: 'Nextcloud credentials invalid. Please reconnect.' }); }
|
|
|
|
var parts = targetPath.split('/').filter(Boolean);
|
|
var current = '';
|
|
for (var i = 0; i < parts.length; i++) {
|
|
current += '/' + parts[i];
|
|
try { await axios({ method: 'MKCOL', url: user.nextcloud_url + '/remote.php/dav/files/' + user.nextcloud_user + current + '/', auth: { username: user.nextcloud_user, password: pw } }); } catch (e) {}
|
|
}
|
|
|
|
var time = new Date().toTimeString().split(' ')[0].replace(/:/g, '-');
|
|
var safeName = (filename || type + '-' + time).replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
var filePath = user.nextcloud_url + '/remote.php/dav/files/' + user.nextcloud_user + targetPath + '/' + safeName + '.txt';
|
|
|
|
await axios({ method: 'PUT', url: filePath, data: content, auth: { username: user.nextcloud_user, password: pw }, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
|
|
|
// Migrate legacy plaintext token to encrypted form on first successful use.
|
|
if (!cryptoUtil.isEncrypted(user.nextcloud_token)) {
|
|
db.run('UPDATE users SET nextcloud_token = ? WHERE id = ?', [cryptoUtil.encryptString(pw), req.user.id]).catch(function(){});
|
|
}
|
|
|
|
res.json({ success: true, message: 'Saved to ' + targetPath + '/' + safeName + '.txt' });
|
|
logger.audit(req.user.id, 'nextcloud_export', 'Exported note to Nextcloud', req, { category: 'export' });
|
|
} catch (err) { return serverError(res, 'Nextcloud export', err, 'Could not export to Nextcloud'); }
|
|
});
|
|
|
|
router.post('/nextcloud/disconnect', authMiddleware, async function(req, res) {
|
|
try {
|
|
await db.run('UPDATE users SET nextcloud_url = NULL, nextcloud_user = NULL, nextcloud_token = NULL, nextcloud_folder = NULL WHERE id = ?', [req.user.id]);
|
|
res.json({ success: true });
|
|
logger.audit(req.user.id, 'nextcloud_disconnect', 'Disconnected Nextcloud', req, { category: 'integration' });
|
|
} catch (err) { return serverError(res, 'Nextcloud disconnect', err, 'Could not disconnect'); }
|
|
});
|
|
|
|
module.exports = router;
|