.
This commit is contained in:
parent
2c89ebb002
commit
0fdf18baf3
@ -41,7 +41,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^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-hook-form-persist": "^3.0.0",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
|
|||||||
@ -6,15 +6,12 @@ import { I18nextProvider } from "react-i18next";
|
|||||||
import { UnsavedWarnProvider } from "@/lib/hooks";
|
import { UnsavedWarnProvider } from "@/lib/hooks";
|
||||||
import { i18n } from "@/locales";
|
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 { DataSourceProvider, createAxiosDataSource, createAxiosInstance } from "@erp/core/client";
|
||||||
import "./app.css";
|
|
||||||
import { AppRoutes } from "./routes/app-routes";
|
|
||||||
|
|
||||||
/**
|
import "./app.css";
|
||||||
* Clave utilizada en el almacenamiento local para el token JWT.
|
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib";
|
||||||
*/
|
import { AppRoutes } from "./routes";
|
||||||
const TOKEN_STORAGE_KEY = "factuges.auth";
|
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@ -26,28 +23,39 @@ export const App = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataSource = createAxiosDataSource(
|
const axiosInstance = createAxiosInstance({
|
||||||
createAxiosInstance({
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
getAccessToken,
|
||||||
getAccessToken: () => getAccessToken(TOKEN_STORAGE_KEY),
|
onAuthError: () => {
|
||||||
onAuthError: () => {
|
clearAccessToken();
|
||||||
clearAccessToken(TOKEN_STORAGE_KEY);
|
window.location.href = "/login"; // o usar navegación programática
|
||||||
window.location.href = "/login"; // o usar navegación programática
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
);
|
console.log(axiosInstance.defaults.env);
|
||||||
|
|
||||||
|
const dataSource = createAxiosDataSource(axiosInstance);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<DataSourceProvider dataSource={dataSource}>
|
<DataSourceProvider dataSource={dataSource}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<AuthProvider
|
||||||
<UnsavedWarnProvider>
|
params={{
|
||||||
<AppRoutes />
|
getAccessToken,
|
||||||
</UnsavedWarnProvider>
|
setAccessToken,
|
||||||
</TooltipProvider>
|
clearAccessToken,
|
||||||
<Toaster />
|
authService: createAuthService(dataSource),
|
||||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
}}
|
||||||
|
>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<UnsavedWarnProvider>
|
||||||
|
<AppRoutes />
|
||||||
|
</UnsavedWarnProvider>
|
||||||
|
</TooltipProvider>
|
||||||
|
<Toaster />
|
||||||
|
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||||
|
</AuthProvider>
|
||||||
</DataSourceProvider>
|
</DataSourceProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</I18nextProvider>
|
</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"
|
"useOptionalChain": "info"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
|
"noImplicitAnyLet": "info",
|
||||||
"noExplicitAny": "info"
|
"noExplicitAny": "info"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -14,12 +14,14 @@
|
|||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/shadcn-ui": "workspace:*",
|
|
||||||
"@repo/rdx-ui": "workspace:*",
|
|
||||||
"@erp/core": "workspace:*",
|
"@erp/core": "workspace:*",
|
||||||
|
"@repo/rdx-ui": "workspace:*",
|
||||||
|
"@repo/shadcn-ui": "workspace:*",
|
||||||
|
"@tanstack/react-query": "^5.74.11",
|
||||||
"i18next": "^25.1.1",
|
"i18next": "^25.1.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-hook-form": "^7.56.2",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"react-secure-storage": "^1.3.2"
|
"react-secure-storage": "^1.3.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,27 @@
|
|||||||
import { ModuleClientParams } from "@erp/core/client";
|
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";
|
import { LoginPage } from "./pages";
|
||||||
|
|
||||||
export const AuthRoutes = (params: ModuleClientParams): RouteObject[] => {
|
export const AuthRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "*",
|
||||||
element: <LoginPage />,
|
element: (
|
||||||
},
|
<AuthLayout>
|
||||||
{
|
<Outlet context={params} />
|
||||||
path: "register",
|
</AuthLayout>
|
||||||
element: <div>Register</div>, // o tu componente real
|
),
|
||||||
|
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-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;
|
token: string | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
login: (email: string, password: string) => Promise<void>;
|
login: (email: string, password: string) => Promise<void>;
|
||||||
logout: () => 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.
|
* 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());
|
const [token, setToken] = useState<string | null>(getAccessToken());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,7 +31,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
|
|
||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
const { access_token } = await authService.login({ email, password });
|
const { access_token } = await authService.login({ email, password });
|
||||||
localStorage.setItem("access_token", access_token);
|
setAccessToken(access_token);
|
||||||
setToken(access_token);
|
setToken(access_token);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,14 +46,3 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</AuthContext.Provider>
|
</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 "./use-auth";
|
||||||
export * from "./useCurrentUser";
|
export * from "./use-is-authenticated";
|
||||||
export * from "./useIsAuthenticated";
|
|
||||||
|
|||||||
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.
|
* Devuelve un booleano reactivo si el usuario está autenticado.
|
||||||
@ -1,2 +1,5 @@
|
|||||||
|
export * from "./context";
|
||||||
|
export * from "./hooks";
|
||||||
export * from "./lib";
|
export * from "./lib";
|
||||||
export * from "./manifest";
|
export * from "./manifest";
|
||||||
|
export * from "./services";
|
||||||
|
|||||||
@ -1,7 +1,20 @@
|
|||||||
|
import { LoginForm } from "../components";
|
||||||
|
import { useAuth } from "../hooks";
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const handleOnSubmit = (data) => {
|
||||||
|
console.log(data);
|
||||||
|
const { email, password } = data;
|
||||||
|
login(email, password);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='flex min-h-svh w-full items-center justify-center p-6 md:p-10'>
|
||||||
<h2>Iniciar Sesión</h2>
|
<div className='w-full max-w-sm'>
|
||||||
|
<LoginForm onSubmit={handleOnSubmit} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,20 @@
|
|||||||
export const authService = createAuthActions(axiosClient);
|
import { IDataSource } from "@erp/core/client";
|
||||||
|
import { ILoginRequestDTO, ILoginResponseDTO } from "../../common";
|
||||||
|
|
||||||
/**
|
export interface IAuthService {
|
||||||
* Autentica al usuario con email y password, y guarda el token en localStorage.
|
login: (credentials: ILoginRequestDTO) => Promise<ILoginResponseDTO>;
|
||||||
*/
|
}
|
||||||
export const login = async (email: string, password: string): Promise<void> => {
|
|
||||||
const { access_token } = await authService.login({ email, password });
|
|
||||||
setAccessToken(access_token);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
export const createAuthService = (dataSource: IDataSource): IAuthService => {
|
||||||
* Limpia el token local y ejecuta el logout remoto (si aplica).
|
return {
|
||||||
*/
|
login: async (credentials: ILoginRequestDTO) => {
|
||||||
export const logout = async (): Promise<void> => {
|
const data = await dataSource.custom<ILoginResponseDTO>({
|
||||||
await authService.logout(); // opcional: puede no existir en backend
|
path: "login",
|
||||||
clearAccessToken();
|
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 { 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 => {
|
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 {
|
return {
|
||||||
|
getBaseUrl: getBaseUrlOrFail,
|
||||||
|
|
||||||
getList: async <T>(resource: string, params?: Record<string, any>) => {
|
getList: async <T>(resource: string, params?: Record<string, any>) => {
|
||||||
const res = await client.get<T[]>(resource, { params });
|
const res = await client.get<T[]>(resource, { params });
|
||||||
return res.data;
|
return res.data;
|
||||||
@ -31,5 +44,54 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
|||||||
deleteOne: async <T>(resource: string, id: string | number) => {
|
deleteOne: async <T>(resource: string, id: string | number) => {
|
||||||
await client.delete(`${resource}/${id}`);
|
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;
|
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.
|
* Crea una instancia de Axios preconfigurada con interceptores.
|
||||||
*
|
*
|
||||||
@ -33,12 +42,7 @@ export const createAxiosInstance = ({
|
|||||||
}: AxiosFactoryConfig): AxiosInstance => {
|
}: AxiosFactoryConfig): AxiosInstance => {
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: {
|
...defaultAxiosRequestConfig,
|
||||||
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
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setupInterceptors(instance, getAccessToken, onAuthError);
|
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 {
|
export interface IDataSource {
|
||||||
|
getBaseUrl(): string;
|
||||||
getList<T>(resource: string, params?: Record<string, any>): Promise<T[]>;
|
getList<T>(resource: string, params?: Record<string, any>): Promise<T[]>;
|
||||||
getOne<T>(resource: string, id: string | number): Promise<T>;
|
getOne<T>(resource: string, id: string | number): Promise<T>;
|
||||||
getMany<T>(resource: string, ids: Array<string | number>): Promise<T[]>;
|
getMany<T>(resource: string, ids: Array<string | number>): Promise<T[]>;
|
||||||
createOne<T>(resource: string, data: Partial<T>): Promise<T>;
|
createOne<T>(resource: string, data: Partial<T>): Promise<T>;
|
||||||
updateOne<T>(resource: string, id: string | number, 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>;
|
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