sync fix - fallback MSAL

This commit is contained in:
beo3000 2026-02-25 07:36:28 +01:00
parent a88b2ea93e
commit a62ada936d
6 changed files with 23 additions and 7 deletions

View File

@ -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). 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
`deploy.ps1` uses normal `docker build` with layer caching. Only changed layers are pushed to ACR. `deploy.ps1` uses normal `docker build` with layer caching. Only changed layers are pushed to ACR.

View File

@ -1 +1 @@
1.1.59 1.1.62

View File

@ -1,8 +1,10 @@
import { getAccessToken } from './authStore.js'; import { getAccessToken, handleUnauthorized } from './authStore.js';
export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> { export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
const token = await getAccessToken(); const token = await getAccessToken();
const headers = new Headers(options.headers); const headers = new Headers(options.headers);
headers.set('Authorization', `Bearer ${token}`); headers.set('Authorization', `Bearer ${token}`);
return fetch(url, { ...options, headers }); const response = await fetch(url, { ...options, headers });
if (response.status === 401) handleUnauthorized();
return response;
} }

View File

@ -22,6 +22,14 @@ export function setBrowserApiKey(key: string | null): void {
browserApiKey.set(key); 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 = { const DEV_ACCOUNT = {
homeAccountId: 'dev-user', homeAccountId: 'dev-user',
environment: 'localhost', environment: 'localhost',

View File

@ -1,5 +1,5 @@
import { writable } from 'svelte/store'; 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 }; 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<Response> { async function apiFetch(path: string, init: RequestInit = {}): Promise<Response> {
const token = await getAccessToken(); const token = await getAccessToken();
return fetch(`${API_BASE}${path}`, { const res = await fetch(`${API_BASE}${path}`, {
...init, ...init,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -17,6 +17,8 @@ async function apiFetch(path: string, init: RequestInit = {}): Promise<Response>
...(init.headers ?? {}), ...(init.headers ?? {}),
}, },
}); });
if (res.status === 401) handleUnauthorized();
return res;
} }
export async function refreshLockStatus(): Promise<void> { export async function refreshLockStatus(): Promise<void> {

View File

@ -1,6 +1,6 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { db } from '$lib/db/schema'; 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 { aiLockStatus } from '$lib/stores/aiLock';
import type { SyncPushRequest, SyncPullResponse } from '@ka-note/shared'; import type { SyncPushRequest, SyncPullResponse } from '@ka-note/shared';
@ -53,7 +53,7 @@ async function apiFetch(path: string, init: RequestInit): Promise<Response> {
const payload = JSON.parse(atob(token.split('.')[1])); 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()); console.log('[sync] token aud:', payload.aud, '| scp:', payload.scp, '| exp:', new Date(payload.exp * 1000).toISOString());
} catch { /* ignore */ } } catch { /* ignore */ }
return fetch(`${API_BASE}${path}`, { const res = await fetch(`${API_BASE}${path}`, {
...init, ...init,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -62,6 +62,8 @@ async function apiFetch(path: string, init: RequestInit): Promise<Response> {
...(init.headers ?? {}), ...(init.headers ?? {}),
}, },
}); });
if (res.status === 401) handleUnauthorized();
return res;
} }
async function pullAndMerge(since: Date | null): Promise<string> { async function pullAndMerge(since: Date | null): Promise<string> {