KI-AGENT: Stabilisiert mobilen Session-Start
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { fetchMe, loginWithEmailPassword, MeResponse, switchTenantRequest, Tenant } from '@/src/lib/api';
|
||||
import { hydrateApiBaseUrl } from '@/src/lib/server-config';
|
||||
@@ -8,6 +8,7 @@ export type AuthUser = MeResponse['user'];
|
||||
|
||||
type AuthContextValue = {
|
||||
isBootstrapping: boolean;
|
||||
bootstrapError: string | null;
|
||||
token: string | null;
|
||||
user: AuthUser | null;
|
||||
tenants: Tenant[];
|
||||
@@ -20,9 +21,12 @@ type AuthContextValue = {
|
||||
logout: () => Promise<void>;
|
||||
refreshUser: () => Promise<void>;
|
||||
switchTenant: (tenantId: number) => Promise<void>;
|
||||
retryBootstrap: () => Promise<void>;
|
||||
resetLocalSession: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
const BOOTSTRAP_TIMEOUT_MS = 20000;
|
||||
|
||||
function normalizeTenantId(value: number | string | null | undefined): number | null {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
@@ -33,14 +37,27 @@ function normalizeTenantId(value: number | string | null | undefined): number |
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
|
||||
promise
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => clearTimeout(timeout));
|
||||
});
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isBootstrapping, setIsBootstrapping] = useState(true);
|
||||
const [bootstrapError, setBootstrapError] = useState<string | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [activeTenantId, setActiveTenantId] = useState<number | null>(null);
|
||||
const [profile, setProfile] = useState<Record<string, unknown> | null>(null);
|
||||
const [permissions, setPermissions] = useState<string[]>([]);
|
||||
const bootstrapRunRef = useRef(0);
|
||||
|
||||
const resetSession = useCallback(() => {
|
||||
setUser(null);
|
||||
@@ -65,6 +82,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const logout = useCallback(async () => {
|
||||
await clearStoredToken();
|
||||
setToken(null);
|
||||
setBootstrapError(null);
|
||||
resetSession();
|
||||
}, [resetSession]);
|
||||
|
||||
@@ -81,31 +99,59 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [hydrateSession, logout, resetSession, token]);
|
||||
|
||||
useEffect(() => {
|
||||
async function bootstrap() {
|
||||
await hydrateApiBaseUrl();
|
||||
const storedToken = await getStoredToken();
|
||||
const resetLocalSession = useCallback(async () => {
|
||||
setToken(null);
|
||||
setBootstrapError(null);
|
||||
setIsBootstrapping(false);
|
||||
resetSession();
|
||||
void clearStoredToken().catch(() => undefined);
|
||||
}, [resetSession]);
|
||||
|
||||
if (!storedToken) {
|
||||
setIsBootstrapping(false);
|
||||
return;
|
||||
}
|
||||
const bootstrap = useCallback(async () => {
|
||||
const runId = bootstrapRunRef.current + 1;
|
||||
bootstrapRunRef.current = runId;
|
||||
setIsBootstrapping(true);
|
||||
setBootstrapError(null);
|
||||
|
||||
try {
|
||||
await hydrateSession(storedToken);
|
||||
setToken(storedToken);
|
||||
} catch {
|
||||
await clearStoredToken();
|
||||
try {
|
||||
await withTimeout(
|
||||
(async () => {
|
||||
await hydrateApiBaseUrl();
|
||||
const storedToken = await getStoredToken();
|
||||
|
||||
if (!storedToken) {
|
||||
if (bootstrapRunRef.current !== runId) return;
|
||||
setToken(null);
|
||||
resetSession();
|
||||
return;
|
||||
}
|
||||
|
||||
await hydrateSession(storedToken);
|
||||
|
||||
if (bootstrapRunRef.current !== runId) return;
|
||||
setToken(storedToken);
|
||||
})(),
|
||||
BOOTSTRAP_TIMEOUT_MS,
|
||||
'Die mobile Session konnte nicht rechtzeitig initialisiert werden.'
|
||||
);
|
||||
} catch (err) {
|
||||
if (bootstrapRunRef.current === runId) {
|
||||
setToken(null);
|
||||
resetSession();
|
||||
} finally {
|
||||
setBootstrapError(err instanceof Error ? err.message : 'Die mobile Session konnte nicht initialisiert werden.');
|
||||
void clearStoredToken().catch(() => undefined);
|
||||
}
|
||||
} finally {
|
||||
if (bootstrapRunRef.current === runId) {
|
||||
setIsBootstrapping(false);
|
||||
}
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
}, [hydrateSession, resetSession]);
|
||||
|
||||
useEffect(() => {
|
||||
void bootstrap();
|
||||
}, [bootstrap]);
|
||||
|
||||
const login = useCallback(
|
||||
async (email: string, password: string) => {
|
||||
const nextToken = await loginWithEmailPassword(email, password);
|
||||
@@ -140,6 +186,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isBootstrapping,
|
||||
bootstrapError,
|
||||
token,
|
||||
user,
|
||||
tenants,
|
||||
@@ -152,10 +199,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
logout,
|
||||
refreshUser,
|
||||
switchTenant,
|
||||
retryBootstrap: bootstrap,
|
||||
resetLocalSession,
|
||||
}),
|
||||
[
|
||||
activeTenant,
|
||||
activeTenantId,
|
||||
bootstrap,
|
||||
bootstrapError,
|
||||
isBootstrapping,
|
||||
login,
|
||||
logout,
|
||||
@@ -163,6 +214,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
profile,
|
||||
refreshUser,
|
||||
requiresTenantSelection,
|
||||
resetLocalSession,
|
||||
switchTenant,
|
||||
tenants,
|
||||
token,
|
||||
|
||||
Reference in New Issue
Block a user