102 lines
3.2 KiB
JavaScript
102 lines
3.2 KiB
JavaScript
// YOUreka Auth cookie session → PocketBase client. Ported from
|
|||
|
|
// one-school-chat (src/lib/server/yrka-auth.ts): the portal returns a
|
||
|
|
// PocketBase session cookie from `GET /api/web/auth/{sessionId}/cookie`; we
|
||
|
|
// store its encoded value under our own `session` cookie and reload it into a
|
||
|
|
// PocketBase SDK client (pb.itcass.net), refreshing when invalid or near
|
||
|
|
// expiry. `createPb` is injectable for tests; production always uses the
|
||
|
|
// hardcoded pbUrl default.
|
||
|
|
import PocketBase from 'pocketbase';
|
||
|
|
|
||
|
|
export const PB_URL = 'https://pb.itcass.net';
|
||
|
|
|
||
|
|
/** Refresh proactively when the token expires within this window. */
|
||
|
|
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
||
|
|
|
||
|
|
/** The auth cookie name used by the YRKA-auth PocketBase instance. */
|
||
|
|
const PB_COOKIE_KEY = 'pb_auth';
|
||
|
|
|
||
|
|
/** Extract the bare `pb_auth` value from a Set-Cookie-style string. */
|
||
|
|
export function extractPbCookieValue(raw) {
|
||
|
|
const match = new RegExp(`${PB_COOKIE_KEY}=([^;]+)`).exec(raw);
|
||
|
|
if (match) return match[1];
|
||
|
|
// Already a bare value (no attributes / no name prefix).
|
||
|
|
return raw.split(';')[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Decode a JWT's `exp` (seconds) → ms, or 0 if unreadable. */
|
||
|
|
export function tokenExpiresAtMs(token) {
|
||
|
|
if (!token) return 0;
|
||
|
|
try {
|
||
|
|
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
|
||
|
|
return typeof payload.exp === 'number' ? payload.exp * 1000 : 0;
|
||
|
|
} catch {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function rebuildCookieHeader(encodedValue) {
|
||
|
|
return `${PB_COOKIE_KEY}=${encodedValue}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function recordCollectionName(pb) {
|
||
|
|
return pb.authStore.record?.collectionName ?? 'users';
|
||
|
|
}
|
||
|
|
|
||
|
|
function exportCookieValue(pb) {
|
||
|
|
return extractPbCookieValue(pb.authStore.exportToCookie({}, PB_COOKIE_KEY));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function tryRefresh(pb) {
|
||
|
|
try {
|
||
|
|
await pb.collection(recordCollectionName(pb)).authRefresh();
|
||
|
|
return true;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load a YRKA-auth PocketBase session from the persisted cookie value,
|
||
|
|
* refreshing the token when it is expired or about to expire. Returns
|
||
|
|
* { pb, refreshed, cookie? } or null when the session cannot be recovered
|
||
|
|
* (caller treats the operator as anonymous).
|
||
|
|
*/
|
||
|
|
export async function loadYrkaSession(
|
||
|
|
encodedCookieValue,
|
||
|
|
{ pbUrl = PB_URL, createPb = (url) => new PocketBase(url) } = {}
|
||
|
|
) {
|
||
|
|
if (!encodedCookieValue) return null;
|
||
|
|
|
||
|
|
// Normalize: tolerate stale values that still carry a `pb_auth=` prefix.
|
||
|
|
const normalized = extractPbCookieValue(encodedCookieValue);
|
||
|
|
|
||
|
|
const pb = createPb(pbUrl);
|
||
|
|
pb.autoCancellation(false);
|
||
|
|
|
||
|
|
try {
|
||
|
|
pb.authStore.loadFromCookie(rebuildCookieHeader(normalized));
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!pb.authStore.token) return null;
|
||
|
|
|
||
|
|
// Invalid / expired → attempt one refresh (the stored token may redeem).
|
||
|
|
if (!pb.authStore.isValid) {
|
||
|
|
if (await tryRefresh(pb)) {
|
||
|
|
return { pb, refreshed: true, cookie: exportCookieValue(pb) };
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Valid but nearing expiry → refresh proactively and persist the new token.
|
||
|
|
const expiresAt = tokenExpiresAtMs(pb.authStore.token);
|
||
|
|
if (expiresAt > 0 && Date.now() + REFRESH_BUFFER_MS >= expiresAt) {
|
||
|
|
if (await tryRefresh(pb)) {
|
||
|
|
return { pb, refreshed: true, cookie: exportCookieValue(pb) };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { pb, refreshed: false };
|
||
|
|
}
|