Checkpoint before Personal-auth refactor

This commit is contained in:
Dwindi Ramadhana
2026-02-17 13:15:07 +07:00
parent 4c79f1ad04
commit 901f81b7a9
13 changed files with 320 additions and 31 deletions

171
panel.js Normal file → Executable file
View File

@@ -107,6 +107,7 @@ function setLicenseBusy(on, label){
}
const diagRunBtn = document.getElementById('diag-run');
const diagForceBtn = document.getElementById('diag-force');
const diagSpin = document.getElementById('diag-spin');
const diagOut = document.getElementById('diag-out');
@@ -153,11 +154,53 @@ function categoryComparator(a, b) {
return a.localeCompare(b, undefined, { sensitivity: "base" }); // both not preferred → AZ
}
// === Extension verification token (GCM registration) ===
const EXT_TOKEN_KEY = 'dewemojiExtToken';
const EXT_TOKEN_COOLDOWN_MS = 60000;
let extTokenPromise = null;
let extTokenLastAttempt = 0;
async function getExtensionToken(force=false) {
if (extTokenPromise) return extTokenPromise;
extTokenPromise = (async () => {
try {
const got = await chrome.storage.local.get([EXT_TOKEN_KEY]);
if (got[EXT_TOKEN_KEY] && !force) return got[EXT_TOKEN_KEY];
if (!force && extTokenLastAttempt && Date.now() - extTokenLastAttempt < EXT_TOKEN_COOLDOWN_MS) {
return null;
}
extTokenLastAttempt = Date.now();
const res = await chrome.runtime.sendMessage({ type: 'dewemoji_get_ext_token', force });
if (res?.token) {
await chrome.storage.local.set({ [EXT_TOKEN_KEY]: res.token });
return res.token;
}
return null;
} catch (err) {
console.error('Token retrieval failed:', err);
return null;
}
})().finally(() => {
extTokenPromise = null;
});
return extTokenPromise;
}
async function getExtensionTokenCached() {
try {
const got = await chrome.storage.local.get([EXT_TOKEN_KEY]);
return got[EXT_TOKEN_KEY] || null;
} catch {
return null;
}
}
async function loadCategories() {
// Try live endpoint; fall back silently to local list (no subs)
let ok = false;
try {
const res = await fetch(`${API.base}${API.cats}`);
const headers = await buildHeaders();
const res = await fetch(`${API.base}${API.cats}`, { headers });
if (res && res.ok) {
const data = await res.json();
// Normalize payloads:
@@ -579,9 +622,12 @@ async function savePersistedCache(cacheObj){
}
function isFresh(ts){ return (Date.now() - ts) < CACHE_TTL_MS; }
function buildHeaders(){
async function buildHeaders(){
const base = { 'X-Dewemoji-Frontend': FRONTEND_ID };
if (licenseValid && licenseKeyCurrent) {
try {
if (chrome?.runtime?.id) base['X-Extension-Id'] = chrome.runtime.id;
} catch {}
if (licenseValid && licenseKeyCurrent) {
try {
const acctPromise = accountId();
return acctPromise.then(acct => ({
@@ -596,9 +642,63 @@ function buildHeaders(){
return Promise.resolve({ ...base, 'Authorization': `Bearer ${licenseKeyCurrent}`, 'X-License-Key': licenseKeyCurrent });
}
}
return Promise.resolve(base);
return base;
}
function getListPaths(){
// Try newest route first, then legacy-compatible fallbacks.
return ['/extension/search', '/emojis', '/search'];
}
function getApiBases(){
const list = [
API.base,
'https://dewemoji.backoffice.biz.id/v1',
'https://api.dewemoji.com/v1',
'http://127.0.0.1:8000/v1',
];
const seen = new Set();
const out = [];
for (const raw of list) {
const base = String(raw || '').trim().replace(/\/+$/, '');
if (!base || seen.has(base)) continue;
seen.add(base);
out.push(base);
}
return out;
}
async function fetchJsonWithFallback(params, headers, { quiet = false } = {}) {
let lastRes = null;
const tried = [];
const paths = getListPaths();
const bases = getApiBases();
for (const base of bases) {
for (const path of paths) {
const url = `${base}${path}?${params.toString()}`;
try {
const res = await fetch(url, { cache: 'no-store', headers });
tried.push(`${base}${path}:${res.status}`);
if (res.ok) {
const data = await res.json().catch(() => null);
// Lock to first healthy base to keep future requests fast.
API.base = base;
return { res, data, path, base };
}
lastRes = res;
} catch (err) {
const msg = err && err.message ? err.message : 'network_error';
tried.push(`${base}${path}:${msg}`);
}
}
}
if (quiet) return { res: lastRes, data: null, path: null, base: null };
throw new Error(`API ${lastRes ? lastRes.status : 0} (${tried.join(' | ')})`);
}
async function prefetchNextIfNeeded(currentSig){
try {
// only prefetch when there are more results and we know the next page index
@@ -622,15 +722,10 @@ async function prefetchNextIfNeeded(currentSig){
if (q.value.trim()) params.set('q', q.value.trim());
if (cat.value.trim()) params.set('category', cat.value.trim());
if (sub.value.trim()) params.set('subcategory', sub.value.trim());
const url = `${API.base}${API.list}?${params.toString()}`;
// headers (with optional license/account)
const headers = await buildHeaders();
// fetch quietly; avoid throwing UI errors — prefetch is best-effort
const res = await fetch(url, { cache: 'no-store', headers });
if (!res.ok) { PREFETCHING.delete(key); return; }
const data = await res.json().catch(()=>null);
const { data } = await fetchJsonWithFallback(params, headers, { quiet: true });
if (!data || !Array.isArray(data.items)) { PREFETCHING.delete(key); return; }
const record = { ts: Date.now(), data };
@@ -638,7 +733,9 @@ async function prefetchNextIfNeeded(currentSig){
const persisted2 = await getPersistedCache();
persisted2[key] = record;
// prune stale
for (const k of Object.keys(persisted2)) { if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k]; }
for (const k of Object.keys(persisted2)) {
if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k];
}
await savePersistedCache(persisted2);
} catch { /* silent */ }
finally { PREFETCHING.delete(`${currentSig}|${page+1}`); }
@@ -712,12 +809,10 @@ async function fetchPage(reset=false) {
if (q.value.trim()) params.set('q', q.value.trim());
if (cat.value.trim()) params.set('category', cat.value.trim());
if (sub.value.trim()) params.set('subcategory', sub.value.trim());
const url = `${API.base}${API.list}?${params.toString()}`;
const headers = await buildHeaders();
// (usage increment moved to after successful fetch)
const headers = await buildHeaders();
// Optional nicety: offline guard (early exit if no network AND no fresh cache)
if (!navigator.onLine) {
const hasFreshMem = mem && isFresh(mem.ts);
@@ -727,11 +822,9 @@ async function fetchPage(reset=false) {
}
}
const res = await fetch(url, { cache: 'no-store', headers });
if (!res.ok) throw new Error(`API ${res.status}`);
const { res, data } = await fetchJsonWithFallback(params, headers);
if (!res || !data) throw new Error('API 0');
lastServerTier = res.headers.get('X-Dewemoji-Tier'); // 'pro' for Pro; null for Free/whitelist
const data = await res.json();
total = data.total || 0;
if (page === 1 && (!Array.isArray(data.items) || data.items.length === 0)) {
@@ -1525,11 +1618,30 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
}
});
function renderDiag(obj) {
async function renderDiag(obj) {
const lines = [];
const token = await getExtensionTokenCached();
lines.push(`GCM Token: ${token ? '✅ Received' : '❌ Missing'}`);
if (token) lines.push(`Token ID: ${token.slice(0, 10)}`);
try {
const lock = await chrome.storage.local.get(['dewemojiExtTokenLockTs']);
const ts = lock?.dewemojiExtTokenLockTs;
if (ts) {
const ageSec = Math.max(0, Math.round((Date.now() - ts) / 1000));
lines.push(`GCM Lock: ${ageSec}s ago`);
}
} catch {}
try {
const err = await chrome.storage.local.get(['dewemojiExtTokenLastError','dewemojiExtTokenLastErrorTs']);
if (err?.dewemojiExtTokenLastError) {
const ts = err?.dewemojiExtTokenLastErrorTs;
const ageSec = ts ? Math.max(0, Math.round((Date.now() - ts) / 1000)) : null;
lines.push(`GCM Error: ${err.dewemojiExtTokenLastError}${ageSec != null ? ` (${ageSec}s ago)` : ''}`);
}
} catch {}
lines.push(`Content script loaded: ${obj ? 'yes' : 'no'}`);
if (!obj) return lines.join('\n');
lines.push(`Active editable type: ${obj.activeType ?? 'none'}`);
lines.push(`Has caret/selection: ${obj.hasRange ? 'yes' : 'no'}`);
lines.push(`Last insert result: ${obj.lastInsertOK === null ? 'n/a' : obj.lastInsertOK ? 'success' : 'failed'}`);
@@ -1537,25 +1649,34 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
return lines.join('\n');
}
diagRunBtn?.addEventListener('click', async () => {
async function runDiagnostics({ forceToken = false } = {}) {
diagOut.textContent = '';
diagSpin.style.display = 'inline';
try {
if (forceToken) {
const timeoutMs = 5000;
const tokenPromise = getExtensionToken(true);
const timeoutPromise = new Promise((_, rej) => setTimeout(() => rej(new Error('Token refresh timed out')), timeoutMs));
await Promise.race([tokenPromise, timeoutPromise]);
}
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) { diagOut.textContent = 'No active tab.'; return; }
const ready = await ensureContentScript(tab.id);
if (!ready) { diagOut.textContent = renderDiag(null); return; }
if (!ready) { diagOut.textContent = await renderDiag(null); return; }
const info = await chrome.tabs.sendMessage(tab.id, { type: 'dewemoji_diag' });
diagOut.textContent = renderDiag(info || null);
diagOut.textContent = await renderDiag(info || null);
} catch (e) {
diagOut.textContent = `Error: ${e?.message || e}`;
} finally {
diagSpin.style.display = 'none';
}
});
}
diagRunBtn?.addEventListener('click', () => runDiagnostics({ forceToken: false }));
diagForceBtn?.addEventListener('click', () => runDiagnostics({ forceToken: true }));
async function performEmojiAction(glyph) {
// Free users always copy
@@ -1852,4 +1973,4 @@ openSheet = function(){
document.getElementById('tab-general')?.classList.add('active');
document.getElementById('tab-pro')?.classList.remove('active');
renderToneSettingsSection();
};
};