feat: harden authenticated deployment
This commit is contained in:
+24
-1
@@ -1 +1,24 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Roast Planner Admin</title><link rel="stylesheet" href="/app.css"></head><body><main class="auth-page"><section class="panel-card auth-card"><a href="/app">← Plans</a><h1>Administration</h1><label><input id="signup" type="checkbox"> Allow new signups</label><button id="save" class="primary-btn">Save setting</button><h2>Users</h2><ul id="users"></ul><h2>Recent roast plans</h2><ul id="plans"></ul></section></main><script type="module">const csrf=()=>document.cookie.split('; ').find(v=>v.startsWith('rp_csrf='))?.split('=')[1]||'';const users=document.querySelector('#users'),plans=document.querySelector('#plans');async function load(){const b=await(await fetch('/api/admin/users')).json();document.querySelector('#signup').checked=b.signupEnabled;users.replaceChildren(...b.users.map(u=>Object.assign(document.createElement('li'),{textContent:`${u.email} (${u.role}) — ${u.plan_count} plans`})));const p=await(await fetch('/api/admin/plans')).json();plans.replaceChildren(...p.plans.map(x=>Object.assign(document.createElement('li'),{textContent:`${x.email}: ${x.plan?.fields?.['0.1']||'Untitled plan'}`})));}document.querySelector('#save').onclick=async()=>{await fetch('/api/admin/signup-enabled',{method:'PUT',headers:{'content-type':'application/json','x-csrf-token':csrf()},body:JSON.stringify({enabled:document.querySelector('#signup').checked})});await load();};load();</script></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Roast Planner Admin</title>
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="auth-page">
|
||||
<section class="panel-card auth-card">
|
||||
<a href="/app">← Plans</a>
|
||||
<h1>Administration</h1>
|
||||
<label><input id="signup" type="checkbox" /> Allow new signups</label
|
||||
><button id="save" class="primary-btn">Save setting</button>
|
||||
<h2>Users</h2>
|
||||
<ul id="users"></ul>
|
||||
<h2>Recent roast plans</h2>
|
||||
<ul id="plans"></ul>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1975
-741
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
const csrf = () =>
|
||||
document.cookie
|
||||
.split("; ")
|
||||
.find((value) => value.startsWith("rp_csrf="))
|
||||
?.split("=")[1] || "";
|
||||
const users = document.querySelector("#users");
|
||||
const plans = document.querySelector("#plans");
|
||||
|
||||
async function load() {
|
||||
const usersResponse = await fetch("/api/admin/users");
|
||||
if (!usersResponse.ok) return;
|
||||
const body = await usersResponse.json();
|
||||
document.querySelector("#signup").checked = body.signupEnabled;
|
||||
users.replaceChildren(
|
||||
...body.users.map((user) =>
|
||||
Object.assign(document.createElement("li"), {
|
||||
textContent: `${user.email} (${user.role}) — ${user.plan_count} plans`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const plansResponse = await fetch("/api/admin/plans");
|
||||
if (!plansResponse.ok) return;
|
||||
const plansBody = await plansResponse.json();
|
||||
plans.replaceChildren(
|
||||
...plansBody.plans.map((plan) =>
|
||||
Object.assign(document.createElement("li"), {
|
||||
textContent: `${plan.email}: ${plan.plan?.fields?.["0.1"] || "Untitled plan"}`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
document.querySelector("#save").addEventListener("click", async () => {
|
||||
await fetch("/api/admin/signup-enabled", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-csrf-token": csrf(),
|
||||
},
|
||||
body: JSON.stringify({ enabled: document.querySelector("#signup").checked }),
|
||||
});
|
||||
await load();
|
||||
});
|
||||
|
||||
load();
|
||||
+90
-7
@@ -1,9 +1,92 @@
|
||||
const csrf = () => document.cookie.split("; ").find((v) => v.startsWith("rp_csrf="))?.split("=")[1] || "";
|
||||
const setText = (el, text, error = false) => { const node = document.createElement("p"); node.textContent = text; if (error) node.style.color="#a8371a"; el.replaceChildren(node); };
|
||||
const csrf = () =>
|
||||
document.cookie
|
||||
.split("; ")
|
||||
.find((v) => v.startsWith("rp_csrf="))
|
||||
?.split("=")[1] || "";
|
||||
const setText = (el, text, error = false) => {
|
||||
const node = document.createElement("p");
|
||||
node.textContent = text;
|
||||
if (error) node.style.color = "#a8371a";
|
||||
el.replaceChildren(node);
|
||||
};
|
||||
export function initAlogPanel({ state, recompute }) {
|
||||
const fileInput=document.getElementById("alog-file"), libraryBtn=document.getElementById("alog-library-refresh"), libraryList=document.getElementById("alog-library-list"), resultEl=document.getElementById("alog-result");
|
||||
fileInput.addEventListener("change", async(e)=>{const file=e.target.files?.[0];if(!file)return;setText(resultEl,"Parsing…");try{const res=await fetch("/api/alog",{method:"POST",headers:{"content-type":"application/json","x-csrf-token":csrf()},body:JSON.stringify({filename:file.name,content:await file.text()})});applyResult(await res.json());}catch(err){setText(resultEl,err.message,true);}});
|
||||
libraryBtn.addEventListener("click",async()=>{libraryList.classList.remove("hidden");libraryList.replaceChildren(Object.assign(document.createElement("li"),{textContent:"Loading…"}));try{const body=await (await fetch("/api/alog/library")).json();if(!body.ok||!body.files.length){libraryList.replaceChildren(Object.assign(document.createElement("li"),{textContent:"No .alog files found. Set ALOG_DIR or drop files in ~/Roastetta."}));return;}libraryList.replaceChildren(...body.files.map(f=>{const li=document.createElement("li");li.textContent=`${f.filename} (${Math.round(f.sizeBytes/1024)} KB)`;li.onclick=async()=>{setText(resultEl,"Loading…");applyResult(await (await fetch(`/api/alog/library/${encodeURIComponent(f.filename)}`)).json());};return li;}));}catch(err){const li=document.createElement("li");li.textContent=err.message;li.style.color="#a8371a";libraryList.replaceChildren(li);}});
|
||||
function applyResult(body){if(!body.ok){setText(resultEl,body.error??body.code,true);return;}state.plan.reference=body;recompute();setText(resultEl,`${body.roast.title} — ${body.roast.roastDate||"no date"}; first crack ${fmt(body.derived?.firstCrackS)}; development ${fmt(body.derived?.developmentS)}; drop ${fmt(body.derived?.dropS)}; DTR ${body.derived?.dtrPct??"—"}%.`);}
|
||||
const fileInput = document.getElementById("alog-file"),
|
||||
libraryBtn = document.getElementById("alog-library-refresh"),
|
||||
libraryList = document.getElementById("alog-library-list"),
|
||||
resultEl = document.getElementById("alog-result");
|
||||
fileInput.addEventListener("change", async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setText(resultEl, "Parsing…");
|
||||
try {
|
||||
const res = await fetch("/api/alog", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "x-csrf-token": csrf() },
|
||||
body: JSON.stringify({
|
||||
filename: file.name,
|
||||
content: await file.text(),
|
||||
}),
|
||||
});
|
||||
applyResult(await res.json());
|
||||
} catch (err) {
|
||||
setText(resultEl, err.message, true);
|
||||
}
|
||||
});
|
||||
libraryBtn.addEventListener("click", async () => {
|
||||
libraryList.classList.remove("hidden");
|
||||
libraryList.replaceChildren(
|
||||
Object.assign(document.createElement("li"), { textContent: "Loading…" }),
|
||||
);
|
||||
try {
|
||||
const body = await (await fetch("/api/alog/library")).json();
|
||||
if (!body.ok || !body.files.length) {
|
||||
libraryList.replaceChildren(
|
||||
Object.assign(document.createElement("li"), {
|
||||
textContent:
|
||||
"No .alog files found. Set ALOG_DIR or drop files in ~/Roastetta.",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
libraryList.replaceChildren(
|
||||
...body.files.map((f) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${f.filename} (${Math.round(f.sizeBytes / 1024)} KB)`;
|
||||
li.onclick = async () => {
|
||||
setText(resultEl, "Loading…");
|
||||
applyResult(
|
||||
await (
|
||||
await fetch(
|
||||
`/api/alog/library/${encodeURIComponent(f.filename)}`,
|
||||
)
|
||||
).json(),
|
||||
);
|
||||
};
|
||||
return li;
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = err.message;
|
||||
li.style.color = "#a8371a";
|
||||
libraryList.replaceChildren(li);
|
||||
}
|
||||
});
|
||||
function applyResult(body) {
|
||||
if (!body.ok) {
|
||||
setText(resultEl, body.error ?? body.code, true);
|
||||
return;
|
||||
}
|
||||
state.plan.reference = body;
|
||||
recompute();
|
||||
setText(
|
||||
resultEl,
|
||||
`${body.roast.title} — ${body.roast.roastDate || "no date"}; first crack ${fmt(body.derived?.firstCrackS)}; development ${fmt(body.derived?.developmentS)}; drop ${fmt(body.derived?.dropS)}; DTR ${body.derived?.dtrPct ?? "—"}%.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
function fmt(seconds) {
|
||||
if (seconds == null) return "—";
|
||||
const s = Math.round(seconds);
|
||||
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
||||
}
|
||||
function fmt(seconds){if(seconds==null)return "—";const s=Math.round(seconds);return `${Math.floor(s/60)}:${String(s%60).padStart(2,"0")}`;}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const form = document.querySelector("#auth-form");
|
||||
const message = document.querySelector("#message");
|
||||
|
||||
async function submit(url) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(Object.fromEntries(new FormData(form))),
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) {
|
||||
message.textContent = body.error || body.code;
|
||||
return;
|
||||
}
|
||||
location.assign("/app");
|
||||
}
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
submit("/api/auth/login");
|
||||
});
|
||||
document.querySelector("#signup").addEventListener("click", () =>
|
||||
submit("/api/auth/signup"),
|
||||
);
|
||||
+549
-416
File diff suppressed because it is too large
Load Diff
+96
-17
@@ -1,21 +1,100 @@
|
||||
// URL prefill UI; output is always built with DOM nodes so remote text never becomes markup.
|
||||
const csrf = () => document.cookie.split("; ").find((v) => v.startsWith("rp_csrf="))?.split("=")[1] || "";
|
||||
function message(target, text, error = false) { const p = document.createElement("p"); p.textContent = text; if (error) p.style.color = "#a8371a"; target.replaceChildren(p); }
|
||||
const csrf = () =>
|
||||
document.cookie
|
||||
.split("; ")
|
||||
.find((v) => v.startsWith("rp_csrf="))
|
||||
?.split("=")[1] || "";
|
||||
function message(target, text, error = false) {
|
||||
const p = document.createElement("p");
|
||||
p.textContent = text;
|
||||
if (error) p.style.color = "#a8371a";
|
||||
target.replaceChildren(p);
|
||||
}
|
||||
|
||||
export function initPrefillPanel({ state, renderFormFromPlan, recompute }) {
|
||||
const urlInput = document.getElementById("prefill-url"), overwriteBox = document.getElementById("prefill-overwrite"), goBtn = document.getElementById("prefill-go"), undoBtn = document.getElementById("prefill-undo"), resultEl = document.getElementById("prefill-result"); let snapshot = null;
|
||||
goBtn.addEventListener("click", async () => {
|
||||
const url = urlInput.value.trim(); if (!url) return; goBtn.disabled = true; message(resultEl, "Fetching…");
|
||||
try {
|
||||
const res = await fetch("/api/prefill", { method: "POST", headers: { "content-type": "application/json", "x-csrf-token": csrf() }, body: JSON.stringify({ url }) }); const body = await res.json();
|
||||
if (!body.ok) { message(resultEl, body.error ?? body.code, true); return; }
|
||||
snapshot = structuredClone(state.plan); let applied = 0;
|
||||
for (const [id, value] of Object.entries(body.fields ?? {})) { if (!overwriteBox.checked && (state.plan.fields[id] ?? "") !== "") continue; state.plan.fields[id] = value; applied++; }
|
||||
renderFormFromPlan(); markPrefilled(Object.keys(body.fields ?? {}), body.provenance ?? {}); recompute(); undoBtn.disabled = false;
|
||||
const p = document.createElement("p"), link = document.createElement("a"); link.href = body.source.finalUrl; link.target = "_blank"; link.rel = "noopener"; link.textContent = body.source.finalUrl; p.append(`Applied ${applied} field${applied === 1 ? "" : "s"} from `, link, ".");
|
||||
const children = [p]; if (body.warnings?.length) { const ul=document.createElement("ul"); ul.className="warnings"; for(const warning of body.warnings){const li=document.createElement("li");li.textContent=warning;ul.append(li);} children.push(ul); } resultEl.replaceChildren(...children);
|
||||
} catch (err) { message(resultEl, err.message, true); } finally { goBtn.disabled = false; }
|
||||
});
|
||||
undoBtn.addEventListener("click", () => { if (!snapshot) return; state.plan = snapshot; snapshot = null; undoBtn.disabled = true; renderFormFromPlan(); for (const el of document.querySelectorAll(".prefilled")) { el.classList.remove("prefilled"); el.removeAttribute("title"); } recompute(); message(resultEl, "Prefill undone."); });
|
||||
function markPrefilled(ids, provenance) { for (const id of ids) { const el=document.querySelector(`[name="${CSS.escape(id)}"]`); if(el){el.classList.add("prefilled");if(provenance[id])el.title=`from: ${provenance[id]}`;} } }
|
||||
const urlInput = document.getElementById("prefill-url"),
|
||||
overwriteBox = document.getElementById("prefill-overwrite"),
|
||||
goBtn = document.getElementById("prefill-go"),
|
||||
undoBtn = document.getElementById("prefill-undo"),
|
||||
resultEl = document.getElementById("prefill-result");
|
||||
let snapshot = null;
|
||||
goBtn.addEventListener("click", async () => {
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) return;
|
||||
goBtn.disabled = true;
|
||||
message(resultEl, "Fetching…");
|
||||
try {
|
||||
const res = await fetch("/api/prefill", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "x-csrf-token": csrf() },
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
const body = await res.json();
|
||||
if (!body.ok) {
|
||||
message(resultEl, body.error ?? body.code, true);
|
||||
return;
|
||||
}
|
||||
snapshot = structuredClone(state.plan);
|
||||
let applied = 0;
|
||||
for (const [id, value] of Object.entries(body.fields ?? {})) {
|
||||
if (!overwriteBox.checked && (state.plan.fields[id] ?? "") !== "")
|
||||
continue;
|
||||
state.plan.fields[id] = value;
|
||||
applied++;
|
||||
}
|
||||
renderFormFromPlan();
|
||||
markPrefilled(Object.keys(body.fields ?? {}), body.provenance ?? {});
|
||||
recompute();
|
||||
undoBtn.disabled = false;
|
||||
const p = document.createElement("p"),
|
||||
link = document.createElement("a");
|
||||
link.href = body.source.finalUrl;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener";
|
||||
link.textContent = body.source.finalUrl;
|
||||
p.append(
|
||||
`Applied ${applied} field${applied === 1 ? "" : "s"} from `,
|
||||
link,
|
||||
".",
|
||||
);
|
||||
const children = [p];
|
||||
if (body.warnings?.length) {
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "warnings";
|
||||
for (const warning of body.warnings) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = warning;
|
||||
ul.append(li);
|
||||
}
|
||||
children.push(ul);
|
||||
}
|
||||
resultEl.replaceChildren(...children);
|
||||
} catch (err) {
|
||||
message(resultEl, err.message, true);
|
||||
} finally {
|
||||
goBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
undoBtn.addEventListener("click", () => {
|
||||
if (!snapshot) return;
|
||||
state.plan = snapshot;
|
||||
snapshot = null;
|
||||
undoBtn.disabled = true;
|
||||
renderFormFromPlan();
|
||||
for (const el of document.querySelectorAll(".prefilled")) {
|
||||
el.classList.remove("prefilled");
|
||||
el.removeAttribute("title");
|
||||
}
|
||||
recompute();
|
||||
message(resultEl, "Prefill undone.");
|
||||
});
|
||||
function markPrefilled(ids, provenance) {
|
||||
for (const id of ids) {
|
||||
const el = document.querySelector(`[name="${CSS.escape(id)}"]`);
|
||||
if (el) {
|
||||
el.classList.add("prefilled");
|
||||
if (provenance[id]) el.title = `from: ${provenance[id]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-1
@@ -1 +1,46 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Roast Planner</title><link rel="stylesheet" href="/app.css"><meta name="theme-color" content="#A8481A"></head><body><main class="auth-page"><section class="panel-card auth-card"><h1>Roast Planner</h1><p>Build, save, and revisit your coffee roast plans.</p><div id="message" role="status"></div><form id="auth-form"><label>Email <input class="field-input" required type="email" name="email" autocomplete="email"></label><label>Password <input class="field-input" required type="password" minlength="12" name="password" autocomplete="current-password"></label><button class="primary-btn" type="submit">Log in</button><button class="ghost-btn" type="button" id="signup">Create account</button></form><p class="muted">Accounts require a password of at least 12 characters.</p></section></main><script type="module">const f=document.querySelector('#auth-form'),m=document.querySelector('#message');async function submit(url){const r=await fetch(url,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(Object.fromEntries(new FormData(f)))});const b=await r.json();if(!r.ok){m.textContent=b.error||b.code;return;}location='/app';}f.onsubmit=e=>{e.preventDefault();submit('/api/auth/login')};document.querySelector('#signup').onclick=()=>submit('/api/auth/signup');</script></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Roast Planner</title>
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
<meta name="theme-color" content="#A8481A" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="auth-page">
|
||||
<section class="panel-card auth-card">
|
||||
<h1>Roast Planner</h1>
|
||||
<p>Build, save, and revisit your coffee roast plans.</p>
|
||||
<div id="message" role="status"></div>
|
||||
<form id="auth-form">
|
||||
<label
|
||||
>Email
|
||||
<input
|
||||
class="field-input"
|
||||
required
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="email" /></label
|
||||
><label
|
||||
>Password
|
||||
<input
|
||||
class="field-input"
|
||||
required
|
||||
type="password"
|
||||
minlength="12"
|
||||
name="password"
|
||||
autocomplete="current-password" /></label
|
||||
><button class="primary-btn" type="submit">Log in</button
|
||||
><button class="ghost-btn" type="button" id="signup">
|
||||
Create account
|
||||
</button>
|
||||
</form>
|
||||
<p class="muted">
|
||||
Accounts require a password of at least 12 characters.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="/js/landing.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+68
-46
@@ -1,59 +1,81 @@
|
||||
const CACHE_NAME = "roast-planner-static-v1";
|
||||
const CACHE_NAME = "roast-planner-static-v2";
|
||||
const APP_SHELL = [
|
||||
"/",
|
||||
"/landing.html",
|
||||
"/app.css",
|
||||
"/worksheet.css",
|
||||
"/manifest.webmanifest",
|
||||
"/icon.svg",
|
||||
"/js/main.js",
|
||||
"/js/prefill-ui.js",
|
||||
"/js/alog-ui.js",
|
||||
"/js/print.js",
|
||||
"/shared/fields.js",
|
||||
"/shared/ledger.js",
|
||||
"/shared/time.js",
|
||||
"/shared/reference-data.js",
|
||||
"/shared/curve.js",
|
||||
"/",
|
||||
"/landing.html",
|
||||
"/app.css",
|
||||
"/worksheet.css",
|
||||
"/manifest.webmanifest",
|
||||
"/icon.svg",
|
||||
"/js/main.js",
|
||||
"/js/landing.js",
|
||||
"/js/admin.js",
|
||||
"/js/prefill-ui.js",
|
||||
"/js/alog-ui.js",
|
||||
"/js/print.js",
|
||||
"/shared/fields.js",
|
||||
"/shared/ledger.js",
|
||||
"/shared/time.js",
|
||||
"/shared/reference-data.js",
|
||||
"/shared/curve.js",
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)));
|
||||
self.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)),
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))),
|
||||
);
|
||||
self.clients.claim();
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((key) => key !== CACHE_NAME)
|
||||
.map((key) => caches.delete(key)),
|
||||
),
|
||||
),
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
let url;
|
||||
try {
|
||||
url = new URL(request.url);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (request.method !== "GET" || url.origin !== self.location.origin || url.pathname.startsWith("/api/")) return;
|
||||
const { request } = event;
|
||||
let url;
|
||||
try {
|
||||
url = new URL(request.url);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
request.method !== "GET" ||
|
||||
url.origin !== self.location.origin ||
|
||||
url.pathname.startsWith("/api/")
|
||||
)
|
||||
return;
|
||||
|
||||
if (request.mode === "navigate") {
|
||||
// Never serve authenticated application HTML from cache after logout.
|
||||
event.respondWith(fetch(request).catch(() => caches.match("/landing.html")));
|
||||
return;
|
||||
}
|
||||
if (request.mode === "navigate") {
|
||||
// Never serve authenticated application HTML from cache after logout.
|
||||
event.respondWith(
|
||||
fetch(request).catch(() => caches.match("/landing.html")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
const update = fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok) caches.open(CACHE_NAME).then((cache) => cache.put(request, response.clone()));
|
||||
return response;
|
||||
})
|
||||
.catch(() => cached);
|
||||
return cached ?? update;
|
||||
}),
|
||||
);
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
const update = fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok)
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.put(request, response.clone()));
|
||||
return response;
|
||||
})
|
||||
.catch(() => cached);
|
||||
return cached ?? update;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user