// Copyright © 2026 Olaf Kolkman // SPDX-License-Identifier: GPL-3.0-or-later (() => { const profileForm = document.querySelector('#profile-form'); const passwordForm = document.querySelector('#password-form'); const mastodonForm = document.querySelector('#mastodon-form'); const profileLogoutButton = document.querySelector('#logout-button'); const accessToken = localStorage.getItem('linklogAccessToken'); const defaultPostPrefix = 'From my #LinkLog: '; const mastodonConnectButton = document.querySelector('#mastodon-connect'); const otpSetupButton = document.querySelector('#otp-setup'); const otpEnableButton = document.querySelector('#otp-enable'); const otpDisableButton = document.querySelector('#otp-disable'); const otpProvisioning = document.querySelector('#otp-provisioning'); const otpDisabled = document.querySelector('#otp-disabled'); const otpEnabled = document.querySelector('#otp-enabled'); const otpSecret = document.querySelector('#otp-secret'); const otpUri = document.querySelector('#otp-uri'); const otpStatus = document.querySelector('#otp-status'); function authHeaders(includeJson = false) { return { ...(includeJson ? {'Content-Type': 'application/json'} : {}), ...(accessToken ? {Authorization: `Bearer ${accessToken}`} : {}), }; } function setStatus(selector, message, isError = false) { const status = document.querySelector(selector); status.textContent = message; status.style.color = isError ? '#b91c1c' : '#166534'; } function setOtpStatus(message, isError = false) { otpStatus.textContent = message; otpStatus.style.color = isError ? '#b91c1c' : '#166534'; } 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'); const result = await response.json(); otpDisabled.classList.toggle('hidden', result.enabled); otpEnabled.classList.toggle('hidden', !result.enabled); } otpSetupButton.addEventListener('click', async () => { const response = await fetch('/api/user/otp/setup', {method: 'POST', headers: authHeaders()}); const result = await response.json(); if (!response.ok) { setOtpStatus(result.detail || 'Could not start one-time password setup.', true); return; } otpSecret.textContent = result.secret; otpUri.href = result.otpauth_url; otpProvisioning.classList.remove('hidden'); setOtpStatus('Enter a code from your authenticator app to confirm setup.'); }); otpEnableButton.addEventListener('click', async () => { const code = document.querySelector('#otp-setup-code').value.trim(); const response = await fetch('/api/user/otp', { method: 'POST', headers: authHeaders(true), body: JSON.stringify({action: 'enable', code}), }); const result = await response.json(); if (!response.ok) { setOtpStatus(result.detail || 'Could not enable one-time password.', true); return; } otpDisabled.classList.add('hidden'); otpEnabled.classList.remove('hidden'); otpProvisioning.classList.add('hidden'); setOtpStatus('One-time password enabled.'); }); otpDisableButton.addEventListener('click', async () => { const code = document.querySelector('#otp-disable-code').value.trim(); const response = await fetch('/api/user/otp', { method: 'POST', headers: authHeaders(true), body: JSON.stringify({action: 'disable', code}), }); const result = await response.json(); if (!response.ok) { setOtpStatus(result.detail || 'Could not disable one-time password.', true); return; } otpDisabled.classList.remove('hidden'); otpEnabled.classList.add('hidden'); document.querySelector('#otp-disable-code').value = ''; setOtpStatus('One-time password disabled.'); }); async function loadProfile() { const response = await fetch('/api/user/me', {headers: authHeaders()}); 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('#bio').value = profile.bio || ''; const avatarPreview = document.querySelector('#avatar-preview'); if (profile.avatar_url) { avatarPreview.src = profile.avatar_url; avatarPreview.classList.remove('hidden'); } if (profile.is_admin) { document.querySelector('#auth-admin-link')?.classList.remove('hidden'); } profileLogoutButton.classList.remove('hidden'); } async function loadMastodonConfig() { const response = await fetch('/api/user/plugins/mastodon', {headers: authHeaders()}); if (!response.ok) throw new Error('Could not load Mastodon settings'); const config = await response.json(); document.querySelector('#mastodon-instance').value = config.instance || 'mastodon.social'; document.querySelector('#mastodon-post-prefix').value = config.post_prefix || defaultPostPrefix; } profileForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(profileForm); formData.delete('avatar'); const response = await fetch('/api/user/me', { method: 'PUT', headers: authHeaders(true), body: JSON.stringify(Object.fromEntries(formData)), }); setStatus('#profile-status', response.ok ? 'Profile saved.' : 'Could not save profile.', !response.ok); }); document.querySelector('#avatar-file').addEventListener('change', async (event) => { const file = event.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('avatar', file); const response = await fetch('/api/user/avatar', { method: 'POST', headers: authHeaders(), body: formData, }); if (!response.ok) { setStatus('#profile-status', 'Could not upload avatar.', true); return; } const data = await response.json(); const avatarPreview = document.querySelector('#avatar-preview'); avatarPreview.src = data.avatar_url; avatarPreview.classList.remove('hidden'); setStatus('#profile-status', 'Avatar uploaded.'); }); mastodonForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(mastodonForm); const response = await fetch('/api/user/plugins/mastodon', { method: 'PUT', headers: authHeaders(true), body: JSON.stringify(Object.fromEntries(formData)), }); setStatus('#mastodon-status', response.ok ? 'Mastodon settings saved.' : 'Could not save Mastodon settings.', !response.ok); }); mastodonConnectButton.addEventListener('click', async () => { mastodonConnectButton.disabled = true; const instance = document.querySelector('#mastodon-instance').value.trim(); if (!instance) { setStatus('#mastodon-status', 'Enter the Mastodon server you want to use.', true); mastodonConnectButton.disabled = false; return; } try { const settingsResponse = await fetch('/api/user/plugins/mastodon', { method: 'PUT', headers: authHeaders(true), body: JSON.stringify({instance}), }); if (!settingsResponse.ok) throw new Error('Could not save the Mastodon server.'); setStatus('#mastodon-status', `Authorizing with ${instance}...`); const response = await fetch(`/api/mastodon/oauth/start?instance=${encodeURIComponent(instance)}`, {headers: authHeaders()}); const result = await response.json(); if (!response.ok || !result.authorization_url) throw new Error(result.detail || result.error || 'Could not start Mastodon authorization.'); window.location.assign(result.authorization_url); } catch (error) { setStatus('#mastodon-status', error.message, true); mastodonConnectButton.disabled = false; } }); const mastodonParams = new URLSearchParams(window.location.search); if (mastodonParams.get('mastodon') === 'connected') setStatus('#mastodon-status', 'Mastodon connected.'); if (mastodonParams.get('mastodon_error')) setStatus('#mastodon-status', mastodonParams.get('mastodon_error'), true); passwordForm.addEventListener('submit', async (event) => { event.preventDefault(); const password = passwordForm.elements.new_password.value; const confirmation = passwordForm.elements.new_password_confirmation.value; if (password !== confirmation) { const status = document.querySelector('#password-status'); status.textContent = 'New passwords do not match.'; status.style.color = '#f38ba8'; return; } const response = await fetch('/api/user/password', { method: 'PUT', headers: authHeaders(true), body: JSON.stringify(Object.fromEntries(new FormData(passwordForm))), }); const status = document.querySelector('#password-status'); status.textContent = response.ok ? 'Password changed.' : 'Could not change password.'; status.style.color = response.ok ? '#94e2d5' : '#f38ba8'; if (response.ok) passwordForm.reset(); }); Promise.all([loadProfile(), loadMastodonConfig(), loadOtp()]).catch((error) => { setStatus('#profile-status', accessToken ? error.message : 'Please sign in first.', true); }); })();