Session tokens, server-driven menu, and times on My videos
This commit is contained in:
parent
2b611917be
commit
c4bc712972
1 changed files with 105 additions and 45 deletions
150
assets/app.js
150
assets/app.js
|
|
@ -1,24 +1,58 @@
|
||||||
// PayToLaugh front end.
|
// PayToLaugh front end.
|
||||||
//
|
//
|
||||||
// NOTE ON THE TOKEN BELOW: it is public by construction. A static page cannot
|
// IDENTITY
|
||||||
// keep a secret, so the endpoint behind it is built to be safe in public
|
// The browser holds an opaque session token and nothing else. Every call
|
||||||
// hands. It reads and writes small records only and cannot spend money.
|
// carries that token; the server resolves it to an account and ignores any
|
||||||
// Script and video generation are deliberately NOT reachable from here.
|
// 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
|
// THE MENU COMES FROM THE SERVER
|
||||||
// you. Anyone who knows an address can see that account. Temporary, by
|
// The nav is built from what the server says this session may reach, rather
|
||||||
// agreement, until real auth lands.
|
// 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"
|
var API = "https://api.corben.world/functions/da0474ea-4e56-405c-9f0d-35c3e4037a69/invoke"
|
||||||
+ "?token=97cd624cd2bd6a74eb692a940e71851f";
|
+ "?token=97cd624cd2bd6a74eb692a940e71851f";
|
||||||
|
|
||||||
var PHOTO_KINDS = ["head shot", "profile", "full body", "sitting",
|
var PHOTO_KINDS = ["head shot", "profile", "full body", "sitting",
|
||||||
"playing", "sleeping", "action", "other"];
|
"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 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) {
|
async function api(action, payload) {
|
||||||
var body = Object.assign({ action: action }, payload || {});
|
var body = Object.assign({ action: action }, payload || {});
|
||||||
|
var tok = Session.token();
|
||||||
|
if (tok) body.session = tok;
|
||||||
var res;
|
var res;
|
||||||
try {
|
try {
|
||||||
res = await fetch(API, {
|
res = await fetch(API, {
|
||||||
|
|
@ -32,24 +66,18 @@ async function api(action, payload) {
|
||||||
var outer;
|
var outer;
|
||||||
try { outer = await res.json(); }
|
try { outer = await res.json(); }
|
||||||
catch (e) { return { ok: false, error: "The server sent something unreadable." }; }
|
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 = {
|
// An expired or revoked session should land somewhere useful rather than
|
||||||
key: "ptl_email",
|
// failing oddly on every call afterwards.
|
||||||
nameKey: "ptl_name",
|
if (r && r.signed_out) {
|
||||||
get: function () { try { return localStorage.getItem(this.key); } catch (e) { return null; } },
|
Session.clear();
|
||||||
name: function () { try { return localStorage.getItem(this.nameKey) || ""; } catch (e) { return ""; } },
|
if (!/register\.html/.test(location.pathname)) {
|
||||||
set: function (email, name) {
|
location.href = "/register.html?next=" + encodeURIComponent(location.pathname);
|
||||||
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) {}
|
|
||||||
}
|
}
|
||||||
};
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
return String(s == null ? "" : s).replace(/[&<>\"']/g, function (c) {
|
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) {
|
function navBar(current) {
|
||||||
var email = Session.get();
|
var email = Session.get();
|
||||||
var links = [["/", "Home", "home"], ["/pets.html", "My pets", "pets"],
|
var items = Session.menu() || [{ id: "home", label: "Home", href: "/" }];
|
||||||
["/create.html", "New video", "create"], ["/videos.html", "My videos", "videos"]];
|
|
||||||
var html = '<nav><div class="inner">'
|
function paint(list) {
|
||||||
+ '<a class="brand" href="/">Pay<span>To</span>Laugh</a>'
|
var old = document.querySelector("nav");
|
||||||
+ links.map(function (l) {
|
var html = '<nav><div class="inner">'
|
||||||
return '<a class="link' + (l[2] === current ? ' on' : '') + '" href="' + l[0] + '">' + l[1] + '</a>';
|
+ '<a class="brand" href="/">Pay<span>To</span>Laugh</a>'
|
||||||
}).join("")
|
+ list.map(function (l) {
|
||||||
+ '<div class="who">'
|
return '<a class="link' + (l.id === current ? ' on' : '') + '" href="'
|
||||||
+ (email
|
+ esc(l.href) + '">' + esc(l.label) + '</a>';
|
||||||
? esc(email) + ' <button type="button" id="ptl-signout">Sign out</button>'
|
}).join("")
|
||||||
: '<a class="link" href="/register.html">Sign in</a>')
|
+ '<div class="who">'
|
||||||
+ '</div></div></nav>';
|
+ (email
|
||||||
document.body.insertAdjacentHTML("afterbegin", html);
|
? esc(email) + ' <button type="button" id="ptl-signout">Sign out</button>'
|
||||||
var b = document.getElementById("ptl-signout");
|
: '<a class="link" href="/register.html">Sign in</a>')
|
||||||
if (b) b.onclick = function () { Session.clear(); location.href = "/register.html"; };
|
+ '</div></div></nav>';
|
||||||
|
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() {
|
function requireSession() {
|
||||||
var e = Session.get();
|
if (!Session.token()) {
|
||||||
if (!e) { location.href = "/register.html?next=" + encodeURIComponent(location.pathname); return null; }
|
location.href = "/register.html?next=" + encodeURIComponent(location.pathname);
|
||||||
return e;
|
return null;
|
||||||
|
}
|
||||||
|
return Session.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMsg(id, text, kind) {
|
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 };
|
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
|
// 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
|
// slow to send and far more detail than a reference plate needs.
|
||||||
// ample. Returns base64 without the data: prefix.
|
|
||||||
function resizeImage(file, maxPx, quality) {
|
function resizeImage(file, maxPx, quality) {
|
||||||
maxPx = maxPx || 1600;
|
maxPx = maxPx || 1600;
|
||||||
quality = quality || 0.85;
|
quality = quality || 0.85;
|
||||||
|
|
@ -121,7 +181,7 @@ function resizeImage(file, maxPx, quality) {
|
||||||
c.width = cw; c.height = ch;
|
c.width = cw; c.height = ch;
|
||||||
var g = c.getContext("2d");
|
var g = c.getContext("2d");
|
||||||
g.fillStyle = "#ffffff";
|
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);
|
g.drawImage(img, 0, 0, cw, ch);
|
||||||
var url;
|
var url;
|
||||||
try { url = c.toDataURL("image/jpeg", quality); }
|
try { url = c.toDataURL("image/jpeg", quality); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue