diff --git a/assets/app.js b/assets/app.js
index f854b94..711f6a0 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -1,24 +1,58 @@
// PayToLaugh front end.
//
-// NOTE ON THE TOKEN BELOW: it is public by construction. A static page cannot
-// keep a secret, so the endpoint behind it is built to be safe in public
-// hands. It reads and writes small records only and cannot spend money.
-// Script and video generation are deliberately NOT reachable from here.
+// IDENTITY
+// The browser holds an opaque session token and nothing else. Every call
+// carries that token; the server resolves it to an account and ignores any
+// email in the payload. Sending an email address no longer proves anything,
+// which is the point — it used to.
//
-// NOTE ON "LOGIN": an email address identifies you, it does not authenticate
-// you. Anyone who knows an address can see that account. Temporary, by
-// agreement, until real auth lands.
+// THE MENU COMES FROM THE SERVER
+// The nav is built from what the server says this session may reach, rather
+// than rendered and then hidden. Hidden markup is not a permission; anyone
+// can unhide it. It also means a menu changes without a deploy.
+//
+// THE INVOKE TOKEN BELOW IS PUBLIC by construction — a static page cannot keep
+// a secret. It gates nothing on its own; the session does that.
var API = "https://api.corben.world/functions/da0474ea-4e56-405c-9f0d-35c3e4037a69/invoke"
+ "?token=97cd624cd2bd6a74eb692a940e71851f";
var PHOTO_KINDS = ["head shot", "profile", "full body", "sitting",
"playing", "sleeping", "action", "other"];
-// A plate is built from a head-on shot; these are the ones worth having.
var REFERENCE_KINDS = ["head shot", "profile", "full body"];
+var Session = {
+ tokKey: "ptl_session",
+ emailKey: "ptl_email",
+ menuKey: "ptl_menu",
+ nameKey: "ptl_name",
+ token: function () { try { return localStorage.getItem(this.tokKey); } catch (e) { return null; } },
+ get: function () { try { return localStorage.getItem(this.emailKey); } catch (e) { return null; } },
+ name: function () { try { return localStorage.getItem(this.nameKey) || ""; } catch (e) { return ""; } },
+ menu: function () {
+ try { return JSON.parse(localStorage.getItem(this.menuKey) || "null"); }
+ catch (e) { return null; }
+ },
+ start: function (r, email, name) {
+ try {
+ if (r && r.session) localStorage.setItem(this.tokKey, r.session);
+ if (email) localStorage.setItem(this.emailKey, email);
+ if (name) localStorage.setItem(this.nameKey, name);
+ if (r && r.menu) localStorage.setItem(this.menuKey, JSON.stringify(r.menu));
+ } catch (e) {}
+ },
+ clear: function () {
+ var self = this;
+ ["tokKey", "emailKey", "menuKey", "nameKey"].forEach(function (k) {
+ try { localStorage.removeItem(self[k]); } catch (e) {}
+ });
+ }
+};
+
async function api(action, payload) {
var body = Object.assign({ action: action }, payload || {});
+ var tok = Session.token();
+ if (tok) body.session = tok;
var res;
try {
res = await fetch(API, {
@@ -32,24 +66,18 @@ async function api(action, payload) {
var outer;
try { outer = await res.json(); }
catch (e) { return { ok: false, error: "The server sent something unreadable." }; }
- return outer && outer.result ? outer.result : outer;
-}
+ var r = (outer && outer.result) ? outer.result : outer;
-var Session = {
- key: "ptl_email",
- nameKey: "ptl_name",
- get: function () { try { return localStorage.getItem(this.key); } catch (e) { return null; } },
- name: function () { try { return localStorage.getItem(this.nameKey) || ""; } catch (e) { return ""; } },
- set: function (email, name) {
- try {
- localStorage.setItem(this.key, email);
- if (name) localStorage.setItem(this.nameKey, name);
- } catch (e) {}
- },
- clear: function () {
- try { localStorage.removeItem(this.key); localStorage.removeItem(this.nameKey); } catch (e) {}
+ // An expired or revoked session should land somewhere useful rather than
+ // failing oddly on every call afterwards.
+ if (r && r.signed_out) {
+ Session.clear();
+ if (!/register\.html/.test(location.pathname)) {
+ location.href = "/register.html?next=" + encodeURIComponent(location.pathname);
+ }
}
-};
+ return r;
+}
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>\"']/g, function (c) {
@@ -57,29 +85,52 @@ function esc(s) {
});
}
+// Menu items come from the server. What is cached renders immediately so the
+// nav does not flicker; the refresh then corrects it.
function navBar(current) {
var email = Session.get();
- var links = [["/", "Home", "home"], ["/pets.html", "My pets", "pets"],
- ["/create.html", "New video", "create"], ["/videos.html", "My videos", "videos"]];
- var html = '';
- document.body.insertAdjacentHTML("afterbegin", html);
- var b = document.getElementById("ptl-signout");
- if (b) b.onclick = function () { Session.clear(); location.href = "/register.html"; };
+ var items = Session.menu() || [{ id: "home", label: "Home", href: "/" }];
+
+ function paint(list) {
+ var old = document.querySelector("nav");
+ var html = '';
+ if (old) old.outerHTML = html;
+ else document.body.insertAdjacentHTML("afterbegin", html);
+ var b = document.getElementById("ptl-signout");
+ if (b) b.onclick = async function () {
+ await api("sign_out", {});
+ Session.clear();
+ location.href = "/register.html";
+ };
+ }
+
+ paint(items);
+ if (Session.token()) {
+ api("whoami", {}).then(function (r) {
+ if (r && r.ok && r.menu) {
+ try { localStorage.setItem(Session.menuKey, JSON.stringify(r.menu)); } catch (e) {}
+ paint(r.menu);
+ }
+ });
+ }
}
function requireSession() {
- var e = Session.get();
- if (!e) { location.href = "/register.html?next=" + encodeURIComponent(location.pathname); return null; }
- return e;
+ if (!Session.token()) {
+ location.href = "/register.html?next=" + encodeURIComponent(location.pathname);
+ return null;
+ }
+ return Session.get();
}
function showMsg(id, text, kind) {
@@ -100,9 +151,18 @@ function priceFor(segments) {
return { cost: cost, price: Math.round(cost * 5 * 100) / 100, seconds: segments * 8 };
}
+// Date AND time: several videos a day makes a bare date useless for telling
+// them apart.
+function when(s) {
+ if (!s) return "";
+ var d = new Date(s);
+ if (isNaN(d)) return s;
+ return d.toLocaleDateString(undefined, { month: "short", day: "numeric" })
+ + " " + d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
+}
+
// Shrink in the browser before upload. Phone photos run to 5-10MB, which is
-// slow to send and far more detail than a reference plate needs; 1600px is
-// ample. Returns base64 without the data: prefix.
+// slow to send and far more detail than a reference plate needs.
function resizeImage(file, maxPx, quality) {
maxPx = maxPx || 1600;
quality = quality || 0.85;
@@ -121,7 +181,7 @@ function resizeImage(file, maxPx, quality) {
c.width = cw; c.height = ch;
var g = c.getContext("2d");
g.fillStyle = "#ffffff";
- g.fillRect(0, 0, cw, ch); // flatten transparency for JPEG
+ g.fillRect(0, 0, cw, ch);
g.drawImage(img, 0, 0, cw, ch);
var url;
try { url = c.toDataURL("image/jpeg", quality); }