Repository navigation
Expand file tree
/
Copy pathapi-auth.ts
More file actions
85 lines (77 loc) · 3.13 KB
/
Copy pathapi-auth.ts
File metadata and controls
85 lines (77 loc) · 3.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* Optional access token for the Kady backend (see server/src/auth.ts).
*
* A default single-user install never needs one and nothing here changes its
* behaviour: with no stored token, headers and URLs are left untouched. When
* the backend is exposed beyond loopback (or `KADY_REQUIRE_AUTH=1`), the
* launcher opens the UI as `…/#kady-token=<token>`; the fragment never reaches
* a server or a Referer. We keep the token in localStorage, strip it from the
* address bar, and present it as `X-Kady-Token` on fetches and as a
* `kady_token` query parameter on URLs the browser loads itself (`<img>`,
* downloads, pdf.js).
*/
const TOKEN_KEY = "kady:apiToken";
const HASH_PARAM = "kady-token";
const QUERY_PARAM = "kady_token";
export const AUTH_REQUIRED_EVENT = "kady:auth-required";
function captureTokenFromLocation(): void {
if (typeof window === "undefined") return;
try {
const hash = window.location.hash.replace(/^#/, "");
if (!hash.includes(`${HASH_PARAM}=`)) return;
const params = new URLSearchParams(hash);
const token = params.get(HASH_PARAM)?.trim();
if (token) window.localStorage.setItem(TOKEN_KEY, token);
params.delete(HASH_PARAM);
const rest = params.toString();
const url = `${window.location.pathname}${window.location.search}${rest ? `#${rest}` : ""}`;
window.history.replaceState(window.history.state, "", url);
} catch {
// Storage blocked: the token simply is not remembered.
}
}
captureTokenFromLocation();
export function getApiToken(): string | null {
if (typeof window === "undefined") return null;
try {
return window.localStorage.getItem(TOKEN_KEY)?.trim() || null;
} catch {
return null;
}
}
/** Accepts a bare token or a whole `…#kady-token=…` link. */
export function setApiToken(input: string): void {
if (typeof window === "undefined") return;
let token = input.trim();
const at = token.indexOf(`${HASH_PARAM}=`);
if (at !== -1) {
token = decodeURIComponent(token.slice(at + HASH_PARAM.length + 1).split(/[&\s]/)[0] ?? "");
}
try {
if (token) window.localStorage.setItem(TOKEN_KEY, token);
else window.localStorage.removeItem(TOKEN_KEY);
} catch {
// best-effort
}
}
export function withApiTokenHeader(headers: Headers): Headers {
const token = getApiToken();
if (token && !headers.has("X-Kady-Token")) headers.set("X-Kady-Token", token);
return headers;
}
/** Append the token to a backend URL the browser will load by itself. */
export function withApiToken(url: string): string {
const token = getApiToken();
if (!token) return url;
const hashAt = url.indexOf("#");
const base = hashAt === -1 ? url : url.slice(0, hashAt);
const hash = hashAt === -1 ? "" : url.slice(hashAt);
const sep = base.includes("?") ? "&" : "?";
return `${base}${sep}${QUERY_PARAM}=${encodeURIComponent(token)}${hash}`;
}
/** Tell the token gate the backend refused us (see ApiTokenGate). */
export function noteAuthFailure(res: Response): void {
if (typeof window === "undefined") return;
if (!res || res.status !== 401 || res.headers?.get?.("X-Kady-Auth") !== "required") return;
window.dispatchEvent(new CustomEvent(AUTH_REQUIRED_EVENT));
}