UI: password change enforcement + migration recovery
- Web UI admin password reset now enforces admin role server-side (was cookie-auth + CSRF only; any logged-in user could previously reset any account's password) - Added self-service password change UI at GET/PUT /profile: current_password + new_password + confirm_password; server-side equality check; lockout + Argon2id verification; revokes all other sessions on success - password_change_form.html fragment and profile.html page - Nav bar actor name now links to /profile - policy: ActionChangePassword + default rule -7 allowing human accounts to change their own password - openapi.yaml: built-in rules count updated to -7 Migration recovery: - mciasdb schema force --version N: new subcommand to clear dirty migration state without running SQL (break-glass) - schema subcommands bypass auto-migration on open so the tool stays usable when the database is dirty - Migrate(): shim no longer overrides schema_migrations when it already has an entry; duplicate-column error on the latest migration is force-cleaned and treated as success (handles columns added outside the runner) Security: - Admin role is now validated in handleAdminResetPassword before any DB access; non-admin receives 403 - handleSelfChangePassword follows identical lockout + constant-time Argon2id path as the REST self-service handler; current password required to prevent token-theft account takeover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
<li><a href="/audit">Audit</a></li>
|
||||
<li><a href="/policies">Policies</a></li>
|
||||
<li><a href="/pgcreds">PG Creds</a></li>
|
||||
{{if .ActorName}}<li><span class="nav-actor">{{.ActorName}}</span></li>{{end}}
|
||||
{{if .ActorName}}<li><a href="/profile">{{.ActorName}}</a></li>{{end}}
|
||||
<li><form method="POST" action="/logout" style="margin:0"><button class="btn btn-sm btn-secondary" type="submit">Logout</button></form></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
53
web/templates/fragments/password_change_form.html
Normal file
53
web/templates/fragments/password_change_form.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{{define "password_change_form"}}
|
||||
<form id="password-change-form"
|
||||
hx-put="/profile/password"
|
||||
hx-target="#password-change-section"
|
||||
hx-swap="innerHTML"
|
||||
hx-headers='{"X-CSRF-Token": "{{.CSRFToken}}"}'
|
||||
onsubmit="return mciasPwChangeConfirm(this)">
|
||||
<div class="form-group">
|
||||
<label for="current_password">Current Password</label>
|
||||
<input type="password" id="current_password" name="current_password"
|
||||
class="form-control" autocomplete="current-password"
|
||||
placeholder="Your current password" required>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:.5rem">
|
||||
<label for="new_password">New Password</label>
|
||||
<input type="password" id="new_password" name="new_password"
|
||||
class="form-control" autocomplete="new-password"
|
||||
placeholder="Minimum 12 characters" required minlength="12">
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:.5rem">
|
||||
<label for="confirm_password">Confirm New Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password"
|
||||
class="form-control" autocomplete="new-password"
|
||||
placeholder="Repeat new password" required minlength="12">
|
||||
</div>
|
||||
<div id="pw-change-error" role="alert"
|
||||
style="display:none;color:var(--color-danger,#c0392b);font-size:.85rem;margin-top:.35rem"></div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top:.75rem">
|
||||
Change Password
|
||||
</button>
|
||||
</form>
|
||||
<script>
|
||||
function mciasPwChangeConfirm(form) {
|
||||
var pw = form.querySelector('#new_password').value;
|
||||
var cfm = form.querySelector('#confirm_password').value;
|
||||
var err = form.querySelector('#pw-change-error');
|
||||
if (pw !== cfm) {
|
||||
err.textContent = 'Passwords do not match.';
|
||||
err.style.display = 'block';
|
||||
return false;
|
||||
}
|
||||
err.style.display = 'none';
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{define "password_change_result"}}
|
||||
{{if .Flash}}
|
||||
<div class="alert alert-success" role="alert">{{.Flash}}</div>
|
||||
{{end}}
|
||||
{{template "password_change_form" .}}
|
||||
{{end}}
|
||||
16
web/templates/profile.html
Normal file
16
web/templates/profile.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{{define "profile"}}{{template "base" .}}{{end}}
|
||||
{{define "title"}}Profile — MCIAS{{end}}
|
||||
{{define "content"}}
|
||||
<div class="page-header">
|
||||
<h1>Profile</h1>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Change Password</h2>
|
||||
<p class="text-muted text-small" style="margin-bottom:.75rem">
|
||||
Enter your current password and choose a new one. Other active sessions will be revoked.
|
||||
</p>
|
||||
<div id="password-change-section">
|
||||
{{template "password_change_form" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user