.
This commit is contained in:
parent
2c89ebb002
commit
0fdf18baf3
@ -41,7 +41,7 @@
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"react-hook-form": "^7.56.2",
|
||||
"react-hook-form-persist": "^3.0.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
|
||||
@ -6,15 +6,12 @@ import { I18nextProvider } from "react-i18next";
|
||||
import { UnsavedWarnProvider } from "@/lib/hooks";
|
||||
import { i18n } from "@/locales";
|
||||
|
||||
import { clearAccessToken, getAccessToken } from "@erp/auth/client";
|
||||
import { AuthProvider, createAuthService } from "@erp/auth/client";
|
||||
import { DataSourceProvider, createAxiosDataSource, createAxiosInstance } from "@erp/core/client";
|
||||
import "./app.css";
|
||||
import { AppRoutes } from "./routes/app-routes";
|
||||
|
||||
/**
|
||||
* Clave utilizada en el almacenamiento local para el token JWT.
|
||||
*/
|
||||
const TOKEN_STORAGE_KEY = "factuges.auth";
|
||||
import "./app.css";
|
||||
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib";
|
||||
import { AppRoutes } from "./routes";
|
||||
|
||||
export const App = () => {
|
||||
const queryClient = new QueryClient({
|
||||
@ -26,28 +23,39 @@ export const App = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const dataSource = createAxiosDataSource(
|
||||
createAxiosInstance({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
getAccessToken: () => getAccessToken(TOKEN_STORAGE_KEY),
|
||||
onAuthError: () => {
|
||||
clearAccessToken(TOKEN_STORAGE_KEY);
|
||||
window.location.href = "/login"; // o usar navegación programática
|
||||
},
|
||||
})
|
||||
);
|
||||
const axiosInstance = createAxiosInstance({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
getAccessToken,
|
||||
onAuthError: () => {
|
||||
clearAccessToken();
|
||||
window.location.href = "/login"; // o usar navegación programática
|
||||
},
|
||||
});
|
||||
|
||||
console.log(axiosInstance.defaults.env);
|
||||
|
||||
const dataSource = createAxiosDataSource(axiosInstance);
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DataSourceProvider dataSource={dataSource}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<UnsavedWarnProvider>
|
||||
<AppRoutes />
|
||||
</UnsavedWarnProvider>
|
||||
</TooltipProvider>
|
||||
<Toaster />
|
||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
<AuthProvider
|
||||
params={{
|
||||
getAccessToken,
|
||||
setAccessToken,
|
||||
clearAccessToken,
|
||||
authService: createAuthService(dataSource),
|
||||
}}
|
||||
>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<UnsavedWarnProvider>
|
||||
<AppRoutes />
|
||||
</UnsavedWarnProvider>
|
||||
</TooltipProvider>
|
||||
<Toaster />
|
||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
</AuthProvider>
|
||||
</DataSourceProvider>
|
||||
</QueryClientProvider>
|
||||
</I18nextProvider>
|
||||
|
||||
1
apps/web/src/lib/index.ts
Normal file
1
apps/web/src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./token";
|
||||
40
apps/web/src/lib/token.ts
Normal file
40
apps/web/src/lib/token.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import secureLocalStorage from "react-secure-storage";
|
||||
|
||||
/**
|
||||
* Servicio para manejar la obtención del token JWT desde el almacenamiento local.
|
||||
* Este archivo puede evolucionar a un AuthService más completo en el futuro.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clave utilizada en el almacenamiento local para el token JWT.
|
||||
*/
|
||||
|
||||
const TOKEN_STORAGE_KEY = "factuges.auth";
|
||||
|
||||
/**
|
||||
* Obtiene el token JWT almacenado localmente.
|
||||
*
|
||||
* @returns El token como string, o null si no está disponible.
|
||||
*/
|
||||
export const getAccessToken = (): string | null => {
|
||||
const authInfo = secureLocalStorage.getItem(TOKEN_STORAGE_KEY) as { token?: string } | null;
|
||||
return typeof authInfo?.token === "string" ? authInfo.token : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Almacena el token JWT localmente.
|
||||
*
|
||||
* @params El token como string.
|
||||
*/
|
||||
export const setAccessToken = (token: string): void => {
|
||||
secureLocalStorage.setItem(TOKEN_STORAGE_KEY, token);
|
||||
};
|
||||
|
||||
setAccessToken;
|
||||
|
||||
/**
|
||||
* Limpia el token JWT del almacenamiento local.
|
||||
*/
|
||||
export const clearAccessToken = (): void => {
|
||||
secureLocalStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
};
|
||||
@ -28,6 +28,7 @@
|
||||
"useOptionalChain": "info"
|
||||
},
|
||||
"suspicious": {
|
||||
"noImplicitAnyLet": "info",
|
||||
"noExplicitAny": "info"
|
||||
},
|
||||
"style": {
|
||||
|
||||
@ -14,12 +14,14 @@
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@repo/rdx-ui": "workspace:*",
|
||||
"@erp/core": "workspace:*",
|
||||
"@repo/rdx-ui": "workspace:*",
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.74.11",
|
||||
"i18next": "^25.1.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.56.2",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-secure-storage": "^1.3.2"
|
||||
}
|
||||
|
||||
@ -1,16 +1,27 @@
|
||||
import { ModuleClientParams } from "@erp/core/client";
|
||||
import { RouteObject } from "react-router-dom";
|
||||
import { Outlet, RouteObject } from "react-router-dom";
|
||||
import { AuthLayout } from "./components";
|
||||
import { LoginPage } from "./pages";
|
||||
|
||||
export const AuthRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
return [
|
||||
{
|
||||
path: "login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "register",
|
||||
element: <div>Register</div>, // o tu componente real
|
||||
path: "*",
|
||||
element: (
|
||||
<AuthLayout>
|
||||
<Outlet context={params} />
|
||||
</AuthLayout>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "register",
|
||||
element: <div>Register</div>,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
5
modules/auth/src/web/components/auth-layout.tsx
Normal file
5
modules/auth/src/web/components/auth-layout.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export const AuthLayout = ({ children }: PropsWithChildren) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
@ -1 +1,3 @@
|
||||
export * from "./auth-guard";
|
||||
export * from "./auth-layout";
|
||||
export * from "./login-form";
|
||||
|
||||
85
modules/auth/src/web/components/login-form.tsx
Normal file
85
modules/auth/src/web/components/login-form.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
type LoginFormValues = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
interface LoginFormProps extends React.ComponentPropsWithoutRef<"div"> {
|
||||
onSubmit: SubmitHandler<LoginFormValues>;
|
||||
onInvalid?: SubmitErrorHandler<LoginFormValues>;
|
||||
}
|
||||
|
||||
export function LoginForm({ onSubmit, onInvalid, className, ...props }: LoginFormProps) {
|
||||
const form = useForm<LoginFormValues>({
|
||||
mode: "onBlur",
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-2xl'>Login</CardTitle>
|
||||
<CardDescription>Enter your email below to login to your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit, onInvalid)} className='flex flex-col gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='email' autoComplete='username' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='password' autoComplete='current-password' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type='submit' className='w-full'>
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,18 +1,28 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { PropsWithChildren, createContext, useEffect, useState } from "react";
|
||||
import { IAuthService } from "../services";
|
||||
|
||||
interface AuthContextType {
|
||||
export interface AuthContextType {
|
||||
token: string | null;
|
||||
isAuthenticated: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
export interface AuthContextParams {
|
||||
authService: IAuthService;
|
||||
getAccessToken: () => string | null;
|
||||
setAccessToken: (token: string) => void;
|
||||
clearAccessToken: () => void;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Proveedor de autenticación para toda la app.
|
||||
*/
|
||||
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
export const AuthProvider = ({ params, children }: PropsWithChildren<{ params: any }>) => {
|
||||
const { getAccessToken, setAccessToken, clearAccessToken, authService } = params;
|
||||
|
||||
const [token, setToken] = useState<string | null>(getAccessToken());
|
||||
|
||||
useEffect(() => {
|
||||
@ -21,7 +31,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const { access_token } = await authService.login({ email, password });
|
||||
localStorage.setItem("access_token", access_token);
|
||||
setAccessToken(access_token);
|
||||
setToken(access_token);
|
||||
};
|
||||
|
||||
@ -36,14 +46,3 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook para acceder al contexto de autenticación.
|
||||
*/
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth debe usarse dentro de <AuthProvider>");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
1
modules/auth/src/web/context/index.ts
Normal file
1
modules/auth/src/web/context/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./auth-context";
|
||||
@ -1,43 +0,0 @@
|
||||
import { IGetProfileResponse_DTO } from "@shared/contexts";
|
||||
|
||||
export type SuccessNotificationResponse = {
|
||||
message: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type PermissionResponse = unknown;
|
||||
|
||||
export type ProfileResponse = IGetProfileResponse_DTO | null;
|
||||
|
||||
export type AuthActionCheckResponse = {
|
||||
authenticated: boolean;
|
||||
redirectTo?: string;
|
||||
logout?: boolean;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export type AuthActionOnErrorResponse = {
|
||||
redirectTo?: string;
|
||||
logout?: boolean;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export type AuthActionResponse = {
|
||||
success: boolean;
|
||||
redirectTo?: string;
|
||||
error?: Error;
|
||||
[key: string]: unknown;
|
||||
successNotification?: SuccessNotificationResponse;
|
||||
};
|
||||
|
||||
export interface IAuthActions {
|
||||
login: (params: any) => Promise<AuthActionResponse>;
|
||||
logout: (params: any) => Promise<AuthActionResponse>;
|
||||
check: () => Promise<AuthActionCheckResponse>;
|
||||
onError?: (error: any) => Promise<AuthActionOnErrorResponse>;
|
||||
register?: (params: unknown) => Promise<AuthActionResponse>;
|
||||
forgotPassword?: (params: unknown) => Promise<AuthActionResponse>;
|
||||
updatePassword?: (params: unknown) => Promise<AuthActionResponse>;
|
||||
getPermissions?: (params?: Record<string, unknown>) => Promise<PermissionResponse>;
|
||||
getProfile?: (params?: unknown) => Promise<ProfileResponse>;
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import { PropsWithChildren, createContext } from "react";
|
||||
import { IAuthActions } from "./auth-actions";
|
||||
|
||||
export interface IAuthContextState extends Partial<IAuthActions> {}
|
||||
|
||||
export const AuthContext = createContext<Partial<IAuthContextState>>({});
|
||||
|
||||
export const AuthProvider = ({
|
||||
children,
|
||||
authActions,
|
||||
}: PropsWithChildren<{ authActions: Partial<IAuthActions> }>) => {
|
||||
const handleLogin = (params: unknown) => {
|
||||
try {
|
||||
return Promise.resolve(authActions.login?.(params));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = (params: unknown) => {
|
||||
try {
|
||||
return Promise.resolve(authActions.logout?.(params));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheck = async () => {
|
||||
try {
|
||||
return Promise.resolve(authActions.check?.());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
...authActions,
|
||||
login: handleLogin as IAuthActions["login"],
|
||||
logout: handleLogout as IAuthActions["logout"],
|
||||
check: handleCheck as IAuthActions["check"],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
export * from "./auth-actions";
|
||||
export * from "./auth-context";
|
||||
export * from "./use-auth";
|
||||
export * from "./use-get-profile";
|
||||
export * from "./use-is-logged-in";
|
||||
export * from "./use-login";
|
||||
@ -1,8 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { AuthContext } from "./auth-context";
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === null) throw new Error("useAuth must be used within a AuthProvider");
|
||||
return context;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { ProfileResponse } from "./auth-actions";
|
||||
|
||||
export const useGetProfile = (
|
||||
queryOptions?: Omit<UseQueryOptions<ProfileResponse>, "queryKey" | "queryFn">
|
||||
) => {
|
||||
const keys = useQueryKey();
|
||||
const { getProfile } = useAuth();
|
||||
|
||||
const result = useQuery<ProfileResponse>({
|
||||
queryKey: keys().auth().action("profile").get(),
|
||||
queryFn: getProfile,
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { AuthActionCheckResponse } from "./auth-actions";
|
||||
import { useAuth } from "./use-auth";
|
||||
|
||||
export const useIsLoggedIn = (queryOptions?: UseQueryOptions<AuthActionCheckResponse>) => {
|
||||
const keys = useQueryKey();
|
||||
const { check } = useAuth();
|
||||
|
||||
const result = useQuery<AuthActionCheckResponse>({
|
||||
queryKey: keys().auth().action("check").get(),
|
||||
queryFn: check,
|
||||
retry: false,
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { AuthActionResponse, useAuth } from "@/lib/hooks";
|
||||
import { ILogin_DTO } from "@shared/contexts";
|
||||
import { UseMutationOptions, useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { useQueryKey } from "../useQueryKey";
|
||||
|
||||
export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error, ILogin_DTO>) => {
|
||||
const keys = useQueryKey();
|
||||
const { login } = useAuth();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: keys().auth().action("login").get(),
|
||||
mutationFn: login,
|
||||
...params,
|
||||
});
|
||||
};
|
||||
@ -1,45 +0,0 @@
|
||||
import { AuthActionResponse, useAuth } from "@/lib/hooks";
|
||||
import { UseMutationOptions, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useToast } from "@/ui/use-toast";
|
||||
import { useQueryKey } from "../useQueryKey";
|
||||
|
||||
export const useLogout = (params?: UseMutationOptions<AuthActionResponse, Error>) => {
|
||||
const { onSuccess, onError, ...restParams } = params || {};
|
||||
const queryClient = useQueryClient();
|
||||
const keys = useQueryKey();
|
||||
const { logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: keys().auth().action("logout").get(),
|
||||
mutationFn: logout,
|
||||
|
||||
onSuccess: async (data, variables, context) => {
|
||||
queryClient.clear();
|
||||
|
||||
const { success, redirectTo } = data;
|
||||
if (success && redirectTo) {
|
||||
navigate(redirectTo || "/");
|
||||
}
|
||||
if (onSuccess) {
|
||||
onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
const { message } = error;
|
||||
toast({
|
||||
title: "Error",
|
||||
description: message,
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
if (onError) {
|
||||
onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
...restParams,
|
||||
});
|
||||
};
|
||||
@ -1,3 +1,2 @@
|
||||
export * from "./useAuth";
|
||||
export * from "./useCurrentUser";
|
||||
export * from "./useIsAuthenticated";
|
||||
export * from "./use-auth";
|
||||
export * from "./use-is-authenticated";
|
||||
|
||||
13
modules/auth/src/web/hooks/use-auth.ts
Normal file
13
modules/auth/src/web/hooks/use-auth.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { useContext } from "react";
|
||||
import { AuthContext, AuthContextType } from "../context";
|
||||
|
||||
/**
|
||||
* Hook para acceder al contexto de autenticación.
|
||||
*/
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth debe usarse dentro de <AuthProvider>");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { useAuth } from "./useAuth";
|
||||
import { useAuth } from "./use-auth";
|
||||
|
||||
/**
|
||||
* Devuelve un booleano reactivo si el usuario está autenticado.
|
||||
@ -1,2 +1,5 @@
|
||||
export * from "./context";
|
||||
export * from "./hooks";
|
||||
export * from "./lib";
|
||||
export * from "./manifest";
|
||||
export * from "./services";
|
||||
|
||||
@ -1,7 +1,20 @@
|
||||
import { LoginForm } from "../components";
|
||||
import { useAuth } from "../hooks";
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleOnSubmit = (data) => {
|
||||
console.log(data);
|
||||
const { email, password } = data;
|
||||
login(email, password);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Iniciar Sesión</h2>
|
||||
<div className='flex min-h-svh w-full items-center justify-center p-6 md:p-10'>
|
||||
<div className='w-full max-w-sm'>
|
||||
<LoginForm onSubmit={handleOnSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
export const authService = createAuthActions(axiosClient);
|
||||
import { IDataSource } from "@erp/core/client";
|
||||
import { ILoginRequestDTO, ILoginResponseDTO } from "../../common";
|
||||
|
||||
/**
|
||||
* Autentica al usuario con email y password, y guarda el token en localStorage.
|
||||
*/
|
||||
export const login = async (email: string, password: string): Promise<void> => {
|
||||
const { access_token } = await authService.login({ email, password });
|
||||
setAccessToken(access_token);
|
||||
};
|
||||
export interface IAuthService {
|
||||
login: (credentials: ILoginRequestDTO) => Promise<ILoginResponseDTO>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia el token local y ejecuta el logout remoto (si aplica).
|
||||
*/
|
||||
export const logout = async (): Promise<void> => {
|
||||
await authService.logout(); // opcional: puede no existir en backend
|
||||
clearAccessToken();
|
||||
export const createAuthService = (dataSource: IDataSource): IAuthService => {
|
||||
return {
|
||||
login: async (credentials: ILoginRequestDTO) => {
|
||||
const data = await dataSource.custom<ILoginResponseDTO>({
|
||||
path: "login",
|
||||
method: "post",
|
||||
data: credentials,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./auth-service";
|
||||
@ -0,0 +1,16 @@
|
||||
import { useDataSource } from "@erp/core/client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "../hooks";
|
||||
import { User } from "./types";
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
const { token } = useAuth();
|
||||
const client = useDataSource();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: () => client.getOne<User>("/auth", "me"),
|
||||
enabled: !!token,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { useDataSource } from "@erp/core/client";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { User } from "./types";
|
||||
|
||||
export const useUsers = () => {
|
||||
const client = useDataSource();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["users"],
|
||||
queryFn: () => client.getList<User>("/users"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateUser = () => {
|
||||
const client = useDataSource();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (user: Partial<User>) => client.createOne<User>("/users", user),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,21 @@
|
||||
import { AxiosInstance } from "axios";
|
||||
import { IDataSource } from "../datasource.interface";
|
||||
import { ICustomParams, IDataSource } from "../datasource.interface";
|
||||
import { defaultAxiosRequestConfig } from "./create-axios-instance";
|
||||
|
||||
export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
||||
function getBaseUrlOrFail(): string {
|
||||
const baseUrl = client.getUri();
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error("[Axios] baseURL no está definido en esta instancia.");
|
||||
}
|
||||
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
return {
|
||||
getBaseUrl: getBaseUrlOrFail,
|
||||
|
||||
getList: async <T>(resource: string, params?: Record<string, any>) => {
|
||||
const res = await client.get<T[]>(resource, { params });
|
||||
return res.data;
|
||||
@ -31,5 +44,54 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
||||
deleteOne: async <T>(resource: string, id: string | number) => {
|
||||
await client.delete(`${resource}/${id}`);
|
||||
},
|
||||
|
||||
custom: async <T>(customParams: ICustomParams) => {
|
||||
const { url, path, method, responseType, headers, signal, data, ...payload } = customParams;
|
||||
|
||||
const requestUrl = path ? `${getBaseUrlOrFail()}/${path}` : url;
|
||||
if (!requestUrl) throw new Error('"url" or "path" param is missing');
|
||||
|
||||
const config = {
|
||||
url: requestUrl,
|
||||
method,
|
||||
responseType,
|
||||
signal,
|
||||
...payload,
|
||||
...defaultAxiosRequestConfig,
|
||||
headers,
|
||||
};
|
||||
|
||||
let customResponse;
|
||||
|
||||
switch (method) {
|
||||
case "put":
|
||||
case "post":
|
||||
case "patch":
|
||||
customResponse = await client.request<T>({
|
||||
...config,
|
||||
data,
|
||||
});
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
customResponse = await client.delete<T>(requestUrl, {
|
||||
responseType,
|
||||
headers,
|
||||
...payload,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
customResponse = await client.get<T>(requestUrl, {
|
||||
responseType,
|
||||
signal,
|
||||
headers,
|
||||
...payload,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return customResponse.data;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -20,6 +20,15 @@ export interface AxiosFactoryConfig {
|
||||
onAuthError?: () => void;
|
||||
}
|
||||
|
||||
export const defaultAxiosRequestConfig = {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Access-Control-Allow-Origin": "*", // Could work and fix the previous problem, but not in all APIs
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Crea una instancia de Axios preconfigurada con interceptores.
|
||||
*
|
||||
@ -33,12 +42,7 @@ export const createAxiosInstance = ({
|
||||
}: AxiosFactoryConfig): AxiosInstance => {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Access-Control-Allow-Origin": "*", // Could work and fix the previous problem, but not in all APIs
|
||||
},
|
||||
...defaultAxiosRequestConfig,
|
||||
});
|
||||
|
||||
setupInterceptors(instance, getAccessToken, onAuthError);
|
||||
|
||||
@ -1,8 +1,25 @@
|
||||
import { ResponseType } from "axios";
|
||||
|
||||
export interface ICustomParams {
|
||||
url?: string;
|
||||
path?: string;
|
||||
method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch";
|
||||
signal?: AbortSignal;
|
||||
responseType?: ResponseType;
|
||||
headers?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface IDataSource {
|
||||
getBaseUrl(): string;
|
||||
getList<T>(resource: string, params?: Record<string, any>): Promise<T[]>;
|
||||
getOne<T>(resource: string, id: string | number): Promise<T>;
|
||||
getMany<T>(resource: string, ids: Array<string | number>): Promise<T[]>;
|
||||
createOne<T>(resource: string, data: Partial<T>): Promise<T>;
|
||||
updateOne<T>(resource: string, id: string | number, data: Partial<T>): Promise<T>;
|
||||
deleteOne<T>(resource: string, id: string | number): Promise<void>;
|
||||
|
||||
custom: <R>(customParams: ICustomParams) => Promise<R>;
|
||||
}
|
||||
|
||||
3205
pnpm-lock.yaml
3205
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user