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 { useDataSource } from "@erp/core/hooks"; import { isApiClientError } from "@erp/core/client";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useDataSource } from "@erp/core/hooks";
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,
@ -61,20 +63,25 @@ const IdentityAuthSessionContext = createContext<IdentityAuthSessionContextValue
undefined undefined
); );
const EMPTY_COMPANY_SESSION: CompanySession = { const EMPTY_COMPANY_SESSION: CompanySession = {
availableCompanies: [], availableCompanies: [],
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); const restorePromise = Promise.resolve().then(async () => {
return null; const tokens = authTokenStorage.getTokens();
}
if (!tokens.accessToken) {
setIsRestoringSession(true); clearSession();
setIsRestoringSession(false);
try { return null;
const response = await getCurrentSession(dataSource, tokens.accessToken); }
const restoredSession = mapCurrentSessionToAuthSession(response, {
accessToken: tokens.accessToken, setIsRestoringSession(true);
refreshToken: tokens.refreshToken ?? "",
}); try {
let activeTokens: AuthTokens = {
await hydrateCompanySession(tokens.accessToken); accessToken: tokens.accessToken,
setSession(restoredSession); refreshToken: tokens.refreshToken ?? "",
};
return restoredSession; let response: CurrentSessionResponseDTO;
} catch {
clearSession(); try {
return null; response = await getCurrentSession(dataSource, activeTokens.accessToken);
} finally { } catch (error) {
setIsRestoringSession(false); if (!isUnauthorizedError(error) || !activeTokens.refreshToken) {
} throw error;
}, [clearSession, dataSource, hydrateCompanySession]); }
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) => {