feat: agregar contexto de operación y mejorar la restauración de sesión en el proveedor de autenticación

This commit is contained in:
David Arranz 2026-08-29 15:47:55 +02:00
parent 1a8d63373d
commit ce9f9029e3
2 changed files with 84 additions and 43 deletions

View File

@ -71,7 +71,7 @@ export type IdentityInternalDeps = {
}; };
export const buildIdentityDependencies = (params: ModuleParams): IdentityInternalDeps => { export const buildIdentityDependencies = (params: ModuleParams): IdentityInternalDeps => {
const { database, config, transactionContext } = params; const { database, config, operationContext, transactionContext } = params;
const transactionManager = buildTransactionManager(database, transactionContext); const transactionManager = buildTransactionManager(database, transactionContext);
@ -120,6 +120,7 @@ export const buildIdentityDependencies = (params: ModuleParams): IdentityInterna
authenticateRequest: identityAuthenticateRequest({ authenticateRequest: identityAuthenticateRequest({
accessTokenVerifier, accessTokenVerifier,
accountRepository, accountRepository,
operationContext,
}), }),
useCases: { useCases: {
loginWithPassword: () => loginWithPassword: () =>
@ -152,6 +153,7 @@ export const buildIdentityDependencies = (params: ModuleParams): IdentityInterna
authenticateRequest: identityAuthenticateRequest({ authenticateRequest: identityAuthenticateRequest({
accessTokenVerifier, accessTokenVerifier,
accountRepository, accountRepository,
operationContext,
}), }),
useCases: { useCases: {
getCurrentAccount: () => getCurrentAccount: () =>

View File

@ -1,5 +1,7 @@
import { isApiClientError } from "@erp/core/client";
import { useDataSource } from "@erp/core/hooks"; import { useDataSource } from "@erp/core/hooks";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import type { CurrentSessionResponseDTO } from "@erp/identity/common";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import type { LoginForm } from "../login/entities"; import type { LoginForm } from "../login/entities";
import { import {
@ -16,7 +18,7 @@ import {
useRefreshSessionMutation, useRefreshSessionMutation,
} from "../shared"; } from "../shared";
import { getCurrentSession } from "../shared/api"; import { getCurrentSession } from "../shared/api";
import type { ActiveCompany, AuthSession, CompanySession } from "../shared/entities"; import type { ActiveCompany, AuthSession, AuthTokens, CompanySession } from "../shared/entities";
import { import {
resetIdentityAuthSessionRuntimeHandlers, resetIdentityAuthSessionRuntimeHandlers,
@ -66,15 +68,20 @@ const EMPTY_COMPANY_SESSION: CompanySession = {
activeCompany: null, activeCompany: null,
}; };
const isUnauthorizedError = (error: unknown) =>
isApiClientError(error) && error.status === 401;
export function IdentityAuthSessionProvider({ children }: { children: React.ReactNode }) { export function IdentityAuthSessionProvider({ children }: { children: React.ReactNode }) {
const dataSource = useDataSource(); const dataSource = useDataSource();
const loginMutation = useLoginMutation(); const loginMutation = useLoginMutation();
const logoutMutation = useLogoutMutation(); const logoutMutation = useLogoutMutation();
const refreshSessionMutation = useRefreshSessionMutation(); const refreshSessionMutation = useRefreshSessionMutation();
const refreshSessionMutationAsync = refreshSessionMutation.mutateAsync;
const [session, setSession] = useState<AuthSession | null>(null); const [session, setSession] = useState<AuthSession | null>(null);
const [companySession, setCompanySession] = useState<CompanySession>(EMPTY_COMPANY_SESSION); const [companySession, setCompanySession] = useState<CompanySession>(EMPTY_COMPANY_SESSION);
const [isRestoringSession, setIsRestoringSession] = useState(authTokenStorage.hasAccessToken()); const [isRestoringSession, setIsRestoringSession] = useState(authTokenStorage.hasAccessToken());
const restoreInFlightRef = useRef<Promise<AuthSession | null> | null>(null);
const clearSession = useCallback(() => { const clearSession = useCallback(() => {
authTokenStorage.clear(); authTokenStorage.clear();
@ -111,35 +118,67 @@ export function IdentityAuthSessionProvider({ children }: { children: React.Reac
[dataSource] [dataSource]
); );
const restoreSession = useCallback(async () => { const restoreSession = useCallback(() => {
const tokens = authTokenStorage.getTokens(); if (restoreInFlightRef.current) {
return restoreInFlightRef.current;
if (!tokens.accessToken) {
clearSession();
setIsRestoringSession(false);
return null;
} }
setIsRestoringSession(true); const restorePromise = Promise.resolve().then(async () => {
const tokens = authTokenStorage.getTokens();
try { if (!tokens.accessToken) {
const response = await getCurrentSession(dataSource, tokens.accessToken); clearSession();
const restoredSession = mapCurrentSessionToAuthSession(response, { setIsRestoringSession(false);
accessToken: tokens.accessToken, return null;
refreshToken: tokens.refreshToken ?? "", }
});
await hydrateCompanySession(tokens.accessToken); setIsRestoringSession(true);
setSession(restoredSession);
return restoredSession; try {
} catch { let activeTokens: AuthTokens = {
clearSession(); accessToken: tokens.accessToken,
return null; refreshToken: tokens.refreshToken ?? "",
} finally { };
setIsRestoringSession(false); let response: CurrentSessionResponseDTO;
}
}, [clearSession, dataSource, hydrateCompanySession]); try {
response = await getCurrentSession(dataSource, activeTokens.accessToken);
} catch (error) {
if (!isUnauthorizedError(error) || !activeTokens.refreshToken) {
throw error;
}
const refreshedTokens = await refreshSessionMutationAsync({
refreshToken: activeTokens.refreshToken,
});
authTokenStorage.setTokens(refreshedTokens);
activeTokens = refreshedTokens;
response = await getCurrentSession(dataSource, activeTokens.accessToken);
}
const restoredSession = mapCurrentSessionToAuthSession(response, {
accessToken: activeTokens.accessToken,
refreshToken: activeTokens.refreshToken,
});
await hydrateCompanySession(activeTokens.accessToken);
setSession(restoredSession);
return restoredSession;
} catch {
clearSession();
return null;
} finally {
restoreInFlightRef.current = null;
setIsRestoringSession(false);
}
});
restoreInFlightRef.current = restorePromise;
return restorePromise;
}, [clearSession, dataSource, hydrateCompanySession, refreshSessionMutationAsync]);
const login = useCallback( const login = useCallback(
async (params: LoginForm) => { async (params: LoginForm) => {