Tredje trin mod iOS-appen. I en browser deler fladen og API'et origin: hver sti er relativ, og sessionen er en httpOnly-cookie. I appen ligger fladen i appen selv på capacitor://localhost, og serveren er den, ejeren skrev ind. Alt, der er forskelligt, bor i den nye `platform.js`, så browserens svar er præcis, hvad det var før. - `apiURL()` og `authorize()`: relative stier og cookie i en browser; den valgte server og `Authorization: Bearer` i appen, `credentials: 'omit'`. - Login sender `client: app` og gemmer `session_token`; log ud fjerner det fra telefonen, også når serveren ikke svarede. - Live-synk i appen beder om en billet og åbner socket'en på serveren med den. - Første skærm i appen: "Hvilken Verdande vil du logge ind på?". Adressen prøves med /ping, før et adgangskodefelt vises, så en tastefejl, en router eller en server, der er nede, får sin egen sætning. Passkeys og opsætning af første konto vises ikke i appen; begge hører til serverens eget domæne. - Billeder og downloads under /api/v1 hentes med tokenet og vises fra en blob, i appen og kun dér (`nativemedia.js`). Stien bevares i `data-src`, og `richtext.js` læser den først, så en note aldrig gemmer en blob-adresse. - Service worker og Web Push er slået fra i appen: WKWebView har ingen af dem. Serveren: `VERDANDE_APP_ORIGINS` regner flere origins som appens. Den findes for E2E-projektet `app-origin`, der kører fladen fra app.localhost — samme server, anden origin — med en Capacitor-stub og uden cookie. Prøven vælger server, logger ind, ser et projekt fra "en anden enhed" dukke op over socket'en, viser et billede fra en blob og logger ud. Fire plantede fejl, hver fanget af prøven ved sit eget trin: ingen bearer, socket uden billet, billeder ikke hentet, token efterladt ved log ud. Dokumenterer også `VERDANDE_APP_SESSION_TTL` fra trin 1. Co-Authored-By: Claude Opus 5.5 <[email protected]>
116 lines
3.8 KiB
JavaScript
116 lines
3.8 KiB
JavaScript
/**
|
|
* Where the interface is running, and what that changes about reaching the server.
|
|
*
|
|
* In a browser the interface and the API share an origin: every path is relative
|
|
* and the session is an httpOnly cookie no script can see. In the iOS app the
|
|
* interface is bundled into the app and served from capacitor://localhost, and
|
|
* the server is whichever one its owner typed in. So the app needs three things
|
|
* a browser never does: the server's address, its own session token, and that
|
|
* token on every request instead of a cookie.
|
|
*
|
|
* Everything that differs between the two lives here, so `api.js` and the socket
|
|
* ask one question — `apiURL(path)`, `authorize(init)` — and the browser's answer
|
|
* is exactly what it was before this file existed.
|
|
*/
|
|
|
|
/** True inside the iOS app. Capacitor puts its bridge on `window` before any of
|
|
* our code runs. */
|
|
export function isNativeApp() {
|
|
return !!globalThis.Capacitor?.isNativePlatform?.();
|
|
}
|
|
|
|
/**
|
|
* The app's stored settings: which server, and the session on it.
|
|
*
|
|
* localStorage for now, behind this one object, so the Keychain plugin can take
|
|
* its place without anything else changing. The token is a session — whoever
|
|
* holds it is signed in — and the Keychain is where a phone keeps those; until it
|
|
* is wired in, this is a WKWebView's own storage, readable only by the app.
|
|
*/
|
|
const SERVER_KEY = 'verdande.app.server';
|
|
const TOKEN_KEY = 'verdande.app.session';
|
|
|
|
function read(key) {
|
|
try {
|
|
return localStorage.getItem(key) ?? '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function write(key, value) {
|
|
try {
|
|
if (value) localStorage.setItem(key, value);
|
|
else localStorage.removeItem(key);
|
|
} catch {
|
|
// Storage refused (private mode, quota). The app then asks again next
|
|
// launch, which is the right failure: a token that seemed saved and was
|
|
// not would look like being signed out for no reason.
|
|
}
|
|
}
|
|
|
|
export const appSession = {
|
|
server: () => read(SERVER_KEY),
|
|
setServer: (url) => write(SERVER_KEY, url),
|
|
token: () => read(TOKEN_KEY),
|
|
setToken: (token) => write(TOKEN_KEY, token),
|
|
/** Signed out: the token goes, the server stays — signing in again is to the
|
|
* same place nearly every time. */
|
|
signOut: () => write(TOKEN_KEY, '')
|
|
};
|
|
|
|
/** The address of an API path: relative in a browser, on the chosen server in
|
|
* the app. */
|
|
export function apiURL(path) {
|
|
const base = isNativeApp() ? appSession.server() : '';
|
|
return `${base}/api/v1${path}`;
|
|
}
|
|
|
|
/**
|
|
* Fills in how a request proves who is asking.
|
|
*
|
|
* A browser sends its cookie, same-origin only. The app sends no cookie at all —
|
|
* `omit`, so nothing the WebView picked up from somewhere rides along — and its
|
|
* token as a bearer.
|
|
*/
|
|
export function authorize(init) {
|
|
if (!isNativeApp()) {
|
|
init.credentials = 'same-origin';
|
|
return init;
|
|
}
|
|
init.credentials = 'omit';
|
|
const token = appSession.token();
|
|
if (token) {
|
|
init.headers = { ...(init.headers ?? {}), Authorization: `Bearer ${token}` };
|
|
}
|
|
return init;
|
|
}
|
|
|
|
/** What the app calls itself in the list of signed-in devices. */
|
|
export function deviceName() {
|
|
const ua = navigator.userAgent;
|
|
if (/iPad/.test(ua)) return 'iPad';
|
|
if (/iPhone/.test(ua)) return 'iPhone';
|
|
return 'Verdande-appen';
|
|
}
|
|
|
|
/**
|
|
* Normalises what somebody typed as their server's address.
|
|
*
|
|
* "verdande.example.dk" becomes "https://verdande.example.dk"; a trailing slash
|
|
* and a pasted "/api/v1" are dropped. Plain http is kept when typed on purpose,
|
|
* because a self-hoster on their own network may have no certificate — iOS will
|
|
* refuse it unless the app allows it, and that refusal should be the one they see.
|
|
*/
|
|
export function normaliseServer(input) {
|
|
let s = (input ?? '').trim();
|
|
if (!s) return '';
|
|
if (!/^https?:\/\//i.test(s)) s = `https://${s}`;
|
|
try {
|
|
const u = new URL(s);
|
|
return `${u.protocol}//${u.host}`;
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|