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

View File

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