85 lines
3.3 KiB
JavaScript
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 };
|