Add WebDAV support via Apache mod_dav
Backend: - WebDAV config managed at /etc/apache2/conf-available/micronas-webdav.conf with BEGIN/END markers per share; reload apache2 on every write - htpasswd file at /etc/micronas-webdav.passwd synced automatically on create_user, change_password, delete_user - Full CRUD: POST/PATCH/DELETE /api/share/webdav[/<name>] - _validate_webdav_users(): space-separated, no @group (not supported by Apache) - Fix: _validate_users() now allows @groupname for SMB (was incorrectly rejected) - Dashboard: apache2 status + restart button; webdav shares in /api/dashboard Frontend: - WebDAV tab: create form + table with clickable access URLs - Edit modal supports 'webdav' type (name, path, users, permissions section) - badge-webdav CSS class (green tint) in app.css - dashboard.js renders badge-<type> dynamically (smb/nfs/webdav) Docs: README WebDAV mount guide (Linux davfs2, Windows, Mac), PLANNING phase 4 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1be6a05cef
commit
7c9fe07a17
7 files changed
+338
-19
No files matched your search
+3
-2
@@ -50,8 +50,9 @@ body {
|
||||
.progress { background: var(--surf2); border-radius: 4px; }
|
||||
|
||||
/* Badges */
|
||||
.badge-smb { background: #1f3a5f; color: #58a6ff; font-size: .72rem; }
|
||||
.badge-nfs { background: #2d1f5f; color: #a78bfa; font-size: .72rem; }
|
||||
.badge-smb { background: #1f3a5f; color: #58a6ff; font-size: .72rem; }
|
||||
.badge-nfs { background: #2d1f5f; color: #a78bfa; font-size: .72rem; }
|
||||
.badge-webdav { background: #1f3a2f; color: #3fb950; font-size: .72rem; }
|
||||
|
||||
/* Modal */
|
||||
.modal-content { background: var(--surf); border: 1px solid var(--border); }
|
||||
|
||||
@@ -13,8 +13,9 @@ async function loadDashboard() {
|
||||
document.getElementById(id).style.display = 'flex';
|
||||
document.getElementById(id).style.alignItems = 'center';
|
||||
}
|
||||
setDaemon('statusSmbd', 'smbd', d.status.smbd);
|
||||
setDaemon('statusNfsd', 'nfsd', d.status.nfsd);
|
||||
setDaemon('statusSmbd', 'smbd', d.status.smbd);
|
||||
setDaemon('statusNfsd', 'nfsd', d.status.nfsd);
|
||||
setDaemon('statusApache2', 'apache2', d.status.apache2);
|
||||
|
||||
// Disk bar
|
||||
if (d.disk && !d.disk.error) {
|
||||
@@ -43,7 +44,7 @@ async function loadDashboard() {
|
||||
} else {
|
||||
sb.innerHTML = d.shares.map(s => `
|
||||
<tr>
|
||||
<td><span class="badge rounded-pill ${s.type === 'smb' ? 'badge-smb' : 'badge-nfs'}">${s.type.toUpperCase()}</span></td>
|
||||
<td><span class="badge rounded-pill badge-${s.type}">${s.type.toUpperCase()}</span></td>
|
||||
<td>${esc(s.name ?? '—')}</td>
|
||||
<td><code>${esc(s.path)}</code></td>
|
||||
<td style="color:var(--muted); font-size:.87em">${esc(s.access || '—')}</td>
|
||||
|
||||
+71
-1
@@ -30,6 +30,29 @@ async function loadShares() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const wb = document.getElementById('webdavTableBody');
|
||||
if (!shares.webdav?.length) {
|
||||
wb.innerHTML = `<tr><td colspan="5" style="color:var(--muted)">No WebDAV shares.</td></tr>`;
|
||||
} else {
|
||||
wb.innerHTML = shares.webdav.map(s => {
|
||||
const shareJson = esc(JSON.stringify(s));
|
||||
const nameJson = esc(JSON.stringify(s.name));
|
||||
const url = `http://${window.location.hostname}/dav/${esc(s.name)}`;
|
||||
return `<tr>
|
||||
<td>${esc(s.name)}</td>
|
||||
<td><code>${esc(s.path)}</code></td>
|
||||
<td style="color:var(--muted); font-size:.87em">${esc(s.users || '— all —')}</td>
|
||||
<td><a href="${url}" target="_blank" style="color:var(--accent); font-size:.85em">${url}</a></td>
|
||||
<td>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="openEditModal('webdav',${shareJson})">Edit</button>
|
||||
<button class="btn btn-sm btn-danger"
|
||||
onclick="deleteShare('webdav',${nameJson})">Delete</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const nb = document.getElementById('nfsTableBody');
|
||||
if (!shares.nfs?.length) {
|
||||
nb.innerHTML = `<tr><td colspan="4" style="color:var(--muted)">No NFS exports.</td></tr>`;
|
||||
@@ -76,6 +99,26 @@ async function addSMBShare() {
|
||||
loadShares(); loadDashboard();
|
||||
}
|
||||
|
||||
async function addWebDAVShare() {
|
||||
const name = document.getElementById('webdavName').value.trim();
|
||||
const path = document.getElementById('webdavPath').value.trim();
|
||||
const users = document.getElementById('webdavUsers').value.trim();
|
||||
if (!name || !path) return showToast('Name and path are required.', 'danger');
|
||||
|
||||
const res = await apiFetch('/api/share/webdav', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, path, users}),
|
||||
});
|
||||
if (!res) return;
|
||||
const data = await res.json();
|
||||
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
|
||||
showToast('WebDAV share created.');
|
||||
document.getElementById('webdavName').value = '';
|
||||
document.getElementById('webdavPath').value = '';
|
||||
document.getElementById('webdavUsers').value = '';
|
||||
loadShares(); loadDashboard();
|
||||
}
|
||||
|
||||
async function addNFSShare() {
|
||||
const path = document.getElementById('nfsPath').value.trim();
|
||||
const clients = document.getElementById('nfsClients').value.trim() || '*';
|
||||
@@ -200,6 +243,23 @@ async function openEditModal(type, share) {
|
||||
<label class="form-check-label" for="editRecycleBin" style="color:var(--muted)">Recycle Bin — deleted files go to <code>.recycle</code> instead of being gone</label>
|
||||
</div>
|
||||
${permSection}`;
|
||||
} else if (type === 'webdav') {
|
||||
body = `
|
||||
<div class="mb-3">
|
||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Share Name</label>
|
||||
<input type="text" id="editName" class="form-control" value="${esc(share.name)}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Path</label>
|
||||
<input type="text" id="editPath" class="form-control" value="${esc(share.path)}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" style="color:var(--muted); font-size:.85rem">Allowed Users
|
||||
<span style="font-weight:400"> — space-separated, empty = all WebDAV users</span>
|
||||
</label>
|
||||
<input type="text" id="editWebdavUsers" class="form-control" value="${esc(share.users || '')}">
|
||||
</div>
|
||||
${permSection}`;
|
||||
} else {
|
||||
const isRo = /\(ro[,)]/.test(share.clients);
|
||||
const cleanClients = share.clients.replace(/\s*\(.*?\)/g, '').trim();
|
||||
@@ -248,6 +308,14 @@ async function submitEdit() {
|
||||
method: 'PATCH', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, path, validUsers, forceUser, forceGroup, guestOk, readOnly, recycleBin}),
|
||||
});
|
||||
} else if (ctx.type === 'webdav') {
|
||||
const name = document.getElementById('editName').value.trim();
|
||||
const path = document.getElementById('editPath').value.trim();
|
||||
const users = document.getElementById('editWebdavUsers').value.trim();
|
||||
res = await apiFetch(`/api/share/webdav/${encodeURIComponent(ctx.original.name)}`, {
|
||||
method: 'PATCH', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, path, users}),
|
||||
});
|
||||
} else {
|
||||
const clients = document.getElementById('editClients').value.trim();
|
||||
const readOnly = document.getElementById('editReadOnly').checked;
|
||||
@@ -270,7 +338,7 @@ async function submitEdit() {
|
||||
const owner = document.getElementById('editOwner')?.value.trim();
|
||||
const group = document.getElementById('editGroup')?.value.trim();
|
||||
const mode = document.getElementById('editMode')?.value.trim();
|
||||
const permPath = ctx.type === 'smb'
|
||||
const permPath = (ctx.type === 'smb' || ctx.type === 'webdav')
|
||||
? document.getElementById('editPath')?.value.trim()
|
||||
: ctx.original.path;
|
||||
|
||||
@@ -306,6 +374,8 @@ function deleteShare(type, identifier) {
|
||||
let res;
|
||||
if (type === 'smb') {
|
||||
res = await apiFetch(`/api/share/smb/${encodeURIComponent(identifier)}`, { method: 'DELETE' });
|
||||
} else if (type === 'webdav') {
|
||||
res = await apiFetch(`/api/share/webdav/${encodeURIComponent(identifier)}`, { method: 'DELETE' });
|
||||
} else {
|
||||
res = await apiFetch('/api/share/nfs', {
|
||||
method: 'DELETE', headers: {'Content-Type': 'application/json'},
|
||||
|
||||
Reference in new issue
Block a user