Phase 5: secure credentials, first-login setup, user rename, admin settings

Security:
- PBKDF2-HMAC-SHA256 password hashing (260k iterations, random salt per password)
- Legacy MICRONAS_PASS plaintext still works for existing installs
- MICRONAS_PASS_HASH replaces plaintext in env file after first setup/change
- Timing-safe comparison via hmac.compare_digest

First-login setup:
- /setup route: shown when no credentials configured (fresh install)
- Replaces editing /etc/micronas.env manually
- setup.html dark-themed form matching login.html

Admin credential change:
- POST /api/admin/credentials (requires current password to confirm)
- Settings button in topbar opens modal for username + password change
- Session cleared after change → redirected to login

User management:
- PATCH /api/users/<name>: rename Linux user (usermod -l)
  SMB + WebDAV entries removed, password reset required after rename
- Rename button in users table, modal with yellow warning

WebDAV:
- Options Indexes: directories now browsable in a regular web browser

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Sonnet 4.6 committed 2026-06-20 16:45:16 +02:00
1 parent 559954b222
commit a29862860d
4 files changed
+282 -16

No files matched your search

+57
View File
@@ -22,6 +22,8 @@ async function loadUsers() {
</span>
</td>
<td>
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
onclick="openRenameModal(${nameJson})">Rename</button>
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
onclick="openPwModal(${nameJson})">Password</button>
<button class="btn btn-sm btn-danger"
@@ -95,6 +97,61 @@ async function submitPassword() {
showToast(`Password updated for "${name}".`);
}
function openRenameModal(name) {
document.getElementById('renameOldName').value = name;
document.getElementById('renameNewName').value = name;
new bootstrap.Modal(document.getElementById('renameModal')).show();
}
async function submitRename() {
const oldName = document.getElementById('renameOldName').value;
const newName = document.getElementById('renameNewName').value.trim();
if (!newName) return showToast('New username is required.', 'danger');
const res = await apiFetch(`/api/users/${encodeURIComponent(oldName)}`, {
method: 'PATCH', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: newName}),
});
if (!res) return;
const data = await res.json();
bootstrap.Modal.getInstance(document.getElementById('renameModal')).hide();
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
if (data.warning) showToast(data.warning, 'warning');
else showToast(`Renamed to "${newName}".`);
loadUsers();
loadGroups();
}
function openAdminSettings() {
document.getElementById('adminCurrentPw').value = '';
document.getElementById('adminNewUser').value = '';
document.getElementById('adminNewPw').value = '';
document.getElementById('adminNewPwConfirm').value = '';
new bootstrap.Modal(document.getElementById('adminModal')).show();
}
async function submitAdminSettings() {
const currentPw = document.getElementById('adminCurrentPw').value;
const newUser = document.getElementById('adminNewUser').value.trim();
const newPw = document.getElementById('adminNewPw').value;
const newPwConf = document.getElementById('adminNewPwConfirm').value;
if (!currentPw) return showToast('Current password is required.', 'danger');
if (newPw && newPw !== newPwConf) return showToast('New passwords do not match.', 'danger');
if (newPw && newPw.length < 8) return showToast('Password must be at least 8 characters.', 'danger');
const res = await apiFetch('/api/admin/credentials', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({currentPassword: currentPw, username: newUser, password: newPw}),
});
if (!res) return;
const data = await res.json();
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
bootstrap.Modal.getInstance(document.getElementById('adminModal')).hide();
showToast('Credentials updated — please log in again.');
setTimeout(() => { window.location.href = '/login'; }, 1500);
}
// ===== GROUPS =====
async function loadGroups() {