Files
FabledCurator/extension/popup/popup.js
T
bvandeusen d3245f0c22
CI / lint (push) Successful in 4s
CI / backend-lint-and-test (push) Successful in 37s
CI / frontend-build (push) Successful in 40s
extension / lint (push) Successful in 36s
CI / intimp (push) Successful in 4m4s
CI / intapi (push) Successful in 8m2s
CI / intcore (push) Successful in 8m45s
extension / lint (pull_request) Successful in 14s
feat(ext): verify cookies in-browser before uploading (1.0.7)
Pre-upload verify request: after capturing the live browser cookies,
hit a known authenticated endpoint with credentials:'include' from the
extension's background context. If the platform reports we're not
logged in, abort the upload so we don't overwrite FC-side credentials
with stale data.

- platforms.js: add `verify` config per cookie-auth platform
  - hentaifoundry: HEAD /?enterAgree=1 (mirrors gallery-dl's HF
    _init_site_filters; same 401 path the operator hit 2026-06-03)
  - patreon: GET /api/current_user (clean 401 when logged out)
  - subscribestar, deviantart: no stable auth endpoint, skip verify
- cookies.js: verifyCookiesForPlatform() returns {ok, status, reason}.
  ok=true/false/null tri-state — null = verify not configured, caller
  treats as "proceed".
- background.js EXPORT_COOKIES + EXPORT_ALL_COOKIES: verify gates the
  upload; failures bubble up with the platform's name + reason.
- popup.js: success message now appends "(verified ✓)" when applicable.
- manifest + package.json: 1.0.6 → 1.0.7.
2026-06-03 14:04:45 -04:00

241 lines
9.3 KiB
JavaScript

