Files
FEDEO/mobile/src/providers/auth-provider.tsx
2026-02-19 18:29:06 +01:00

187 lines
4.8 KiB
TypeScript

import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { fetchMe, loginWithEmailPassword, MeResponse, switchTenantRequest, Tenant } from '@/src/lib/api';
import { clearStoredToken, getStoredToken, setStoredToken, tokenStorageInfo } from '@/src/lib/token-storage';
export type AuthUser = MeResponse['user'];
type AuthContextValue = {
isBootstrapping: boolean;
token: string | null;
user: AuthUser | null;
tenants: Tenant[];
activeTenantId: number | null;
activeTenant: Tenant | null;
profile: Record<string, unknown> | null;
permissions: string[];
requiresTenantSelection: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
switchTenant: (tenantId: number) => Promise<void>;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
function normalizeTenantId(value: number | string | null | undefined): number | null {
if (value === null || value === undefined || value === '') {
return null;
}
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isBootstrapping, setIsBootstrapping] = useState(true);
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 resetSession = useCallback(() => {
setUser(null);
setTenants([]);
setActiveTenantId(null);
setProfile(null);
setPermissions([]);
}, []);
const hydrateSession = useCallback(
async (tokenToUse: string) => {
const me = await fetchMe(tokenToUse);
setUser(me.user);
setTenants(me.tenants || []);
setActiveTenantId(normalizeTenantId(me.activeTenant));
setProfile(me.profile || null);
setPermissions(me.permissions || []);
},
[]
);
const logout = useCallback(async () => {
await clearStoredToken();
setToken(null);
resetSession();
}, [resetSession]);
const refreshUser = useCallback(async () => {
if (!token) {
resetSession();
return;
}
try {
await hydrateSession(token);
} catch {
await logout();
}
}, [hydrateSession, logout, resetSession, token]);
useEffect(() => {
async function bootstrap() {
const storedToken = await getStoredToken();
if (!storedToken) {
setIsBootstrapping(false);
return;
}
try {
await hydrateSession(storedToken);
setToken(storedToken);
} catch {
await clearStoredToken();
setToken(null);
resetSession();
} finally {
setIsBootstrapping(false);
}
}
void bootstrap();
}, [hydrateSession, resetSession]);
const login = useCallback(
async (email: string, password: string) => {
const nextToken = await loginWithEmailPassword(email, password);
await setStoredToken(nextToken);
await hydrateSession(nextToken);
setToken(nextToken);
},
[hydrateSession]
);
const switchTenant = useCallback(
async (tenantId: number) => {
if (!token) {
throw new Error('No active session found.');
}
const nextToken = await switchTenantRequest(tenantId, token);
await setStoredToken(nextToken);
await hydrateSession(nextToken);
setToken(nextToken);
},
[token, hydrateSession]
);
const activeTenant = useMemo(
() => tenants.find((tenant) => Number(tenant.id) === activeTenantId) || null,
[activeTenantId, tenants]
);
const requiresTenantSelection = Boolean(token) && tenants.length > 0 && !activeTenantId;
const value = useMemo(
() => ({
isBootstrapping,
token,
user,
tenants,
activeTenantId,
activeTenant,
profile,
permissions,
requiresTenantSelection,
login,
logout,
refreshUser,
switchTenant,
}),
[
activeTenant,
activeTenantId,
isBootstrapping,
login,
logout,
permissions,
profile,
refreshUser,
requiresTenantSelection,
switchTenant,
tenants,
token,
user,
]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used inside AuthProvider');
}
return context;
}
export function useTokenStorageInfo() {
return tokenStorageInfo;
}