From 4abe6d758c756245dcd6519ff26bb80bde367038 Mon Sep 17 00:00:00 2001 From: Claudio Date: Sun, 17 May 2026 22:06:33 +0200 Subject: [PATCH] feat(web): OIDC login button + cookie-based session bootstrap The SvelteKit /login was username/password only; the legacy comment even called out 'OIDC SSO ships in M4 when the IdP is wired up'. Authentik is wired up now, so: - /api/v1/config exposes ext.oidc when the IdP is configured. Fetch it on the login page and conditionally render "Sign in with {provider}", which kicks off /api/v1/oidc/login. - After PhotoPrism completes the auth code exchange, it sets `auth_token` + `auth_session` cookies and redirects to siteUrl (/library/browse by default; the deployment's reverse proxy is expected to bounce that to /). bootstrapSessionFromCookies() reads those cookies, calls GET /api/v1/session/ with the cookie's token, and adopts the resulting session into the SPA store on mount. - Root layout's auth guard now waits for the bootstrap pass before punting to /login, so a fresh OIDC return doesn't get redirected away before the session is read. --- web/src/lib/services/photoprism.ts | 30 +++++++++++++++++++ web/src/lib/types/photoprism.ts | 16 ++++++++++ web/src/routes/+layout.svelte | 19 ++++++++++-- web/src/routes/login/+page.svelte | 47 +++++++++++++++++++++++++++--- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/web/src/lib/services/photoprism.ts b/web/src/lib/services/photoprism.ts index 948ed90..4800ef1 100644 --- a/web/src/lib/services/photoprism.ts +++ b/web/src/lib/services/photoprism.ts @@ -67,6 +67,36 @@ export async function fetchSession(id: string): Promise { return data; } +/** + * 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. + * + * Returns the adopted session, or null when the cookies are missing or stale + * (caller treats null as "stay on /login"). + */ +export async function bootstrapSessionFromCookies(): Promise { + 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. + session.accessToken = token; + try { + const resp = await fetchSession(sid); + adoptSession(resp); + return resp; + } catch { + session.accessToken = null; + return null; + } +} + export async function getConfig(): Promise { const { data } = await http.get('/config'); return data; diff --git a/web/src/lib/types/photoprism.ts b/web/src/lib/types/photoprism.ts index 6f65625..c30a398 100644 --- a/web/src/lib/types/photoprism.ts +++ b/web/src/lib/types/photoprism.ts @@ -55,6 +55,22 @@ export interface PpClientConfig { lenses?: number; countries?: number; }; + /** + * Optional extensions block. PhotoPrism reports OIDC availability and + * the per-provider login URI here so the SPA can render a "Sign in + * with " button. Empty (or `enabled:false`) when OIDC isn't + * configured. + */ + ext?: { + oidc?: { + enabled: boolean; + provider?: string; + loginUri?: string; + icon?: string; + register?: boolean; + redirect?: boolean; + }; + }; } export interface PpSessionResponse { diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index df622a5..086d220 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -1,5 +1,6 @@
@@ -71,8 +95,23 @@ {submitting ? 'Signing in…' : 'Sign in'} -

- OIDC SSO ships in M4 when the IdP is wired up. -

+ {#if oidc} +
+ +
+ or +
+
+ + + {/if}