document.addEventListener('DOMContentLoaded', init);
const CONNECTION_TEST_INTERVAL = 2 * 60 * 1000;
async function init() {
try {
const cfg = await browser.runtime.sendMessage({ type: 'GET_CONFIG' });
if (!cfg || !cfg.apiUrl || !cfg.apiKey) {
showSetupRequired();
return;
}
document.getElementById('setup-required').classList.add('hidden');
document.getElementById('main-content').classList.remove('hidden');
setupEventListeners();
showPlatformsLoading();
testConnectionIfNeeded();
loadPlatformStatus().catch(e => showError(`Failed to load platforms: ${e.message}`));
} catch (e) {
showSetupRequired();
const alert = document.querySelector('#setup-required .alert');
alert.textContent = '';
const s = document.createElement('strong'); s.textContent = 'Error';
const p = document.createElement('p'); p.textContent = e.message;
alert.appendChild(s); alert.appendChild(p);
alert.classList.add('alert-error');
}
}
function showSetupRequired() {
document.getElementById('setup-required').classList.remove('hidden');
document.getElementById('main-content').classList.add('hidden');
document.getElementById('open-settings-btn').addEventListener('click', () => {
browser.runtime.openOptionsPage();
});
}
function showPlatformsLoading() {
const c = document.getElementById('platforms-list');
c.textContent = '';
const d = document.createElement('div');
d.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
d.textContent = 'Loading platforms…';
c.appendChild(d);
}
async function testConnectionIfNeeded() {
const stored = await browser.storage.local.get(['lastConnectionTest', 'lastConnectionStatus']);
const now = Date.now();
if (now - (stored.lastConnectionTest || 0) < CONNECTION_TEST_INTERVAL && stored.lastConnectionStatus !== undefined) {
updateConnectionDot(stored.lastConnectionStatus);
if (!stored.lastConnectionStatus) showError('Cannot connect to backend (cached).');
return;
}
const r = await browser.runtime.sendMessage({ type: 'TEST_CONNECTION' });
await browser.storage.local.set({ lastConnectionTest: now, lastConnectionStatus: r.connected });
updateConnectionDot(r.connected);
if (!r.connected) showError(`Cannot connect to backend: ${r.error}`);
}
function updateConnectionDot(connected) {
const d = document.getElementById('connection-status');
d.classList.toggle('connected', connected);
d.title = connected ? 'Connected to FabledCurator' : 'Disconnected';
}
async function loadPlatformStatus() {
const status = await browser.runtime.sendMessage({ type: 'GET_PLATFORM_STATUS' });
const c = document.getElementById('platforms-list');
c.textContent = '';
for (const [key, platform] of Object.entries(PLATFORMS)) {
c.appendChild(createPlatformCard(key, platform, status[key] || {}));
}
}
function createPlatformCard(key, platform, status) {
const card = document.createElement('div');
card.className = 'platform-card';
card.dataset.platform = key;
const isTokenOnly = platform.authType === 'token' && !['discord', 'pixiv'].includes(key);
const discordNeedsToken = key === 'discord' && !status.hasToken;
if (isTokenOnly || discordNeedsToken) card.classList.add('disabled');
const icon = document.createElement('div');
icon.className = 'platform-icon';
icon.style.background = platform.color;
icon.textContent = platform.name[0];
const info = document.createElement('div');
info.className = 'info';
const name = document.createElement('div');
name.className = 'name';
name.textContent = platform.name;
const st = document.createElement('div');
st.className = `status ${statusClass(status, platform, key)}`;
st.textContent = statusText(status, platform, key);
info.appendChild(name); info.appendChild(st);
const act = document.createElement('span');
act.className = 'action-icon';
act.textContent = '↥';
card.appendChild(icon); card.appendChild(info); card.appendChild(act);
if (!isTokenOnly && !discordNeedsToken) {
card.addEventListener('click', () => exportPlatformCookies(key, card));
}
return card;
}
function statusText(s, platform, key) {
if (key === 'discord') return s.hasToken ? 'Token captured — ready' : 'Open Discord to capture token';
if (key === 'pixiv') return s.hasToken ? 'Token captured — ready' : 'Click to authenticate via OAuth';
if (platform.authType === 'token') return 'Manual token entry required';
if (s.error) return 'Error checking cookies';
if (!s.hasCookies || !s.cookieCount) return 'No cookies — log in first';
return `${s.cookieCount} cookies ready`;
}
function statusClass(s, platform, key) {
if (key === 'discord') return s.hasToken ? 'ready' : 'no-cookies';
if (key === 'pixiv') return s.hasToken ? 'ready' : 'no-cookies';
if (platform.authType === 'token') return 'no-cookies';
if (s.error) return 'error';
if (!s.hasCookies || !s.cookieCount) return 'no-cookies';
return 'ready';
}
async function exportPlatformCookies(key, card) {
card.classList.add('loading'); hideStatusMessage();
try {
const r = await browser.runtime.sendMessage({ type: 'EXPORT_COOKIES', platform: key });
if (r.error) showError(r.error);
else {
const n = r.cookieCount ?? null;
const verifiedSuffix = r.verified ? ' (verified ✓)' : '';
const msg = n !== null
? `${PLATFORMS[key].name}: ${n} cookies exported${verifiedSuffix}`
: `${PLATFORMS[key].name}: token exported`;
showSuccess(msg);
await loadPlatformStatus();
}
} catch (e) { showError(e.message); }
finally { card.classList.remove('loading'); }
}
async function exportAllCookies() {
const btn = document.getElementById('export-all-btn');
btn.disabled = true; btn.textContent = 'Exporting…'; hideStatusMessage();
try {
const r = await browser.runtime.sendMessage({ type: 'EXPORT_ALL_COOKIES' });
const wins = Object.values(r).filter(x => x.success).length;
const fails = Object.values(r).filter(x => !x.success && !x.skipped).length;
if (wins && !fails) showSuccess(`Exported ${wins} platforms`);
else if (wins) showWarning(`${wins} succeeded, ${fails} failed`);
else if (fails) showError('All exports failed. Are you logged in?');
else showWarning('Nothing to export');
await loadPlatformStatus();
} catch (e) { showError(e.message); }
finally { btn.disabled = false; btn.textContent = 'Export all platforms'; }
}
async function loadSources() {
const c = document.getElementById('sources-list');
c.textContent = '';
const d = document.createElement('div');
d.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
d.textContent = 'Loading sources…';
c.appendChild(d);
const r = await browser.runtime.sendMessage({ type: 'LIST_SOURCES' });
c.textContent = '';
if (r.error) {
const e = document.createElement('div');
e.style.cssText = 'padding:12px;color:var(--error);';
e.textContent = r.error;
c.appendChild(e);
return;
}
if (!r.sources || r.sources.length === 0) {
const empty = document.createElement('div');
empty.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
empty.textContent = 'No sources yet.';
c.appendChild(empty);
return;
}
for (const src of r.sources) c.appendChild(createSourceRow(src));
}
function createSourceRow(src) {
const row = document.createElement('div');
row.className = 'source-row';
const info = document.createElement('div');
info.className = 'info';
const name = document.createElement('div');
name.className = 'name';
name.textContent = `${src.platform} · #${src.id}`;
const url = document.createElement('div');
url.className = 'url';
url.textContent = src.url;
info.appendChild(name); info.appendChild(url);
const play = document.createElement('button');
play.className = 'play';
play.textContent = '▶';
play.title = 'Check now';
play.addEventListener('click', async () => {
play.disabled = true;
const r = await browser.runtime.sendMessage({ type: 'CHECK_SOURCE', sourceId: src.id });
play.disabled = false;
if (r.error) showError(r.error);
else showSuccess(`Triggered check for source #${src.id}`);
});
row.appendChild(info); row.appendChild(play);
return row;
}
function setupEventListeners() {
document.getElementById('export-all-btn').addEventListener('click', exportAllCookies);
document.getElementById('settings-btn').addEventListener('click', () => browser.runtime.openOptionsPage());
for (const tab of document.querySelectorAll('.tab')) {
tab.addEventListener('click', () => {
for (const t of document.querySelectorAll('.tab')) t.classList.remove('active');
tab.classList.add('active');
for (const p of document.querySelectorAll('.tab-panel')) p.classList.add('hidden');
document.getElementById(`tab-${tab.dataset.tab}`).classList.remove('hidden');
if (tab.dataset.tab === 'sources') loadSources();
});
}
}
function showSuccess(m) { showStatusMessage(m, 'success'); }
function showError(m) { showStatusMessage(m, 'error'); }
function showWarning(m) { showStatusMessage(m, 'warning'); }
function showStatusMessage(text, kind) {
const el = document.getElementById('status-message');
el.textContent = text;
el.className = `status-message ${kind}`;
el.classList.remove('hidden');
}
function hideStatusMessage() {
document.getElementById('status-message').classList.add('hidden');
}