Files
mailgoserver/internal/webui/templates/account.html
T

142 lines
6.9 KiB
HTML

{{define "title"}}Account Settings{{end}}
{{define "page_title"}}Account Settings{{end}}
{{define "content"}}
{{if .mfa_required}}
<div class="alert alert-warning">
<i class="bi bi-shield-exclamation me-2"></i>
Your administrator requires two-factor authentication for all admin accounts. Set up an authenticator app or a passkey below to continue using the dashboard.
</div>
{{end}}
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-person-circle me-2"></i>Profile</h5></div>
<div class="card-body">
<p><strong>Username:</strong> {{.user.Username}}</p>
<hr>
<h6>Change password</h6>
<form method="POST" action="/pymta-manager/account/password">
<div class="mb-3">
<label class="form-label">Current password</label>
<input type="password" class="form-control" name="current_password" required>
</div>
<div class="mb-3">
<label class="form-label">New password</label>
<input type="password" class="form-control" name="new_password" required minlength="10">
<div class="form-text">At least 10 characters, with a letter, a number, and a symbol.</div>
</div>
<div class="mb-3">
<label class="form-label">Confirm new password</label>
<input type="password" class="form-control" name="new_password_confirm" required minlength="10">
</div>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Update password</button>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-phone me-2"></i>Authenticator App (TOTP)</h5></div>
<div class="card-body">
{{if .user.TOTPEnabled}}
<p class="text-success"><i class="bi bi-check-circle me-1"></i>Enabled</p>
<form method="POST" action="/pymta-manager/account/totp/disable">
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Disable authenticator app MFA?">Disable</button>
</form>
{{else}}
<p class="text-muted">Not enabled. Add an authenticator app (Google Authenticator, 1Password, etc.) as an optional second factor.</p>
<form method="POST" action="/pymta-manager/account/totp/setup">
<button type="submit" class="btn btn-outline-primary btn-sm"><i class="bi bi-qr-code me-1"></i>Set up</button>
</form>
{{end}}
</div>
</div>
<div class="card">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-fingerprint me-2"></i>Passkeys / Security Keys</h5></div>
<div class="card-body">
{{if .passkeys}}
<ul class="list-group mb-3">
{{range .passkeys}}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="bi bi-key me-2"></i>{{.Name}} <small class="text-muted">added {{strftime "%Y-%m-%d" .CreatedAt}}</small></span>
<form method="POST" action="/pymta-manager/account/passkey/{{.ID}}/remove">
<button type="submit" class="btn btn-sm btn-outline-danger" data-confirm="Remove this passkey?">Remove</button>
</form>
</li>
{{end}}
</ul>
{{else}}
<p class="text-muted">No passkeys registered yet.</p>
{{end}}
<div id="passkey-error" class="alert alert-danger d-none"></div>
<button type="button" class="btn btn-outline-primary btn-sm" id="add-passkey-btn"><i class="bi bi-plus-circle me-1"></i>Add a passkey</button>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script>
function b64urlToBuf(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const bin = atob(s);
const buf = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
return buf.buffer;
}
function bufToB64url(buf) {
const bytes = new Uint8Array(buf);
let bin = '';
bytes.forEach(b => bin += String.fromCharCode(b));
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
document.getElementById('add-passkey-btn').addEventListener('click', async function() {
const errEl = document.getElementById('passkey-error');
errEl.classList.add('d-none');
try {
const name = prompt('Name this passkey (e.g. "YubiKey", "MacBook Touch ID"):', 'Passkey') || 'Passkey';
const beginResp = await fetch('/pymta-manager/account/passkey/begin', { method: 'POST' });
if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey registration');
const options = await beginResp.json();
const publicKey = options.publicKey;
publicKey.challenge = b64urlToBuf(publicKey.challenge);
publicKey.user.id = b64urlToBuf(publicKey.user.id);
if (publicKey.excludeCredentials) {
publicKey.excludeCredentials = publicKey.excludeCredentials.map(c => ({ ...c, id: b64urlToBuf(c.id) }));
}
const credential = await navigator.credentials.create({ publicKey });
const body = {
id: credential.id,
rawId: bufToB64url(credential.rawId),
type: credential.type,
response: {
attestationObject: bufToB64url(credential.response.attestationObject),
clientDataJSON: bufToB64url(credential.response.clientDataJSON),
},
};
const finishResp = await fetch('/pymta-manager/account/passkey/finish?name=' + encodeURIComponent(name), {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
});
if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Could not save passkey');
showToast('Passkey added', 'success');
setTimeout(() => location.reload(), 800);
} catch (e) {
errEl.textContent = e.message || 'Adding the passkey failed';
errEl.classList.remove('d-none');
}
});
</script>
{{end}}