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:
1 parent
559954b222
commit
a29862860d
4 files changed
+282
-16
No files matched your search
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user