From a62ada936d4fb6c08838196430920a0d04ba34bb Mon Sep 17 00:00:00 2001 From: beo3000 Date: Wed, 25 Feb 2026 07:36:28 +0100 Subject: [PATCH] sync fix - fallback MSAL --- docs/feature-sync.md | 2 ++ ka-note/VERSION | 2 +- ka-note/client/src/lib/auth/apiClient.ts | 6 ++++-- ka-note/client/src/lib/auth/authStore.ts | 8 ++++++++ ka-note/client/src/lib/stores/aiLock.ts | 6 ++++-- ka-note/client/src/lib/sync/syncService.ts | 6 ++++-- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/feature-sync.md b/docs/feature-sync.md index cc50d40..2e73536 100644 --- a/docs/feature-sync.md +++ b/docs/feature-sync.md @@ -47,6 +47,8 @@ UI in Settings → "Browser-Authentifizierung" section. Use case: avoids MSAL silent refresh failures (Azure AD returning HTTP 400 on refresh token). +**401 auto-invalidation:** `authFetch` in `apiClient.ts` detects 401 responses when a browser key was used and calls `setBrowserApiKey(null)` — clears `localStorage` and the store. `isAuthenticated` becomes `false`, restoring the MSAL login UI. Prevents the stuck state where a revoked/expired key blocks both sync and manual login. + ## Deploy `deploy.ps1` uses normal `docker build` with layer caching. Only changed layers are pushed to ACR. diff --git a/ka-note/VERSION b/ka-note/VERSION index 109d841..2442528 100644 --- a/ka-note/VERSION +++ b/ka-note/VERSION @@ -1 +1 @@ -1.1.59 \ No newline at end of file +1.1.62 \ No newline at end of file diff --git a/ka-note/client/src/lib/auth/apiClient.ts b/ka-note/client/src/lib/auth/apiClient.ts index 6179bd8..b250587 100644 --- a/ka-note/client/src/lib/auth/apiClient.ts +++ b/ka-note/client/src/lib/auth/apiClient.ts @@ -1,8 +1,10 @@ -import { getAccessToken } from './authStore.js'; +import { getAccessToken, handleUnauthorized } from './authStore.js'; export async function authFetch(url: string, options: RequestInit = {}): Promise { const token = await getAccessToken(); const headers = new Headers(options.headers); headers.set('Authorization', `Bearer ${token}`); - return fetch(url, { ...options, headers }); + const response = await fetch(url, { ...options, headers }); + if (response.status === 401) handleUnauthorized(); + return response; } diff --git a/ka-note/client/src/lib/auth/authStore.ts b/ka-note/client/src/lib/auth/authStore.ts index 509e030..6c82d4b 100644 --- a/ka-note/client/src/lib/auth/authStore.ts +++ b/ka-note/client/src/lib/auth/authStore.ts @@ -22,6 +22,14 @@ export function setBrowserApiKey(key: string | null): void { browserApiKey.set(key); } +// Call after a 401 response to clear a stale browser key and restore MSAL login UI. +export function handleUnauthorized(): void { + if (get(browserApiKey) !== null) { + console.warn('[auth] 401 received — clearing browser API key to allow MSAL fallback'); + setBrowserApiKey(null); + } +} + const DEV_ACCOUNT = { homeAccountId: 'dev-user', environment: 'localhost', diff --git a/ka-note/client/src/lib/stores/aiLock.ts b/ka-note/client/src/lib/stores/aiLock.ts index eed8944..f929b82 100644 --- a/ka-note/client/src/lib/stores/aiLock.ts +++ b/ka-note/client/src/lib/stores/aiLock.ts @@ -1,5 +1,5 @@ import { writable } from 'svelte/store'; -import { getAccessToken } from '$lib/auth/authStore'; +import { getAccessToken, handleUnauthorized } from '$lib/auth/authStore'; export type AiLockStatus = { locked: false } | { locked: true; lockedAt: string; expiresAt: string }; @@ -9,7 +9,7 @@ const API_BASE = import.meta.env.VITE_API_URL ?? ''; async function apiFetch(path: string, init: RequestInit = {}): Promise { const token = await getAccessToken(); - return fetch(`${API_BASE}${path}`, { + const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { 'Content-Type': 'application/json', @@ -17,6 +17,8 @@ async function apiFetch(path: string, init: RequestInit = {}): Promise ...(init.headers ?? {}), }, }); + if (res.status === 401) handleUnauthorized(); + return res; } export async function refreshLockStatus(): Promise { diff --git a/ka-note/client/src/lib/sync/syncService.ts b/ka-note/client/src/lib/sync/syncService.ts index 6d1fdb9..41712e2 100644 --- a/ka-note/client/src/lib/sync/syncService.ts +++ b/ka-note/client/src/lib/sync/syncService.ts @@ -1,6 +1,6 @@ import { writable } from 'svelte/store'; import { db } from '$lib/db/schema'; -import { getAccessToken } from '$lib/auth/authStore'; +import { getAccessToken, handleUnauthorized } from '$lib/auth/authStore'; import { aiLockStatus } from '$lib/stores/aiLock'; import type { SyncPushRequest, SyncPullResponse } from '@ka-note/shared'; @@ -53,7 +53,7 @@ async function apiFetch(path: string, init: RequestInit): Promise { const payload = JSON.parse(atob(token.split('.')[1])); console.log('[sync] token aud:', payload.aud, '| scp:', payload.scp, '| exp:', new Date(payload.exp * 1000).toISOString()); } catch { /* ignore */ } - return fetch(`${API_BASE}${path}`, { + const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { 'Content-Type': 'application/json', @@ -62,6 +62,8 @@ async function apiFetch(path: string, init: RequestInit): Promise { ...(init.headers ?? {}), }, }); + if (res.status === 401) handleUnauthorized(); + return res; } async function pullAndMerge(since: Date | null): Promise {