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/<id> 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.
This commit is contained in:
Claudio
2026-05-17 22:06:33 +02:00
parent cb5bc120dc
commit 4abe6d758c
4 changed files with 106 additions and 6 deletions

View File

@@ -67,6 +67,36 @@ export async function fetchSession(id: string): Promise<PpSessionResponse> {
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<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.
session.accessToken = token;
try {
const resp = await fetchSession(sid);
adoptSession(resp);
return resp;
} catch {
session.accessToken = null;
return null;
}
}
export async function getConfig(): Promise<PpClientConfig> {
const { data } = await http.get<PpClientConfig>('/config');
return data;