Add file upload portal with per-customer links and WebDAV admin access
- Customer upload via token link (no login), optional password + expiry, drag & drop for files and folders with preserved structure - Admin portal with setup wizard, role-based users (admin/staff), per-customer WebDAV access rules (read/write), session auth - WebDAV container (Debian apache2) with htpasswd + access.conf auto-generated from the SQLite DB and reloaded via inotifywait - Configurable public base URL and janitor cron interval in admin UI; janitor reconciles the uploads table with the filesystem Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user