simple-web-file-upload/src/webdav-config.js

85 lines
3.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const db = require('./db');
const WEBDAV_CONFIG_DIR = process.env.WEBDAV_CONFIG_DIR || '/webdav-config';
function ensureDir() {
fs.mkdirSync(WEBDAV_CONFIG_DIR, { recursive: true });
}
function atomicWrite(filepath, content) {
const tmp = filepath + '.tmp';
fs.writeFileSync(tmp, content);
fs.renameSync(tmp, filepath);
}
// Apache mod_authn_file accepts bcrypt hashes ($2a$/$2b$/$2y$),
// which is exactly what bcrypt npm produces. No re-hashing needed.
function buildHtpasswd() {
const users = db.prepare('SELECT username, password_hash FROM users ORDER BY username').all();
return users.map(u => `${u.username}:${u.password_hash}`).join('\n') + (users.length ? '\n' : '');
}
function buildAccessConf() {
const admins = db.prepare("SELECT username FROM users WHERE role = 'admin' ORDER BY username")
.all().map(r => r.username);
const customers = db.prepare('SELECT * FROM customers ORDER BY slug').all();
let out = '# Auto-generated by app — do not edit.\n\n';
// Root listing auth is already enforced by <Directory "/data/uploads"> in webdav.conf.
// Adding a broad <Location "/"> here would shadow /icons/ and break autoindex graphics.
for (const c of customers) {
const assigns = db.prepare(`
SELECT u.username, ca.access FROM customer_access ca
JOIN users u ON u.id = ca.user_id
WHERE ca.customer_id = ?
ORDER BY u.username
`).all(c.id);
const staffRead = assigns.map(a => a.username);
const staffWrite = assigns.filter(a => a.access === 'write').map(a => a.username);
const readUsers = [...new Set([...admins, ...staffRead])];
const writeUsers = [...new Set([...admins, ...staffWrite])];
const locPath = `/${c.slug}/`;
out += `<Location "${locPath}">\n`;
const sameSet = readUsers.length === writeUsers.length &&
readUsers.every(u => writeUsers.includes(u));
if (sameSet) {
// Same users for read and write — one Require covers it all.
if (readUsers.length) out += ` Require user ${readUsers.join(' ')}\n`;
else out += ` Require all denied\n`;
} else {
// Split explicitly by method so Apache's default RequireAny (OR)
// doesn't let readers inherit write access from a broader outer Require.
out += ` <Limit GET PROPFIND OPTIONS HEAD>\n`;
if (readUsers.length) out += ` Require user ${readUsers.join(' ')}\n`;
else out += ` Require all denied\n`;
out += ` </Limit>\n`;
out += ` <LimitExcept GET PROPFIND OPTIONS HEAD>\n`;
if (writeUsers.length) out += ` Require user ${writeUsers.join(' ')}\n`;
else out += ` Require all denied\n`;
out += ` </LimitExcept>\n`;
}
out += `</Location>\n\n`;
}
return out;
}
function sync() {
try {
ensureDir();
atomicWrite(path.join(WEBDAV_CONFIG_DIR, 'htpasswd'), buildHtpasswd());
atomicWrite(path.join(WEBDAV_CONFIG_DIR, 'access.conf'), buildAccessConf());
// reload trigger (watched by apache entrypoint)
atomicWrite(path.join(WEBDAV_CONFIG_DIR, 'reload.trigger'), String(Date.now()));
} catch (e) {
console.error('webdav-config sync failed:', e.message);
}
}
module.exports = { sync, WEBDAV_CONFIG_DIR };