// Copyright © 2026 Olaf Kolkman // SPDX-License-Identifier: GPL-3.0-or-later const statusEl = document.getElementById('status'); const form = document.getElementById('settings-form'); const backendUrlInput = document.getElementById('backend-url'); const emailInput = document.getElementById('email'); const passwordInput = document.getElementById('password'); const otpInput = document.getElementById('otp'); const session = document.getElementById('logged-in'); const sessionSummary = document.getElementById('session-summary'); const signOutButton = document.getElementById('sign-out'); const sessionStore = browser.storage.session; const t = window.linklogI18n; function normalizeBackendOrigin(backendUrl) { try { const url = new URL(backendUrl); if (!['http:', 'https:'].includes(url.protocol)) { return null; } return url.origin; } catch (error) { return null; } } async function hasBackendPermission(backendUrl) { const origin = normalizeBackendOrigin(backendUrl); if (!origin) return false; return browser.permissions.contains({origins: [`${origin}/*`]}); } async function requestBackendPermission(backendUrl) { const origin = normalizeBackendOrigin(backendUrl); if (!origin) return false; try { return await browser.permissions.request({origins: [`${origin}/*`]}); } catch (error) { return false; } } function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.remove('hidden'); statusEl.classList.toggle('error', isError); statusEl.classList.toggle('success', !isError); } async function loadSettings() { const [settings, sessionSettings] = await Promise.all([ browser.storage.local.get(['backendUrl', 'email', 'username']), sessionStore.get(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']), ]); backendUrlInput.value = settings.backendUrl || ''; emailInput.value = settings.email || ''; if (sessionSettings.accessToken && settings.backendUrl && await hasBackendPermission(settings.backendUrl)) { try { const response = await fetch( `${settings.backendUrl}/api/auth/me`, {headers: {Authorization: `Bearer ${sessionSettings.accessToken}`}}, ); if (response.ok) { const user = await response.json(); showLoggedIn(user.username || settings.email, settings.backendUrl); return; } } catch (error) { // Show the login form when the backend cannot validate the stored token. } await clearSession(); } showLoggedOut(); } function showLoggedIn(username, backendUrl) { sessionSummary.textContent = t('loggedInAt', [username, backendUrl]); session.classList.remove('hidden'); form.classList.add('hidden'); } function showLoggedOut() { session.classList.add('hidden'); form.classList.remove('hidden'); } async function clearSession() { await Promise.all([ sessionStore.remove(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']), browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken', 'deviceId']), ]); } async function refreshAccessToken(settings) { if (!settings.backendUrl || !settings.refreshToken || !settings.deviceId) return null; try { const response = await fetch(`${settings.backendUrl}/api/auth/refresh`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({refresh_token: settings.refreshToken, device_id: settings.deviceId}), }); if (!response.ok) { await clearSession(); return null; } const data = await response.json(); const refreshed = {accessToken: data.access_token, refreshToken: data.refresh_token, tokenExpiresAt: data.expires_at, deviceId: data.device_id || settings.deviceId}; await sessionStore.set(refreshed); return refreshed; } catch (error) { return null; } } async function saveSettingsAndLogin(event) { event.preventDefault(); const backendUrl = normalizeBackendOrigin(backendUrlInput.value.trim()); const email = emailInput.value.trim(); const password = passwordInput.value; const otp = otpInput.value.trim(); if (!backendUrl || !email || !password) { setStatus(t('fillAllFields'), true); return; } if (!(await requestBackendPermission(backendUrl))) { setStatus(t('unableToLogIn'), true); return; } try { const response = await fetch(`${backendUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password, otp: otp || null }) }); if (!response.ok) { throw new Error(t('loginFailed')); } const data = await response.json(); if (response.status === 401) { const refreshed = await refreshAccessToken({...settings, ...sessionSettings}); if (refreshed) { const retry = await fetch(`${settings.backendUrl}/api/auth/me`, { headers: {Authorization: `Bearer ${refreshed.accessToken}`}, }); if (retry.ok) { const user = await retry.json(); showLoggedIn(user.username || settings.email, settings.backendUrl); return; } } } await browser.storage.local.set({ backendUrl, email, username: data.user?.username || email, }); await sessionStore.set({ accessToken: data.access_token, refreshToken: data.refresh_token, tokenExpiresAt: data.expires_at, deviceId: data.device_id || `device-${crypto.randomUUID()}`, }); showLoggedIn(data.user?.username || email, backendUrl); passwordInput.value = ''; otpInput.value = ''; setStatus(t('loggedInSuccessfully')); } catch (error) { setStatus(t('unableToLogIn'), true); } } async function signOut() { const settings = await browser.storage.local.get(['backendUrl']); const sessionSettings = await sessionStore.get(['accessToken']); const backendUrl = normalizeBackendOrigin(backendUrlInput.value.trim()); if (sessionSettings.accessToken && settings.backendUrl && await hasBackendPermission(backendUrl)) { await fetch(`${backendUrl}/api/auth/logout`, { method: 'POST', headers: {Authorization: `Bearer ${sessionSettings.accessToken}`}, }).catch(() => undefined); } await clearSession(); showLoggedOut(); setStatus(t('signedOut')); } form.addEventListener('submit', saveSettingsAndLogin); signOutButton.addEventListener('click', signOut); loadSettings();