// 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 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 ensureExactBackendPermission(backendUrl) { const origin = normalizeBackendOrigin(backendUrl); if (!origin) { return false; } try { const hasPermission = await browser.permissions.contains({ origins: [`${origin}/*`] }); if (hasPermission) { return true; } 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 getStoredSession() { const [localSettings, sessionSettings] = await Promise.all([ browser.storage.local.get(['backendUrl', 'email', 'username']), browser.storage.session.get(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']), ]); return { backendUrl: localSettings.backendUrl || '', email: localSettings.email || '', username: localSettings.username || '', deviceId: sessionSettings.deviceId || '', accessToken: sessionSettings.accessToken || '', refreshToken: sessionSettings.refreshToken || '', tokenExpiresAt: sessionSettings.tokenExpiresAt || '', }; } async function persistSession(session) { await browser.storage.local.set({ backendUrl: session.backendUrl || '', email: session.email || '', username: session.username || '', }); await browser.storage.session.set({ accessToken: session.accessToken || '', refreshToken: session.refreshToken || '', tokenExpiresAt: session.tokenExpiresAt || '', deviceId: session.deviceId || '', }); } async function refreshAccessToken() { const session = await getStoredSession(); if (!session.backendUrl || !session.refreshToken || !session.deviceId) { return null; } try { const response = await fetch(`${session.backendUrl}/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: session.refreshToken, device_id: session.deviceId, }), }); if (!response.ok) { await clearSession(); return null; } const data = await response.json(); const refreshedSession = { ...session, accessToken: data.access_token, refreshToken: data.refresh_token, tokenExpiresAt: data.expires_at, deviceId: data.device_id || session.deviceId, }; await persistSession(refreshedSession); return refreshedSession; } catch (error) { return null; } } async function loadSettings() { const settings = await getStoredSession(); backendUrlInput.value = settings.backendUrl || ''; emailInput.value = settings.email || ''; if (settings.accessToken && settings.backendUrl) { try { const response = await fetch( `${settings.backendUrl}/api/auth/me`, {headers: {Authorization: `Bearer ${settings.accessToken}`}}, ); if (response.ok) { const user = await response.json(); showLoggedIn(user.username || settings.email, settings.backendUrl); return; } if (response.status === 401) { const refreshed = await refreshAccessToken(); if (refreshed) { const userResponse = await fetch( `${refreshed.backendUrl}/api/auth/me`, {headers: {Authorization: `Bearer ${refreshed.accessToken}`}}, ); if (userResponse.ok) { const user = await userResponse.json(); showLoggedIn(user.username || refreshed.email, refreshed.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([ browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken', 'deviceId']), browser.storage.session.remove(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']), ]); } async function saveSettingsAndLogin(event) { event.preventDefault(); const backendUrl = backendUrlInput.value.trim(); const email = emailInput.value.trim(); const password = passwordInput.value; const otp = otpInput.value.trim(); const existingSession = await getStoredSession(); const deviceId = existingSession.deviceId || crypto.randomUUID(); if (!backendUrl || !email || !password) { setStatus(t('fillAllFields'), true); return; } const origin = normalizeBackendOrigin(backendUrl); if (!origin || !(await ensureExactBackendPermission(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, device_id: deviceId }) }); if (!response.ok) { throw new Error(t('loginFailed')); } const data = await response.json(); await persistSession({ backendUrl, email, username: data.user?.username || email, accessToken: data.access_token, refreshToken: data.refresh_token, tokenExpiresAt: data.expires_at, deviceId: data.device_id || 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 getStoredSession(); if (settings.accessToken && settings.backendUrl) { await fetch(`${settings.backendUrl}/api/auth/logout`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: settings.accessToken }), }).catch(() => undefined); } await clearSession(); showLoggedOut(); setStatus(t('signedOut')); } form.addEventListener('submit', saveSettingsAndLogin); signOutButton.addEventListener('click', signOut); loadSettings();