Files
Link-Log/frontend/static/profile.js
T

210 lines
8.5 KiB
JavaScript

// 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 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);
});
})();