token usage tightened with revocation
This commit is contained in:
@@ -12,6 +12,9 @@
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'none'"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "__MSG_extensionName__",
|
||||
"default_popup": "popup.html",
|
||||
@@ -27,7 +30,7 @@
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "linklog@kolkman.org",
|
||||
"strict_min_version": "109.0",
|
||||
"strict_min_version": "112.0",
|
||||
"data_collection_permissions": {
|
||||
"required": ["websiteActivity"],
|
||||
"optional": []
|
||||
|
||||
+54
-9
@@ -10,6 +10,7 @@ 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;
|
||||
|
||||
@@ -49,15 +50,18 @@ function setStatus(message, isError = false) {
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const settings = await browser.storage.local.get(['backendUrl', 'email', 'username', 'accessToken']);
|
||||
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 (settings.accessToken && settings.backendUrl && await hasBackendPermission(settings.backendUrl)) {
|
||||
if (sessionSettings.accessToken && settings.backendUrl && await hasBackendPermission(settings.backendUrl)) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${settings.backendUrl}/api/auth/me`,
|
||||
{headers: {Authorization: `Bearer ${settings.accessToken}`}},
|
||||
{headers: {Authorization: `Bearer ${sessionSettings.accessToken}`}},
|
||||
);
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
@@ -84,7 +88,32 @@ function showLoggedOut() {
|
||||
}
|
||||
|
||||
async function clearSession() {
|
||||
await browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken']);
|
||||
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) {
|
||||
@@ -116,14 +145,29 @@ async function saveSettingsAndLogin(event) {
|
||||
}
|
||||
|
||||
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,
|
||||
tokenType: data.token_type,
|
||||
tokenExpiresAt: data.expires_at,
|
||||
refreshToken: data.refresh_token,
|
||||
tokenExpiresAt: data.expires_at,
|
||||
deviceId: data.device_id || `device-${crypto.randomUUID()}`,
|
||||
});
|
||||
|
||||
showLoggedIn(data.user?.username || email, backendUrl);
|
||||
@@ -136,13 +180,14 @@ async function saveSettingsAndLogin(event) {
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
const settings = await browser.storage.local.get(['accessToken']);
|
||||
const settings = await browser.storage.local.get(['backendUrl']);
|
||||
const sessionSettings = await sessionStore.get(['accessToken']);
|
||||
const backendUrl = normalizeBackendOrigin(backendUrlInput.value.trim());
|
||||
if (settings.accessToken && await hasBackendPermission(backendUrl)) {
|
||||
if (sessionSettings.accessToken && settings.backendUrl && await hasBackendPermission(backendUrl)) {
|
||||
await fetch(`${backendUrl}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: settings.accessToken }),
|
||||
body: JSON.stringify({ token: sessionSettings.accessToken }),
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
await clearSession();
|
||||
|
||||
+63
-13
@@ -13,6 +13,7 @@ const feedLink = document.getElementById('feed-link');
|
||||
const authWarning = document.getElementById('auth-warning');
|
||||
const warningSettingsButton = document.getElementById('warning-settings');
|
||||
const authSession = document.getElementById('auth-session');
|
||||
const sessionStore = browser.storage.session;
|
||||
|
||||
const t = window.linklogI18n;
|
||||
|
||||
@@ -42,12 +43,49 @@ function setStatus(message, isError = false) {
|
||||
}
|
||||
|
||||
async function getSettings() {
|
||||
const result = await browser.storage.local.get([
|
||||
'backendUrl',
|
||||
'accessToken',
|
||||
'tokenExpiresAt',
|
||||
const [settings, sessionSettings] = await Promise.all([
|
||||
browser.storage.local.get(['backendUrl', 'username']),
|
||||
sessionStore.get(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']),
|
||||
]);
|
||||
return result;
|
||||
return {...settings, ...sessionSettings};
|
||||
}
|
||||
|
||||
async function persistSession(settings) {
|
||||
await sessionStore.set({
|
||||
accessToken: settings.accessToken,
|
||||
refreshToken: settings.refreshToken,
|
||||
tokenExpiresAt: settings.tokenExpiresAt,
|
||||
deviceId: settings.deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
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 = {...settings, accessToken: data.access_token, refreshToken: data.refresh_token,
|
||||
tokenExpiresAt: data.expires_at, deviceId: data.device_id || settings.deviceId};
|
||||
await persistSession(refreshed);
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function validateSession(settings) {
|
||||
@@ -57,7 +95,16 @@ async function validateSession(settings) {
|
||||
headers: {'Authorization': `Bearer ${settings.accessToken}`},
|
||||
});
|
||||
if (!response.ok) {
|
||||
await browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken']);
|
||||
if (response.status === 401) {
|
||||
const refreshed = await refreshAccessToken(settings);
|
||||
if (refreshed) {
|
||||
const retry = await fetch(`${refreshed.backendUrl}/api/auth/me`, {
|
||||
headers: {'Authorization': `Bearer ${refreshed.accessToken}`},
|
||||
});
|
||||
if (retry.ok) return await retry.json();
|
||||
}
|
||||
}
|
||||
await clearSession();
|
||||
return null;
|
||||
}
|
||||
return await response.json();
|
||||
@@ -97,7 +144,7 @@ function showSavedState(message) {
|
||||
}
|
||||
|
||||
async function updateFeedLink() {
|
||||
const settings = await browser.storage.local.get(['backendUrl', 'username', 'accessToken']);
|
||||
const settings = await getSettings();
|
||||
if (!settings.backendUrl || !settings.username || !settings.accessToken) return;
|
||||
try {
|
||||
const backend = new URL(settings.backendUrl);
|
||||
@@ -119,9 +166,10 @@ async function loadExistingTags() {
|
||||
showSignedOutState();
|
||||
return;
|
||||
}
|
||||
showSignedInState(user, settings.backendUrl);
|
||||
const response = await fetch(`${settings.backendUrl}/api/tags`, {
|
||||
headers: {'Authorization': `Bearer ${settings.accessToken}`},
|
||||
const currentSettings = await getSettings();
|
||||
showSignedInState(user, currentSettings.backendUrl);
|
||||
const response = await fetch(`${currentSettings.backendUrl}/api/tags`, {
|
||||
headers: {'Authorization': `Bearer ${currentSettings.accessToken}`},
|
||||
});
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
@@ -212,12 +260,14 @@ async function handleSubmit(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSettings = await getSettings();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${backendUrl}/api/links`, {
|
||||
const response = await fetch(`${currentSettings.backendUrl}/api/links`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Authorization': `Bearer ${currentSettings.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: titleInput.value,
|
||||
@@ -240,7 +290,7 @@ async function handleSubmit(event) {
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const saveMessage = result.duplicate ? t('linkAlreadyExists') : t('linkSaved', backendUrl);
|
||||
const saveMessage = result.duplicate ? t('linkAlreadyExists') : t('linkSaved', currentSettings.backendUrl);
|
||||
if (result.plugin_errors?.length) {
|
||||
const errors = result.plugin_errors.map((error) => `${error.plugin}: ${error.reason}`).join(' ');
|
||||
showSavedState(`${saveMessage} ${t('publishingErrors', errors)}`);
|
||||
|
||||
Reference in New Issue
Block a user