primary email change functionality
Build LinkLog Development Image / development-image (push) Successful in 12s

This commit is contained in:
2026-08-26 15:25:49 +02:00
parent 079eb146f6
commit fec7836384
14 changed files with 572 additions and 125 deletions
+79 -2
View File
@@ -18,6 +18,9 @@ const otpEnabled = document.querySelector('#otp-enabled');
const otpSecret = document.querySelector('#otp-secret');
const otpUri = document.querySelector('#otp-uri');
const otpStatus = document.querySelector('#otp-status');
const emailAddressList = document.querySelector('#email-address-list');
const additionalEmailForm = document.querySelector('#additional-email-form');
const emailAddressStatus = document.querySelector('#email-address-status');
function authHeaders(includeJson = false) {
return {
@@ -37,6 +40,80 @@ function setOtpStatus(message, isError = false) {
otpStatus.style.color = isError ? '#b91c1c' : '#166534';
}
function setEmailAddressStatus(message, isError = false) {
emailAddressStatus.textContent = message;
emailAddressStatus.style.color = isError ? '#b91c1c' : '#166534';
}
function renderEmailAddresses(addresses) {
emailAddressList.replaceChildren(...addresses.map((address) => {
const row = document.createElement('div');
row.className = 'email-address-row';
const label = document.createElement('span');
label.textContent = `${address.email} - ${address.verified ? 'validated' : 'not validated'}${address.primary ? ' (primary)' : ''}`;
row.appendChild(label);
if (!address.primary) {
if (!address.verified) {
const resend = document.createElement('button');
resend.type = 'button';
resend.textContent = 'Resend validation';
resend.addEventListener('click', async () => {
resend.disabled = true;
const response = await fetch(`/api/user/emails/${encodeURIComponent(address.id)}/resend`, {method: 'POST', headers: authHeaders()});
const result = await response.json();
setEmailAddressStatus(response.ok ? result.message : (result.detail || 'Could not send validation email.'), !response.ok);
if (response.ok && result.next_allowed_at) window.setTimeout(() => { resend.disabled = false; }, Math.max(0, Date.parse(result.next_allowed_at) - Date.now()));
else if (!response.ok) resend.disabled = false;
});
row.appendChild(resend);
}
if (address.verified && address.can_be_primary) {
const makePrimary = document.createElement('button');
makePrimary.type = 'button';
makePrimary.textContent = 'Make primary';
makePrimary.addEventListener('click', async () => {
const response = await fetch(`/api/user/emails/${encodeURIComponent(address.id)}/make-primary`, {method: 'POST', headers: authHeaders()});
const result = await response.json();
setEmailAddressStatus(response.ok ? `${result.email} is now the primary email address.` : (result.detail || 'Could not change primary email.'), !response.ok);
if (response.ok) {
await loadEmailAddresses();
}
});
row.appendChild(makePrimary);
}
const remove = document.createElement('button');
remove.type = 'button';
remove.textContent = 'Remove';
remove.addEventListener('click', async () => {
const response = await fetch(`/api/user/emails/${encodeURIComponent(address.id)}`, {method: 'DELETE', headers: authHeaders()});
if (response.ok) loadEmailAddresses();
else setEmailAddressStatus('Could not remove email address.', true);
});
row.appendChild(remove);
}
return row;
}));
}
async function loadEmailAddresses() {
const response = await fetch('/api/user/emails', {headers: authHeaders()});
if (!response.ok) throw new Error('Could not load email addresses');
renderEmailAddresses(await response.json());
}
additionalEmailForm.addEventListener('submit', async (event) => {
event.preventDefault();
const response = await fetch('/api/user/emails', {
method: 'POST', headers: authHeaders(true), body: JSON.stringify({email: additionalEmailForm.elements.email.value.trim()}),
});
const result = await response.json();
setEmailAddressStatus(response.ok ? 'Email address added. Check your inbox to validate it.' : (result.detail || 'Could not add email address.'), !response.ok);
if (response.ok) {
additionalEmailForm.reset();
loadEmailAddresses();
}
});
async function loadOtp() {
const response = await fetch('/api/user/otp', {headers: authHeaders()});
if (!response.ok) throw new Error('Could not load one-time password settings');
@@ -95,7 +172,7 @@ async function loadProfile() {
if (!response.ok) throw new Error('Could not load profile');
const profile = await response.json();
document.querySelector('#username').textContent = profile.username || '';
document.querySelector('#email').value = profile.email || '';
document.querySelector('#email').textContent = profile.email || '';
document.querySelector('#bio').value = profile.bio || '';
const avatarPreview = document.querySelector('#avatar-preview');
if (profile.avatar_url) {
@@ -211,7 +288,7 @@ passwordForm.addEventListener('submit', async (event) => {
if (response.ok) passwordForm.reset();
});
Promise.all([loadProfile(), loadMastodonConfig(), loadOtp()]).catch((error) => {
Promise.all([loadProfile(), loadMastodonConfig(), loadOtp(), loadEmailAddresses()]).catch((error) => {
setStatus('#profile-status', accessToken ? error.message : 'Please sign in first.', true);
});
})();