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).
**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.

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> {
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;
}

View File

@ -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',

View File

@ -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<Response> {
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<Response>
...(init.headers ?? {}),
},
});
if (res.status === 401) handleUnauthorized();
return res;
}
export async function refreshLockStatus(): Promise<void> {

View File

@ -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<Response> {
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<Response> {
...(init.headers ?? {}),
},
});
if (res.status === 401) handleUnauthorized();
return res;
}
async function pullAndMerge(since: Date | null): Promise<string> {