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 in webdav.conf. // Adding a broad 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 += `\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 += ` \n`; if (readUsers.length) out += ` Require user ${readUsers.join(' ')}\n`; else out += ` Require all denied\n`; out += ` \n`; out += ` \n`; if (writeUsers.length) out += ` Require user ${writeUsers.join(' ')}\n`; else out += ` Require all denied\n`; out += ` \n`; } out += `\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 };