Mulimage 2.0 #1

Merged
dtoro merged 64 commits from new into main 2026-05-21 22:48:55 +02:00
3 changed files with 50 additions and 19 deletions
Showing only changes of commit 9a3ad3e579 - Show all commits

View File

@@ -68,28 +68,51 @@ export async function fetchSession(id: string): Promise<PpSessionResponse> {
}
/**
* After OIDC completes, PhotoPrism redirects to `siteUrl` with two cookies
* set: `auth_token` (the X-Auth-Token value) and `auth_session` (the session
* UID). If both are present, fetch the matching session and adopt it so the
* SPA picks up the OIDC-issued identity without a username/password trip.
* After OIDC completes, PhotoPrism returns an HTML page that writes the
* issued session into `localStorage` under the namespaced keys
* `pp:<storageNamespace>:session.{id,token,user,provider}` and then runs
* `window.location.href = "/library/login"`. With our Caddy bouncing
* `/library/*` back to `/`, the browser lands on the SvelteKit root with
* those entries already in localStorage but with no PhotoPrism cookies set
* — so we read them back to adopt the OIDC-issued session.
*
* Returns the adopted session, or null when the cookies are missing or stale
* Returns the adopted session, or null when nothing is waiting in storage
* (caller treats null as "stay on /login").
*/
export async function bootstrapSessionFromCookies(): Promise<PpSessionResponse | null> {
export async function bootstrapSessionFromPhotoPrism(): Promise<PpSessionResponse | null> {
if (!browser) return null;
const read = (name: string): string | null => {
const m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]+)'));
return m ? decodeURIComponent(m[1]) : null;
};
const token = read('auth_token');
const sid = read('auth_session');
if (!token || !sid) return null;
// Prime the http client so the X-Auth-Token interceptor fires.
// PhotoPrism's storageNamespace is per-instance (build-time hash); fetch
// it via /api/v1/config so we resolve the right key prefix.
let namespace: string | undefined;
try {
const cfg = await getConfig();
namespace = cfg.storageNamespace;
} catch {
return null;
}
if (!namespace) return null;
const prefix = `pp:${namespace}:`;
const sid = localStorage.getItem(prefix + 'session.id');
const token = localStorage.getItem(prefix + 'session.token');
if (!sid || !token) return null;
// Prime the http client so the X-Auth-Token interceptor fires for the
// session lookup below.
session.accessToken = token;
try {
const resp = await fetchSession(sid);
adoptSession(resp);
// adoptSession persists into our own storage key (`pp_session`);
// PhotoPrism's `pp:<ns>:session.*` entries are one-shot delivery,
// so clear them now to avoid stale state on logout.
for (const k of [
'session.id',
'session.token',
'session.user',
'session.provider',
'session.error'
]) {
localStorage.removeItem(prefix + k);
}
return resp;
} catch {
session.accessToken = null;

View File

@@ -23,6 +23,13 @@ export interface PpClientConfig {
previewToken: string;
downloadToken: string;
flags?: string;
/**
* Per-instance hash that PhotoPrism uses to namespace its own
* `localStorage` entries (e.g. `pp:<namespace>:session.token`). The
* OIDC redirect HTML drops the issued session under this prefix; we
* read it back to adopt the SSO identity into the SPA store.
*/
storageNamespace?: string;
/**
* Precomputed library counters. PhotoPrism updates these incrementally
* on every mutation, so they're cheap to read and accurate without a

View File

@@ -19,21 +19,22 @@
let { children } = $props();
// Bootstrap state: the OIDC return drops the user back on `/` with
// PhotoPrism's auth_token/auth_session cookies set, but the SPA store
// is empty. We try to adopt the cookie session on first mount before
// the auth guard can punt to /login.
// PhotoPrism's session info written to localStorage under
// `pp:<storageNamespace>:session.*`, but the SPA store is empty. We
// try to adopt that session on first mount before the auth guard
// can punt to /login.
let bootstrapped = $state(false);
onMount(async () => {
if (!isAuthenticated()) {
await bootstrapSessionFromCookies();
await bootstrapSessionFromPhotoPrism();
}
bootstrapped = true;
});
// Auth guard. Anything outside /login requires a session; otherwise
// punt to the login page (which itself redirects authenticated users
// back to /). Held until the cookie bootstrap has had a chance to run.
// back to /). Held until the bootstrap pass has had a chance to run.
$effect(() => {
if (!browser || !bootstrapped) return;
const onLogin = page.url.pathname === '/login';