From d69c8b3831cd1e56302e10a833a4b2569eb1220c Mon Sep 17 00:00:00 2001 From: David Arranz Date: Tue, 1 Oct 2024 12:21:08 +0200 Subject: [PATCH] Enviar una incidencia --- client/index.html | 2 +- client/package.json | 1 - client/src/App.tsx | 40 ++--- client/src/Routes.tsx | 32 ++-- client/src/app/catalog/layout.tsx | 16 +- client/src/app/dealers/layout.tsx | 12 +- client/src/app/quotes/create.tsx | 14 +- client/src/app/quotes/index.ts | 1 + client/src/app/quotes/layout.tsx | 10 +- client/src/app/settings/layout.tsx | 30 ++-- .../app/support/components/SupportModal.tsx | 160 ++++++++++++++++++ client/src/app/support/components/index.tsx | 1 + client/src/app/support/hooks/index.ts | 1 + client/src/app/support/hooks/useSupport.tsx | 28 +++ client/src/app/support/index.tsx | 2 + client/src/components/Layout/Layout.tsx | 10 +- client/src/components/Layout/LayoutHeader.tsx | 2 + .../ProtectedRoute/ProtectedRoute.tsx | 6 +- .../src/lib/axios/createAxiosDataProvider.ts | 62 +++---- client/src/lib/hooks/useAuth/useLogout.tsx | 9 +- .../src/lib/hooks/useDataSource/DataSource.ts | 3 +- client/src/locales/es.json | 7 + client/src/ui/toast.tsx | 5 +- client/src/ui/toaster.tsx | 6 +- client/src/ui/use-toast.ts | 6 +- client/yarn.lock | 9 +- server/package.json | 2 + server/src/config/environments/development.ts | 7 + server/src/config/environments/production.ts | 16 ++ server/src/config/index.ts | 15 +- .../auth/infrastructure/Auth.context.ts | 36 +--- .../auth/infrastructure/Auth.repository.ts | 25 +-- .../controllers/AuthenticateController.ts | 36 ---- .../identity/presenter/Identity.presenter.ts | 2 +- .../login/presenter/Login.presenter.ts | 2 +- .../express/controllers/profileMiddleware.ts | 18 -- .../express/passport/Auth.middleware.ts | 69 ++++++++ .../express/passport/authMiddleware.ts | 65 ------- .../express/passport/configurePassportAuth.ts | 7 +- .../express/passport/emailStrategy.ts | 5 +- .../infrastructure/express/passport/index.ts | 2 +- .../express/passport/jwtStrategy.ts | 6 +- .../src/contexts/auth/infrastructure/index.ts | 2 + .../infrastructure/mappers/authuser.mapper.ts | 4 - .../catalog/infrastructure/Catalog.context.ts | 33 +--- .../application/services/Email.service.ts | 20 +++ .../common/application/services/index.ts | 1 + .../common/infrastructure/Common.context.ts | 22 +++ .../common/infrastructure/ContextFactory.ts | 31 ---- .../infrastructure/InfrastructureError.ts | 15 +- .../express/ControllerFactory.ts | 9 +- .../infrastructure/express/middlewares.ts | 2 +- .../contexts/common/infrastructure/index.ts | 2 +- .../nodemailer/BrevoMailService.ts | 29 ++++ .../nodemailer/LoggerMailService.ts | 10 ++ .../common/infrastructure/nodemailer/index.ts | 2 + .../profile/infrastructure/Profile.context.ts | 26 +-- .../sales/infrastructure/Sales.context.ts | 26 +-- .../updateQuote/UpdateQuote.controller.ts | 36 +--- .../express/middlewares/Dealer.middleware.ts | 94 ++++++++++ .../express/middlewares/dealerMiddleware.ts | 39 ----- .../application/SendIncidence.useCase.ts | 119 +++++++++++++ .../src/contexts/support/application/index.ts | 1 + .../src/contexts/support/domain/Incidence.ts | 45 +++++ server/src/contexts/support/domain/index.ts | 1 + .../support/infrastructure/Support.context.ts | 10 ++ .../express/controllers/index.ts | 1 + .../sendIncidence/SendIncidence.controller.ts | 76 +++++++++ .../controllers/sendIncidence/index.ts | 16 ++ .../support/infrastructure/express/index.ts | 1 + .../contexts/support/infrastructure/index.ts | 2 + .../users/infrastructure/User.context.ts | 36 +--- .../express/api/CommonContext.middleware.ts | 8 + ...oad.middleware.ts => Upload.middleware.ts} | 0 .../express/api/context.middleware.ts | 3 - .../express/api/routes/catalog.routes.ts | 2 +- .../express/api/routes/dealers.routes.ts | 8 +- .../express/api/routes/profile.routes.ts | 12 +- .../express/api/routes/quote.routes.ts | 4 +- .../express/api/routes/support.routes.ts | 32 ++++ .../express/api/routes/users.routes.ts | 10 +- server/src/infrastructure/express/api/v1.ts | 27 ++- server/src/infrastructure/express/app.ts | 8 +- server/src/infrastructure/logger/index.ts | 6 +- .../src/infrastructure/sequelize/initData.ts | 11 +- server/yarn.lock | 12 ++ shared/lib/contexts/index.ts | 1 + .../ISendIncidence_Request.dto.ts | 20 +++ .../support/dto/SendIncidence.dto/index.ts | 1 + shared/lib/contexts/support/dto/index.ts | 1 + shared/lib/contexts/support/index.ts | 1 + 91 files changed, 1063 insertions(+), 603 deletions(-) create mode 100644 client/src/app/support/components/SupportModal.tsx create mode 100644 client/src/app/support/components/index.tsx create mode 100644 client/src/app/support/hooks/index.ts create mode 100644 client/src/app/support/hooks/useSupport.tsx create mode 100644 client/src/app/support/index.tsx delete mode 100644 server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts delete mode 100644 server/src/contexts/auth/infrastructure/express/controllers/profileMiddleware.ts create mode 100644 server/src/contexts/auth/infrastructure/express/passport/Auth.middleware.ts delete mode 100644 server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts create mode 100644 server/src/contexts/common/application/services/Email.service.ts create mode 100644 server/src/contexts/common/infrastructure/Common.context.ts delete mode 100644 server/src/contexts/common/infrastructure/ContextFactory.ts create mode 100644 server/src/contexts/common/infrastructure/nodemailer/BrevoMailService.ts create mode 100644 server/src/contexts/common/infrastructure/nodemailer/LoggerMailService.ts create mode 100644 server/src/contexts/common/infrastructure/nodemailer/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/middlewares/Dealer.middleware.ts delete mode 100644 server/src/contexts/sales/infrastructure/express/middlewares/dealerMiddleware.ts create mode 100644 server/src/contexts/support/application/SendIncidence.useCase.ts create mode 100644 server/src/contexts/support/application/index.ts create mode 100644 server/src/contexts/support/domain/Incidence.ts create mode 100644 server/src/contexts/support/domain/index.ts create mode 100644 server/src/contexts/support/infrastructure/Support.context.ts create mode 100644 server/src/contexts/support/infrastructure/express/controllers/index.ts create mode 100644 server/src/contexts/support/infrastructure/express/controllers/sendIncidence/SendIncidence.controller.ts create mode 100644 server/src/contexts/support/infrastructure/express/controllers/sendIncidence/index.ts create mode 100644 server/src/contexts/support/infrastructure/express/index.ts create mode 100644 server/src/contexts/support/infrastructure/index.ts create mode 100644 server/src/infrastructure/express/api/CommonContext.middleware.ts rename server/src/infrastructure/express/api/{upload.middleware.ts => Upload.middleware.ts} (100%) delete mode 100644 server/src/infrastructure/express/api/context.middleware.ts create mode 100644 server/src/infrastructure/express/api/routes/support.routes.ts create mode 100644 shared/lib/contexts/support/dto/SendIncidence.dto/ISendIncidence_Request.dto.ts create mode 100644 shared/lib/contexts/support/dto/SendIncidence.dto/index.ts create mode 100644 shared/lib/contexts/support/dto/index.ts create mode 100644 shared/lib/contexts/support/index.ts diff --git a/client/index.html b/client/index.html index 0bc7085..5535581 100644 --- a/client/index.html +++ b/client/index.html @@ -1,4 +1,4 @@ - + diff --git a/client/package.json b/client/package.json index ad36a47..c485461 100644 --- a/client/package.json +++ b/client/package.json @@ -68,7 +68,6 @@ "react-resizable-panels": "^2.0.23", "react-router-dom": "^6.26.0", "react-secure-storage": "^1.3.2", - "react-toastify": "^10.0.5", "react-wrap-balancer": "^1.1.1", "recharts": "^2.12.7", "slugify": "^1.6.6", diff --git a/client/src/App.tsx b/client/src/App.tsx index 096ad99..ce91ad4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,7 +3,6 @@ import { Toaster, TooltipProvider } from "@/ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Suspense } from "react"; -import "react-toastify/dist/ReactToastify.css"; import { Routes } from "./Routes"; import { LoadingOverlay, TailwindIndicator } from "./components"; import { createAxiosAuthActions, createAxiosDataProvider } from "./lib/axios"; @@ -20,25 +19,26 @@ function App() { }); return ( - - - - - - - }> - - - - - - - - - - - - + <> + + + + + + + }> + + + + + + + + + + + + ); } diff --git a/client/src/Routes.tsx b/client/src/Routes.tsx index 3f8a102..72b2a2f 100644 --- a/client/src/Routes.tsx +++ b/client/src/Routes.tsx @@ -13,7 +13,7 @@ import { } from "./app"; import { CatalogLayout, CatalogList } from "./app/catalog"; import { DashboardPage } from "./app/dashboard"; -import { QuotesLayout } from "./app/quotes/layout"; +import { QuotesLayout } from "./app/quotes"; import { QuotesList } from "./app/quotes/list"; import { ProtectedRoute } from "./components"; @@ -46,11 +46,9 @@ export const Routes = () => { { path: "/catalog", element: ( - - - - - + + + ), children: [ { @@ -62,11 +60,9 @@ export const Routes = () => { { path: "/dealers", element: ( - - - - - + + + ), children: [ { @@ -77,7 +73,11 @@ export const Routes = () => { }, { path: "/quotes", - element: , + element: ( + + + + ), children: [ { index: true, @@ -96,11 +96,9 @@ export const Routes = () => { { path: "/settings", element: ( - - - - - + + + ), children: [ { diff --git a/client/src/app/catalog/layout.tsx b/client/src/app/catalog/layout.tsx index 4b388c3..af37672 100644 --- a/client/src/app/catalog/layout.tsx +++ b/client/src/app/catalog/layout.tsx @@ -1,14 +1,16 @@ -import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { Layout, LayoutContent, LayoutHeader, ProtectedRoute } from "@/components"; import { PropsWithChildren } from "react"; import { CatalogProvider } from "./CatalogContext"; export const CatalogLayout = ({ children }: PropsWithChildren) => { return ( - - - - {children} - - + + + + + {children} + + + ); }; diff --git a/client/src/app/dealers/layout.tsx b/client/src/app/dealers/layout.tsx index 169331e..456ab43 100644 --- a/client/src/app/dealers/layout.tsx +++ b/client/src/app/dealers/layout.tsx @@ -1,11 +1,13 @@ -import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { Layout, LayoutContent, LayoutHeader, ProtectedRoute } from "@/components"; import { PropsWithChildren } from "react"; export const DealerLayout = ({ children }: PropsWithChildren) => { return ( - - - {children} - + + + + {children} + + ); }; diff --git a/client/src/app/quotes/create.tsx b/client/src/app/quotes/create.tsx index 1bdb926..2e0c6fb 100644 --- a/client/src/app/quotes/create.tsx +++ b/client/src/app/quotes/create.tsx @@ -11,13 +11,13 @@ import { t } from "i18next"; import { SubmitButton } from "@/components"; import { useUnsavedChangesNotifier } from "@/lib/hooks"; import { Button, Form, Separator } from "@/ui"; +import { useToast } from "@/ui/use-toast"; import { joiResolver } from "@hookform/resolvers/joi"; import { ICreateQuote_Request_DTO } from "@shared/contexts"; import Joi from "joi"; import { useMemo } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; -import { toast } from "react-toastify"; import SpanishJoiMessages from "../../spanish-joi-messages.json"; import { useQuotes } from "./hooks"; @@ -25,6 +25,7 @@ interface QuoteDataForm extends ICreateQuote_Request_DTO {} export const QuoteCreate = () => { const navigate = useNavigate(); + const { toast } = useToast(); const { useCreate } = useQuotes(); const { mutate, isPending } = useCreate(); @@ -67,11 +68,18 @@ export const QuoteCreate = () => { mutate(formData, { onError: (error) => { console.debug(error); - toast.error(error.message); + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); }, onSuccess: (data) => { reset(getValues()); - toast.success("Cotización guardada"); + toast({ + title: "Cotización guardada", + className: "bg-green-300", + }); navigate(`/quotes/edit/${data.id}`, { relative: "path" }); }, }); diff --git a/client/src/app/quotes/index.ts b/client/src/app/quotes/index.ts index ca5d8e1..541935e 100644 --- a/client/src/app/quotes/index.ts +++ b/client/src/app/quotes/index.ts @@ -1,3 +1,4 @@ export * from "./create"; export * from "./edit"; +export * from "./layout"; export * from "./list"; diff --git a/client/src/app/quotes/layout.tsx b/client/src/app/quotes/layout.tsx index 6f10f29..d8e8111 100644 --- a/client/src/app/quotes/layout.tsx +++ b/client/src/app/quotes/layout.tsx @@ -1,16 +1,14 @@ import { Layout, LayoutContent, LayoutHeader, ProtectedRoute } from "@/components"; -import { Outlet } from "react-router-dom"; +import { PropsWithChildren } from "react"; import { QuotesProvider } from "./QuotesContext"; -export const QuotesLayout = () => { +export const QuotesLayout = ({ children }: PropsWithChildren) => { return ( - + - - - + {children} diff --git a/client/src/app/settings/layout.tsx b/client/src/app/settings/layout.tsx index 0e16be8..d6fc2ce 100644 --- a/client/src/app/settings/layout.tsx +++ b/client/src/app/settings/layout.tsx @@ -1,22 +1,24 @@ -import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { Layout, LayoutContent, LayoutHeader, ProtectedRoute } from "@/components"; import { PropsWithChildren } from "react"; import { Trans } from "react-i18next"; import { SettingsProvider } from "./SettingsContext"; export const SettingsLayout = ({ children }: PropsWithChildren) => { return ( - - - - -
-

- -

-
- {children} -
-
-
+ + + + + +
+

+ +

+
+ {children} +
+
+
+
); }; diff --git a/client/src/app/support/components/SupportModal.tsx b/client/src/app/support/components/SupportModal.tsx new file mode 100644 index 0000000..9b453d3 --- /dev/null +++ b/client/src/app/support/components/SupportModal.tsx @@ -0,0 +1,160 @@ +import { FormTextAreaField } from "@/components"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + Form, +} from "@/ui"; +import { useState } from "react"; + +import { SubmitHandler, useForm } from "react-hook-form"; + +import { useToast } from "@/ui/use-toast"; +import { joiResolver } from "@hookform/resolvers/joi"; +import { ISendIncidence_Request_DTO } from "@shared/contexts"; +import { t } from "i18next"; +import Joi from "joi"; +import { HelpCircleIcon } from "lucide-react"; +import { useSupport } from "../hooks"; + +const formSchema = Joi.object({ + incidence: Joi.string().min(10).required().messages({ + "string.empty": "Debe escribir algo antes de enviar", + "string.min": "El texto es demasiado corto. Debe tener al menos 10 caracteres", + "string.max": "El texto es demasiado largo.", + "any.required": "La descripción es requerida", + }), +}); + +type SupportDataForm = ISendIncidence_Request_DTO; + +export default function SupportModal() { + const [isOpen, setIsOpen] = useState(false); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const { toast } = useToast(); + const { useSubmitIncidence } = useSupport(); + + const form = useForm({ + mode: "onBlur", + resolver: joiResolver(formSchema), + defaultValues: { + incidence: "", + }, + }); + + const { handleSubmit, watch, reset } = form; + + const incidenceValue = watch("incidence"); + + const { mutate } = useSubmitIncidence({ + mutateOptions: { + onSuccess: () => { + toast({ + title: "Incidencia enviada", + description: "La incidencia se ha enviado correctamente", + variant: "success", + }); + setIsOpen(false); + reset(); + }, + onError: () => { + toast({ + title: "Error en el envío", + description: + "No se ha podido enviar la incidencia correctamente. Por favor, inténtalo de nuevo.", + variant: "destructive", + }); + }, + }, + }); + + const onSubmit: SubmitHandler = async (data) => { + mutate(data); + }; + + const handleClose = () => { + console.log("handleClose", incidenceValue.trim()); + if (incidenceValue.trim()) { + setShowConfirmDialog(true); + } else { + setIsOpen(false); + reset(); + } + }; + + const confirmClose = () => { + setShowConfirmDialog(false); + setIsOpen(false); + reset(); + }; + + return ( + <> + + + + + + + {t("support.modal.title")} + {t("support.modal.subtitle")} + +
+ + + + + + + + +
+
+ + + + + ¿Estás seguro de que quieres cancelar? + + Has escrito texto en el campo de descripción. Si cierras la ventana, perderás los + cambios no guardados. + + + + setShowConfirmDialog(false)}> + Volver al formulario + + Sí, cerrar + + + + + ); +} diff --git a/client/src/app/support/components/index.tsx b/client/src/app/support/components/index.tsx new file mode 100644 index 0000000..54246b8 --- /dev/null +++ b/client/src/app/support/components/index.tsx @@ -0,0 +1 @@ +export * from "./SupportModal"; diff --git a/client/src/app/support/hooks/index.ts b/client/src/app/support/hooks/index.ts new file mode 100644 index 0000000..e6d9a2a --- /dev/null +++ b/client/src/app/support/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useSupport"; diff --git a/client/src/app/support/hooks/useSupport.tsx b/client/src/app/support/hooks/useSupport.tsx new file mode 100644 index 0000000..f4d1219 --- /dev/null +++ b/client/src/app/support/hooks/useSupport.tsx @@ -0,0 +1,28 @@ +import { TDataSourceError } from "@/lib/hooks/useDataSource/types"; +import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource"; +import { ISendIncidence_Request_DTO } from "@shared/contexts"; +import { useMutation, UseMutationOptions } from "@tanstack/react-query"; + +export type UseSupportGetParamsType = { + mutateOptions?: UseMutationOptions; +}; + +export const useSupport = () => { + const dataSource = useDataSource(); + + return { + useSubmitIncidence: (params?: UseSupportGetParamsType) => { + const { mutateOptions = {} } = params || {}; + + return useMutation({ + mutationFn: (data) => { + return dataSource.createOne({ + resource: "support", + data, + }); + }, + ...mutateOptions, + }); + }, + }; +}; diff --git a/client/src/app/support/index.tsx b/client/src/app/support/index.tsx new file mode 100644 index 0000000..a234113 --- /dev/null +++ b/client/src/app/support/index.tsx @@ -0,0 +1,2 @@ +export * from "./components"; +export * from "./hooks"; diff --git a/client/src/components/Layout/Layout.tsx b/client/src/components/Layout/Layout.tsx index c2696e4..c7c3ed2 100644 --- a/client/src/components/Layout/Layout.tsx +++ b/client/src/components/Layout/Layout.tsx @@ -1,9 +1,15 @@ import { UnsavedWarnProvider } from "@/lib/hooks"; +import { cn } from "@/lib/utils"; import { PropsWithChildren } from "react"; -export const Layout = ({ children }: PropsWithChildren) => ( +export const Layout = ({ + className, + children, +}: PropsWithChildren<{ + className?: string; +}>) => ( -
{children}
+
{children}
); diff --git a/client/src/components/Layout/LayoutHeader.tsx b/client/src/components/Layout/LayoutHeader.tsx index e139948..104fa65 100644 --- a/client/src/components/Layout/LayoutHeader.tsx +++ b/client/src/components/Layout/LayoutHeader.tsx @@ -1,5 +1,6 @@ import { Button, Sheet, SheetContent, SheetTrigger } from "@/ui"; +import SupportModal from "@/app/support/components/SupportModal"; import { cn } from "@/lib/utils"; import { MenuIcon, Package2Icon } from "lucide-react"; import { useCallback } from "react"; @@ -105,6 +106,7 @@ export const LayoutHeader = () => {
+
); diff --git a/client/src/components/ProtectedRoute/ProtectedRoute.tsx b/client/src/components/ProtectedRoute/ProtectedRoute.tsx index bad1eed..3a85512 100644 --- a/client/src/components/ProtectedRoute/ProtectedRoute.tsx +++ b/client/src/components/ProtectedRoute/ProtectedRoute.tsx @@ -23,16 +23,16 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => { } }, [profile, profileStatus, i18n]);*/ - if (isPending) { + /*if (isPending) { return null; - } + }*/ if (isSuccess && !authenticated) { // Redirect them to the /login page, but save the current location they were // trying to go to when they were redirected. This allows us to send them // along to that page after they login, which is a nicer user experience // than dropping them off on the home page. - return ; + return ; } return <>{children ?? null}; diff --git a/client/src/lib/axios/createAxiosDataProvider.ts b/client/src/lib/axios/createAxiosDataProvider.ts index efc5eb4..8089b95 100644 --- a/client/src/lib/axios/createAxiosDataProvider.ts +++ b/client/src/lib/axios/createAxiosDataProvider.ts @@ -15,7 +15,7 @@ import { IUpdateOneDataProviderParams, IUploadFileDataProviderParam, } from "../hooks/useDataSource/DataSource"; -import { createAxiosInstance } from "./axiosInstance"; +import { createAxiosInstance, defaultAxiosRequestConfig } from "./axiosInstance"; export const createAxiosDataProvider = ( apiUrl: string, @@ -170,62 +170,54 @@ export const createAxiosDataProvider = ( }, custom: async (params: ICustomDataProviderParam): Promise => { - const { url, method, responseType, headers, signal, data, ...payload } = params; - const requestUrl = `${url}?`; + const { url, path, method, responseType, headers, signal, data, ...payload } = params; + let requestUrl: string; - /*if (sort) { - const generatedSort = extractSortParams(sort); - if (generatedSort) { - const { _sort, _order } = generatedSort; - const sortQuery = { - _sort: _sort.join(","), - _order: _order.join(","), - }; - requestUrl = `${requestUrl}&${queryString.stringify(sortQuery)}`; - } + if (path) { + requestUrl = `${apiUrl}/${path}`; + } else if (url) { + requestUrl = url; + } else { + throw new Error('"url" or "path" param is missing'); } - if (filters) { - const filterQuery = extractFilterParams(filters); - requestUrl = `${requestUrl}&${queryString.stringify(filterQuery)}`; - }*/ - - /*if (query) { - requestUrl = `${requestUrl}&${queryString.stringify(query)}`; - }*/ - - if (headers) { - httpClient.defaults.headers = { - ...httpClient.defaults.headers, - ...headers, - }; - } + console.log(apiUrl, path, url, requestUrl.toString()); + // Preparar la respuesta personalizada let customResponse; + + // Configurar opciones comunes para la petición + const config = { + url: requestUrl.toString(), + method, + responseType, + signal, + ...payload, + ...defaultAxiosRequestConfig, + }; + switch (method) { case "put": case "post": case "patch": customResponse = await httpClient.request({ - url, - method, - responseType, - headers, + ...config, data, - ...payload, }); break; case "delete": - customResponse = await httpClient.delete(url, { + customResponse = await httpClient.delete(requestUrl.toString(), { responseType, headers, + ...payload, }); break; default: - customResponse = await httpClient.get(requestUrl, { + customResponse = await httpClient.get(requestUrl.toString(), { responseType, signal, headers, + ...payload, }); break; } diff --git a/client/src/lib/hooks/useAuth/useLogout.tsx b/client/src/lib/hooks/useAuth/useLogout.tsx index a3e3a70..c8fca56 100644 --- a/client/src/lib/hooks/useAuth/useLogout.tsx +++ b/client/src/lib/hooks/useAuth/useLogout.tsx @@ -1,8 +1,8 @@ import { AuthActionResponse, useAuth } from "@/lib/hooks"; import { UseMutationOptions, useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; -import { toast } from "react-toastify"; +import { useToast } from "@/ui/use-toast"; import { useQueryKey } from "../useQueryKey"; export const useLogout = (params?: UseMutationOptions) => { @@ -11,6 +11,7 @@ export const useLogout = (params?: UseMutationOptions const keys = useQueryKey(); const { logout } = useAuth(); const navigate = useNavigate(); + const { toast } = useToast(); return useMutation({ mutationKey: keys().auth().action("logout").get(), @@ -29,7 +30,11 @@ export const useLogout = (params?: UseMutationOptions }, onError: (error, variables, context) => { const { message } = error; - toast.error(message); + toast({ + title: "Error", + description: message, + variant: "destructive", + }); if (onError) { onError(error, variables, context); diff --git a/client/src/lib/hooks/useDataSource/DataSource.ts b/client/src/lib/hooks/useDataSource/DataSource.ts index 1337511..16052eb 100644 --- a/client/src/lib/hooks/useDataSource/DataSource.ts +++ b/client/src/lib/hooks/useDataSource/DataSource.ts @@ -74,7 +74,8 @@ export interface IUploadFileDataProviderParam { } export interface ICustomDataProviderParam { - url: string; + url?: string; + path?: string; method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch"; signal?: AbortSignal; responseType?: ResponseType; diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 2cc47a2..69ce9ac 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -433,6 +433,13 @@ "desc": "Texto para indicar el tiempo de validez de la cotización" } } + }, + "support": { + "modal": { + "title": "Enviar una incidencia", + "subtitle": "Utiliza este formulario para informar sobre cualquier problema que hayas encontrado mientras usabas la aplicación. Nuestro equipo de desarrollo revisará tu incidencia y tratará de resolverla." + }, + "form_fields": {} } } } diff --git a/client/src/ui/toast.tsx b/client/src/ui/toast.tsx index 17d543f..cb8c526 100644 --- a/client/src/ui/toast.tsx +++ b/client/src/ui/toast.tsx @@ -1,3 +1,5 @@ +"use client"; + import * as ToastPrimitives from "@radix-ui/react-toast"; import { cva, type VariantProps } from "class-variance-authority"; import { X } from "lucide-react"; @@ -14,7 +16,7 @@ const ToastViewport = React.forwardRef< + {toasts.map(function ({ id, title, description, action, ...props }) { return ( @@ -25,7 +27,7 @@ export function Toaster() { ); })} - + ); } diff --git a/client/src/ui/use-toast.ts b/client/src/ui/use-toast.ts index a49b29c..b8ae16f 100644 --- a/client/src/ui/use-toast.ts +++ b/client/src/ui/use-toast.ts @@ -1,10 +1,12 @@ +"use client"; + // Inspired by react-hot-toast library import * as React from "react"; import type { ToastActionElement, ToastProps } from "./toast"; -const TOAST_LIMIT = 5; -const TOAST_REMOVE_DELAY = 10000; +const TOAST_LIMIT = 3; +const TOAST_REMOVE_DELAY = 1000000; type ToasterToast = ToastProps & { id: string; diff --git a/client/yarn.lock b/client/yarn.lock index 7836b34..b775f04 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2593,7 +2593,7 @@ clsx@2.0.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== -clsx@^2.0.0, clsx@^2.1.0, clsx@^2.1.1: +clsx@^2.0.0, clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -5094,13 +5094,6 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" -react-toastify@^10.0.5: - version "10.0.5" - resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e" - integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw== - dependencies: - clsx "^2.1.0" - react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" diff --git a/server/package.json b/server/package.json index 11941c7..8abe6ea 100644 --- a/server/package.json +++ b/server/package.json @@ -58,6 +58,7 @@ "@joi/date": "^2.1.0", "@reis/joi-luxon": "^3.0.0", "@types/mime-types": "^2.1.4", + "@types/nodemailer": "^6.4.16", "bcrypt": "^5.1.1", "cls-rtracer": "^2.6.3", "cors": "^2.8.5", @@ -79,6 +80,7 @@ "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "mysql2": "^3.6.0", + "nodemailer": "^6.9.15", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", diff --git a/server/src/config/environments/development.ts b/server/src/config/environments/development.ts index 3ffa188..6015526 100644 --- a/server/src/config/environments/development.ts +++ b/server/src/config/environments/development.ts @@ -41,6 +41,11 @@ module.exports = { "/home/rodax/Documentos/uecko-presupuestos/server/uploads/dealer-logos", dealer_logo_placeholder: "/home/rodax/Documentos/uecko-presupuestos/server/uploads/images/logo-placeholder-200x100.png", + + support: { + from: "noreply@presupuestos.uecko.com", + subject: "Nueva incidencia Presupuestador Uecko", + }, }, admin: { @@ -56,4 +61,6 @@ module.exports = { password: "123456", language: "en", }, + + nodemailer: {}, }; diff --git a/server/src/config/environments/production.ts b/server/src/config/environments/production.ts index 4406e30..f0be569 100644 --- a/server/src/config/environments/production.ts +++ b/server/src/config/environments/production.ts @@ -24,6 +24,10 @@ module.exports = { defaults: { dealer_logos_upload_path: "/api/uploads/dealer-logos", dealer_logo_placeholder: "/api/uploads/images/logo-placeholder-200x100.png", + + support: { + from: "noreply@presupuestos.uecko.com", + }, }, admin: { @@ -39,4 +43,16 @@ module.exports = { password: "123456", language: "en", }, + + nodemailer: { + brevo: { + host: "smtp-relay.brevo.com", + port: 587, + secure: false, + auth: { + user: "7d0c4e001@smtp-brevo.com", + pass: "xsmtpsib-42ff61d359e148710fce8376854330891677a38172fd4217a0dc220551cce210-Wxm4DQwItYgTUcF6", + }, + }, + }, }; diff --git a/server/src/config/index.ts b/server/src/config/index.ts index 40fe5d0..2a4b8c7 100644 --- a/server/src/config/index.ts +++ b/server/src/config/index.ts @@ -10,11 +10,10 @@ const extension = isProduction ? ".js" : ".ts"; const environmentConfig = require(path.resolve(__dirname, "environments", environment + extension)); -export const config = Object.assign( - { - environment, - isProduction, - isDevelopment, - }, - environmentConfig -); +export const config = { + environment, + isProduction, + isDevelopment, + + ...environmentConfig, +}; diff --git a/server/src/contexts/auth/infrastructure/Auth.context.ts b/server/src/contexts/auth/infrastructure/Auth.context.ts index aa1107c..0bfc092 100644 --- a/server/src/contexts/auth/infrastructure/Auth.context.ts +++ b/server/src/contexts/auth/infrastructure/Auth.context.ts @@ -1,35 +1,3 @@ -import { - IRepositoryManager, - RepositoryManager, -} from "@/contexts/common/domain"; -import { - ISequelizeAdapter, - createSequelizeAdapter, -} from "@/contexts/common/infrastructure/sequelize"; +import { ICommonContext } from "@/contexts/common/infrastructure"; -export interface IAuthContext { - adapter: ISequelizeAdapter; - repositoryManager: IRepositoryManager; - //services: IApplicationService; -} - -export class AuthContext { - private static instance: AuthContext | null = null; - - public static getInstance(): IAuthContext { - if (!AuthContext.instance) { - AuthContext.instance = new AuthContext({ - adapter: createSequelizeAdapter(), - repositoryManager: RepositoryManager.getInstance(), - }); - } - - return AuthContext.instance.context; - } - - private context: IAuthContext; - - private constructor(context: IAuthContext) { - this.context = context; - } -} +export interface IAuthContext extends ICommonContext {} diff --git a/server/src/contexts/auth/infrastructure/Auth.repository.ts b/server/src/contexts/auth/infrastructure/Auth.repository.ts index e84242d..79fedd1 100644 --- a/server/src/contexts/auth/infrastructure/Auth.repository.ts +++ b/server/src/contexts/auth/infrastructure/Auth.repository.ts @@ -1,13 +1,9 @@ -import { IAuthContext } from "./Auth.context"; - -import { - ISequelizeAdapter, - SequelizeRepository, -} from "@/contexts/common/infrastructure/sequelize"; +import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize"; import { Email, ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { Transaction } from "sequelize"; import { AuthUser } from "../domain/entities"; import { IAuthRepository } from "../domain/repository/AuthRepository.interface"; +import { IAuthContext } from "./Auth.context"; import { IUserMapper, createUserMapper } from "./mappers/authuser.mapper"; export type QueryParams = { @@ -15,10 +11,7 @@ export type QueryParams = { filters: Record; }; -export class AuthRepository - extends SequelizeRepository - implements IAuthRepository -{ +export class AuthRepository extends SequelizeRepository implements IAuthRepository { protected mapper: IUserMapper; public constructor(props: { @@ -42,11 +35,7 @@ export class AuthRepository } public async findUserByEmail(email: Email): Promise { - const rawUser: any = await this._getBy( - "AuthUser_Model", - "email", - email.toPrimitive(), - ); + const rawUser: any = await this._getBy("AuthUser_Model", "email", email.toPrimitive()); if (!rawUser === true) { return null; @@ -55,12 +44,10 @@ export class AuthRepository return this.mapper.mapToDomain(rawUser); } - public async findAll( - queryCriteria?: IQueryCriteria, - ): Promise> { + public async findAll(queryCriteria?: IQueryCriteria): Promise> { const { rows, count } = await this._findAll( "AuthUser_Model", - queryCriteria, + queryCriteria /*{ include: [], // esto es para quitar las asociaciones al hacer la consulta }*/ diff --git a/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts b/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts deleted file mode 100644 index c057f5d..0000000 --- a/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Import the necessary packages and modules -import { AuthUser } from "@/contexts/auth/domain"; -import { IServerError } from "@/contexts/common/domain/errors"; -import { ExpressController } from "@/contexts/common/infrastructure/express"; -import passport from "passport"; - -export class AuthenticateController extends ExpressController { - async executeImpl() { - try { - return passport.authenticate( - "local-jwt", - { session: false }, - ( - err: any, - user?: AuthUser | false | null, - info?: object | string | Array, - status?: number | Array - ) => { - if (err) { - return this.next(err); - } - - if (!user) { - return this.unauthorizedError("Unauthorized access. No token provided."); - } - - // If the user is authenticated, attach the user object to the request and move on to the next middleware - this.req["user"] = user; - return this.next(); - } - ); - } catch (e: unknown) { - return this.fail(e as IServerError); - } - } -} diff --git a/server/src/contexts/auth/infrastructure/express/controllers/identity/presenter/Identity.presenter.ts b/server/src/contexts/auth/infrastructure/express/controllers/identity/presenter/Identity.presenter.ts index 8921c85..ba70f73 100644 --- a/server/src/contexts/auth/infrastructure/express/controllers/identity/presenter/Identity.presenter.ts +++ b/server/src/contexts/auth/infrastructure/express/controllers/identity/presenter/Identity.presenter.ts @@ -1,5 +1,5 @@ import { IAuthUser } from "@/contexts/auth/domain"; -import { IAuthContext } from "@/contexts/auth/infrastructure/Auth.context"; +import { IAuthContext } from "@/contexts/auth/infrastructure"; import { IIdentity_Response_DTO } from "@shared/contexts"; export interface IIdentityUser extends IAuthUser {} diff --git a/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts b/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts index 9876025..13b1a32 100644 --- a/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts +++ b/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts @@ -1,5 +1,5 @@ import { IAuthUser } from "@/contexts/auth/domain"; -import { IAuthContext } from "@/contexts/auth/infrastructure/Auth.context"; +import { IAuthContext } from "@/contexts/auth/infrastructure"; import { ILogin_Response_DTO } from "@shared/contexts"; export interface ILoginUser { diff --git a/server/src/contexts/auth/infrastructure/express/controllers/profileMiddleware.ts b/server/src/contexts/auth/infrastructure/express/controllers/profileMiddleware.ts deleted file mode 100644 index f1e7ca7..0000000 --- a/server/src/contexts/auth/infrastructure/express/controllers/profileMiddleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AuthUser } from "@/contexts/auth/domain"; -import { generateExpressError } from "@/contexts/common/infrastructure/express"; -import { NextFunction, Request, Response } from "express"; -import httpStatus from "http-status"; - -interface AuthenticatedRequest extends Request { - user?: AuthUser; -} - -const profileMiddleware = (req: Request, res: Response, next: NextFunction) => { - const _req = req as AuthenticatedRequest; - const user = _req.user; - - if (!user || !user.isAdmin) { - generateExpressError(req, res, httpStatus.UNAUTHORIZED); - } - next(); -}; diff --git a/server/src/contexts/auth/infrastructure/express/passport/Auth.middleware.ts b/server/src/contexts/auth/infrastructure/express/passport/Auth.middleware.ts new file mode 100644 index 0000000..a54e882 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/passport/Auth.middleware.ts @@ -0,0 +1,69 @@ +import { AuthUser } from "@/contexts/auth/domain"; +import { ICommonContext } from "@/contexts/common/infrastructure"; +import { generateExpressError } from "@/contexts/common/infrastructure/express"; +import { ensureIdIsValid } from "@shared/contexts"; +import { NextFunction, Request, Response } from "express"; +import httpStatus from "http-status"; +import passport from "passport"; + +// Extender el Request de Express para incluir el usuario autenticado optionalmente +interface AuthenticatedRequest extends Request { + user?: AuthUser; +} + +// Middleware para autenticar usando passport con el local-jwt strategy +const authenticateJwt = passport.authenticate("local-jwt", { session: false }); + +// Para establecer el contexto de autenticación +const setAuthContext = (req: AuthenticatedRequest, res: Response, user: AuthUser) => { + const { context } = res.locals || {}; + res.locals.context = { + ...context, + user, + } as ICommonContext; +}; + +// Comprueba el rol del usuario +const authorizeUser = (condition: (user: AuthUser) => boolean) => { + return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + const user = req.user as AuthUser; + if (!user || !condition(user)) { + return generateExpressError(req, res, httpStatus.UNAUTHORIZED); + } + + setAuthContext(req, res, user); + next(); + }; +}; + +// Verifica que el usuario esté autenticado +export const checkUser = [authenticateJwt, authorizeUser((user) => user.isUser)]; + +// Verifica que el usuario sea administrador +export const checkIsAdmin = [authenticateJwt, authorizeUser((user) => user.isAdmin)]; + +// Middleware para verificar que el usuario sea administrador o el dueño de los datos (self) +export const checkAdminOrSelf = [ + authenticateJwt, + (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + const user = req.user as AuthUser; + const { userId } = req.params; + + // Si el usuario es admin, está autorizado + if (user.isAdmin) { + setAuthContext(req, res, user); + next(); + } + + // Si el usuario es sí mismo + if (user.isUser && userId) { + const paramIdOrError = ensureIdIsValid(userId); + if (paramIdOrError.isSuccess && user.id.equals(paramIdOrError.object)) { + setAuthContext(req, res, user); + next(); + } + } + + return generateExpressError(req, res, httpStatus.UNAUTHORIZED); + }, +]; diff --git a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts b/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts deleted file mode 100644 index 50d56ac..0000000 --- a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { AuthUser } from "@/contexts/auth/domain"; -import { composeMiddleware, generateExpressError } from "@/contexts/common/infrastructure/express"; -import { ensureIdIsValid } from "@shared/contexts"; -import { NextFunction, Request, Response } from "express"; -import httpStatus from "http-status"; -import passport from "passport"; - -interface AuthenticatedRequest extends Request { - user?: AuthUser; -} - -export const checkUser = composeMiddleware([ - passport.authenticate("local-jwt", { - session: false, - }), - (req: Request, res: Response, next: NextFunction) => { - const _req = req as AuthenticatedRequest; - const user = _req.user; - - if (!user || !user.isUser) { - return generateExpressError(req, res, httpStatus.UNAUTHORIZED); - } - return next(); - }, -]); - -export const checkisAdmin = composeMiddleware([ - passport.authenticate("local-jwt", { - session: false, - }), - (req: Request, res: Response, next: NextFunction) => { - const _req = req as AuthenticatedRequest; - const user = _req.user; - - if (!user || !user.isAdmin) { - generateExpressError(req, res, httpStatus.UNAUTHORIZED); - } - return next(); - }, -]); - -export const checkAdminOrSelf = composeMiddleware([ - passport.authenticate("local-jwt", { - session: false, - }), - (req: Request, res: Response, next: NextFunction) => { - const _req = req as AuthenticatedRequest; - const user = _req.user; - - const { userId } = req.params; - - if (user && user.isAdmin) { - return next(); - } - - if (user && user.isUser && userId) { - const paramIdOrError = ensureIdIsValid(userId); - if (paramIdOrError.isSuccess && user.id.equals(paramIdOrError.object)) { - return next(); - } - } - - return generateExpressError(req, res, httpStatus.UNAUTHORIZED); - }, -]); diff --git a/server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts b/server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts index a602bee..9845ba0 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts @@ -1,10 +1,11 @@ +import { createCommonContext } from "@/contexts/common/infrastructure"; import { PassportStatic } from "passport"; -import { AuthContext } from "../../Auth.context"; import { initEmailStrategy } from "./emailStrategy"; import { initJWTStrategy } from "./jwtStrategy"; // Export a function that will be used to configure Passport authentication export const configurePassportAuth = (passport: PassportStatic) => { - passport.use("local-email", initEmailStrategy(AuthContext.getInstance())); - passport.use("local-jwt", initJWTStrategy(AuthContext.getInstance())); + const context = createCommonContext(); + passport.use("local-email", initEmailStrategy(context)); + passport.use("local-jwt", initJWTStrategy(context)); }; diff --git a/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts b/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts index 08aa73e..0ead12d 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts @@ -5,6 +5,7 @@ import { Strategy as EmailStrategy, IVerifyOptions } from "passport-local"; import { LoginUseCase } from "@/contexts/auth/application"; import { AuthUser } from "@/contexts/auth/domain"; +import { ICommonContext } from "@/contexts/common/infrastructure"; import { IAuthContext } from "../../Auth.context"; import { registerAuthRepository } from "../../Auth.repository"; @@ -54,9 +55,9 @@ class EmailStrategyController extends PassportStrategyController { } } -export const initEmailStrategy = (context: IAuthContext) => +export const initEmailStrategy = (context: ICommonContext) => new EmailStrategy(strategyOpts, async (username, password, done) => { - registerAuthRepository(context); + registerAuthRepository(context as IAuthContext); return new EmailStrategyController( { useCase: new LoginUseCase(context), diff --git a/server/src/contexts/auth/infrastructure/express/passport/index.ts b/server/src/contexts/auth/infrastructure/express/passport/index.ts index 6c00109..bc5beb5 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/index.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/index.ts @@ -1,2 +1,2 @@ -export * from "./authMiddleware"; +export * from "./Auth.middleware"; export * from "./configurePassportAuth"; diff --git a/server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts b/server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts index 7dd5288..8a38fdb 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts @@ -1,6 +1,7 @@ import { config } from "@/config"; import { FindUserByEmailUseCase } from "@/contexts/auth/application/FindUserByEmail.useCase"; import { IServerError } from "@/contexts/common/domain/errors"; +import { createCommonContext, ICommonContext } from "@/contexts/common/infrastructure"; import { PassportStrategyController } from "@/contexts/common/infrastructure/express"; import { ExtractJwt, Strategy as JWTStrategy, VerifiedCallback } from "passport-jwt"; import { IAuthContext } from "../../Auth.context"; @@ -44,9 +45,10 @@ class JWTStrategyController extends PassportStrategyController { } } -export const initJWTStrategy = (context: IAuthContext) => +export const initJWTStrategy = (context: ICommonContext) => new JWTStrategy(strategyOpts, async (payload, done) => { - registerAuthRepository(context); + const context = createCommonContext(); + registerAuthRepository(context as IAuthContext); return new JWTStrategyController( { useCase: new FindUserByEmailUseCase(context), diff --git a/server/src/contexts/auth/infrastructure/index.ts b/server/src/contexts/auth/infrastructure/index.ts index c754fca..5447837 100644 --- a/server/src/contexts/auth/infrastructure/index.ts +++ b/server/src/contexts/auth/infrastructure/index.ts @@ -1,2 +1,4 @@ +export * from "./Auth.context"; +export * from "./Auth.repository"; export * from "./express"; export * from "./sequelize"; diff --git a/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts b/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts index 3d2c65e..1c0fe57 100644 --- a/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts +++ b/server/src/contexts/auth/infrastructure/mappers/authuser.mapper.ts @@ -13,10 +13,6 @@ class AuthUserMapper extends SequelizeMapper implements IUserMapper { - public constructor(props: { context: IAuthContext }) { - super(props); - } - protected toDomainMappingImpl(source: AuthUser_Model, params: any): AuthUser { const props: IAuthUserProps = { name: this.mapsValue(source, "name", Name.create), diff --git a/server/src/contexts/catalog/infrastructure/Catalog.context.ts b/server/src/contexts/catalog/infrastructure/Catalog.context.ts index 63aa2e3..85a3f93 100644 --- a/server/src/contexts/catalog/infrastructure/Catalog.context.ts +++ b/server/src/contexts/catalog/infrastructure/Catalog.context.ts @@ -1,32 +1,3 @@ -import { IRepositoryManager, RepositoryManager } from "@/contexts/common/domain"; -import { - ISequelizeAdapter, - createSequelizeAdapter, -} from "@/contexts/common/infrastructure/sequelize"; +import { ICommonContext } from "@/contexts/common/infrastructure"; -export interface ICatalogContext { - adapter: ISequelizeAdapter; - repositoryManager: IRepositoryManager; - //services: IApplicationService; -} - -export class CatalogContext { - private static instance: CatalogContext | null = null; - - public static getInstance(): ICatalogContext { - if (!CatalogContext.instance) { - CatalogContext.instance = new CatalogContext({ - adapter: createSequelizeAdapter(), - repositoryManager: RepositoryManager.getInstance(), - }); - } - - return CatalogContext.instance.context; - } - - private context: ICatalogContext; - - private constructor(context: ICatalogContext) { - this.context = context; - } -} +export interface ICatalogContext extends ICommonContext {} diff --git a/server/src/contexts/common/application/services/Email.service.ts b/server/src/contexts/common/application/services/Email.service.ts new file mode 100644 index 0000000..9e8f111 --- /dev/null +++ b/server/src/contexts/common/application/services/Email.service.ts @@ -0,0 +1,20 @@ +export interface ISendEmailAddress { + name: string; + address: string; +} + +export interface ISendEmailOptions { + from: string | ISendEmailAddress | undefined; + to: string | ISendEmailAddress; + subject?: string; + text?: string; + html?: string; + cc?: string | ISendEmailAddress | Array | undefined; + bcc?: string | ISendEmailAddress | Array | undefined; + replyTo?: string | ISendEmailAddress | Array | undefined; + attachments?: Array<{ filename: string; path: string }>; +} + +export interface IEmailService { + sendMail(mailOptions: ISendEmailOptions, dry?: boolean): Promise; +} diff --git a/server/src/contexts/common/application/services/index.ts b/server/src/contexts/common/application/services/index.ts index 8975c2e..c37281e 100644 --- a/server/src/contexts/common/application/services/index.ts +++ b/server/src/contexts/common/application/services/index.ts @@ -1,2 +1,3 @@ export * from "./ApplicationService"; +export * from "./Email.service"; export * from "./QueryCriteriaService"; diff --git a/server/src/contexts/common/infrastructure/Common.context.ts b/server/src/contexts/common/infrastructure/Common.context.ts new file mode 100644 index 0000000..9c462c3 --- /dev/null +++ b/server/src/contexts/common/infrastructure/Common.context.ts @@ -0,0 +1,22 @@ +import { config } from "@/config"; +import { RepositoryManager } from "@/contexts/common/domain"; +import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; + +import { AuthUser } from "@/contexts/auth/domain"; +import { IRepositoryManager } from "../domain"; +import { ISequelizeAdapter } from "./sequelize"; + +export interface IContext {} +11111111; +export interface ICommonContext extends IContext { + adapter: ISequelizeAdapter; + repositoryManager: IRepositoryManager; + defaults: Record; + user?: AuthUser; +} + +export const createCommonContext = () => ({ + defaults: config.defaults, + adapter: createSequelizeAdapter(), + repositoryManager: RepositoryManager.getInstance(), +}); diff --git a/server/src/contexts/common/infrastructure/ContextFactory.ts b/server/src/contexts/common/infrastructure/ContextFactory.ts deleted file mode 100644 index cb9edd7..0000000 --- a/server/src/contexts/common/infrastructure/ContextFactory.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { config } from "../../../config"; -import { IRepositoryManager, RepositoryManager } from "../domain"; -import { ISequelizeAdapter, createSequelizeAdapter } from "./sequelize"; - -export interface IContext { - adapter: ISequelizeAdapter; - repositoryManager: IRepositoryManager; - defaults: Record; -} - -export class ContextFactory { - protected static instance: ContextFactory | null = null; - - public static getInstance(): IContext { - if (!ContextFactory.instance) { - ContextFactory.instance = new ContextFactory({ - defaults: config.defaults, // Agregamos los valores específicos de ProfileContext - adapter: createSequelizeAdapter(), - repositoryManager: RepositoryManager.getInstance(), - }); - } - - return ContextFactory.instance.context; - } - - protected context: IContext; - - protected constructor(context: IContext) { - this.context = context; - } -} diff --git a/server/src/contexts/common/infrastructure/InfrastructureError.ts b/server/src/contexts/common/infrastructure/InfrastructureError.ts index 8c2c494..f370f06 100755 --- a/server/src/contexts/common/infrastructure/InfrastructureError.ts +++ b/server/src/contexts/common/infrastructure/InfrastructureError.ts @@ -4,21 +4,18 @@ import { IServerError, ServerError } from "../domain/errors"; export interface IInfrastructureError extends IServerError {} -export class InfrastructureError - extends ServerError - implements IInfrastructureError -{ +export class InfrastructureError extends ServerError implements IInfrastructureError { + public static readonly BAD_REQUEST = "BAD_REQUEST"; public static readonly UNEXCEPTED_ERROR = "UNEXCEPTED_ERROR"; public static readonly INVALID_INPUT_DATA = "INVALID_INPUT_DATA"; public static readonly RESOURCE_NOT_READY = "RESOURCE_NOT_READY"; public static readonly RESOURCE_NOT_FOUND_ERROR = "RESOURCE_NOT_FOUND_ERROR"; - public static readonly RESOURCE_ALREADY_REGISTERED = - "RESOURCE_ALREADY_REGISTERED"; + public static readonly RESOURCE_ALREADY_REGISTERED = "RESOURCE_ALREADY_REGISTERED"; public static create( code: string, message: string, - error?: UseCaseError | ValidationError, + error?: UseCaseError | ValidationError ): InfrastructureError { let payload = {}; @@ -29,9 +26,7 @@ export class InfrastructureError } else { // UseCaseError const _error = error; - const _payload = Array.isArray(_error.payload) - ? _error.payload - : [_error.payload]; + const _payload = Array.isArray(_error.payload) ? _error.payload : [_error.payload]; payload = _payload.map((item: Record) => ({ path: item.path, diff --git a/server/src/contexts/common/infrastructure/express/ControllerFactory.ts b/server/src/contexts/common/infrastructure/express/ControllerFactory.ts index ce83606..ede1780 100644 --- a/server/src/contexts/common/infrastructure/express/ControllerFactory.ts +++ b/server/src/contexts/common/infrastructure/express/ControllerFactory.ts @@ -1,12 +1,7 @@ import { NextFunction, Request, Response } from "express"; -import { IContext } from "../ContextFactory"; -import { ExpressController } from "./ExpressController"; - -export type ControllerFactory = (context: T) => ExpressController; export const handleRequest = - (controllerFactory: ControllerFactory) => - (req: Request, res: Response, next: NextFunction) => { - const context: T = res.locals["context"]; + (controllerFactory: any) => (req: Request, res: Response, next: NextFunction) => { + const context = res.locals["context"]; return controllerFactory(context).execute(req, res, next); }; diff --git a/server/src/contexts/common/infrastructure/express/middlewares.ts b/server/src/contexts/common/infrastructure/express/middlewares.ts index 944ae16..8ddf546 100644 --- a/server/src/contexts/common/infrastructure/express/middlewares.ts +++ b/server/src/contexts/common/infrastructure/express/middlewares.ts @@ -40,7 +40,7 @@ function composeMiddleware(middlewareArray: any[]) { return function (req: Request, res: Response, next: NextFunction) { head(req, res, function (err: unknown) { - if (err) return next(err); + if (err) next(err); composeMiddleware(tail)(req, res, next); }); }; diff --git a/server/src/contexts/common/infrastructure/index.ts b/server/src/contexts/common/infrastructure/index.ts index 0d15aac..118f020 100644 --- a/server/src/contexts/common/infrastructure/index.ts +++ b/server/src/contexts/common/infrastructure/index.ts @@ -1,4 +1,4 @@ -export * from "./ContextFactory"; +export * from "./Common.context"; export * from "./Controller.interface"; export * from "./InfrastructureError"; export * from "./mappers"; diff --git a/server/src/contexts/common/infrastructure/nodemailer/BrevoMailService.ts b/server/src/contexts/common/infrastructure/nodemailer/BrevoMailService.ts new file mode 100644 index 0000000..131b10d --- /dev/null +++ b/server/src/contexts/common/infrastructure/nodemailer/BrevoMailService.ts @@ -0,0 +1,29 @@ +import { config } from "@/config"; +import nodemailer from "nodemailer"; +import { ApplicationService, IEmailService, ISendEmailOptions } from "../../application"; +import { LoggerMailService } from "./LoggerMailService"; + +export class BrevoMailService extends ApplicationService implements IEmailService { + private transporter: any; + + constructor() { + super(); + this.transporter = nodemailer.createTransport(config.nodemailer.brevo); + } + + async sendMail(mailOptions: ISendEmailOptions, dry?: boolean): Promise { + if (dry) { + // No enviar el email + new LoggerMailService().sendMail(mailOptions); + return; + } + + await this.transporter.sendMail({ + ...mailOptions, + attachments: mailOptions.attachments?.map((att) => ({ + filename: att.filename, + path: att.path, + })), + }); + } +} diff --git a/server/src/contexts/common/infrastructure/nodemailer/LoggerMailService.ts b/server/src/contexts/common/infrastructure/nodemailer/LoggerMailService.ts new file mode 100644 index 0000000..2e64847 --- /dev/null +++ b/server/src/contexts/common/infrastructure/nodemailer/LoggerMailService.ts @@ -0,0 +1,10 @@ +import { logger } from "@/infrastructure/logger"; +import { ApplicationService, IEmailService, ISendEmailOptions } from "../../application"; + +export class LoggerMailService extends ApplicationService implements IEmailService { + async sendMail(mailOptions: ISendEmailOptions): Promise { + await logger().debug( + `Email no enviado (modo desarrollo):\n${JSON.stringify(mailOptions, null, 2)}\n\n` + ); + } +} diff --git a/server/src/contexts/common/infrastructure/nodemailer/index.ts b/server/src/contexts/common/infrastructure/nodemailer/index.ts new file mode 100644 index 0000000..05d38bc --- /dev/null +++ b/server/src/contexts/common/infrastructure/nodemailer/index.ts @@ -0,0 +1,2 @@ +export * from "./BrevoMailService"; +export * from "./LoggerMailService"; diff --git a/server/src/contexts/profile/infrastructure/Profile.context.ts b/server/src/contexts/profile/infrastructure/Profile.context.ts index 20caa96..6ebc748 100644 --- a/server/src/contexts/profile/infrastructure/Profile.context.ts +++ b/server/src/contexts/profile/infrastructure/Profile.context.ts @@ -1,25 +1,3 @@ -import { ContextFactory, IContext } from "@/contexts/common/infrastructure"; +import { ICommonContext } from "@/contexts/common/infrastructure"; -export interface IProfileContext extends IContext {} - -export class ProfileContext extends ContextFactory { - protected static instance: ProfileContext | null = null; - - public static getInstance(): IProfileContext { - if (!ProfileContext.instance) { - try { - ProfileContext.instance = new ProfileContext({ - ...ContextFactory.getInstance(), // Reutilizamos el contexto de la clase base - }); - } catch (error: unknown) { - throw new Error(`Error initializing ProfileContext: ${(error as Error).message}`); - } - } - - return ProfileContext.instance.context as IProfileContext; - } - - private constructor(context: IProfileContext) { - super(context); // Llamamos al constructor de la clase base - } -} +export interface IProfileContext extends ICommonContext {} diff --git a/server/src/contexts/sales/infrastructure/Sales.context.ts b/server/src/contexts/sales/infrastructure/Sales.context.ts index b8f70ee..b721ecd 100644 --- a/server/src/contexts/sales/infrastructure/Sales.context.ts +++ b/server/src/contexts/sales/infrastructure/Sales.context.ts @@ -1,31 +1,9 @@ -import { ContextFactory, IContext } from "@/contexts/common/infrastructure"; +import { ICommonContext } from "@/contexts/common/infrastructure"; import { Dealer, IQuoteReferenceGeneratorService } from "../domain"; -export interface ISalesContext extends IContext { +export interface ISalesContext extends ICommonContext { services?: { QuoteReferenceGeneratorService: IQuoteReferenceGeneratorService; }; dealer?: Dealer; } - -export class SalesContext extends ContextFactory { - protected static instance: SalesContext | null = null; - - public static getInstance(): ISalesContext { - if (!SalesContext.instance) { - try { - SalesContext.instance = new SalesContext({ - ...ContextFactory.getInstance(), // Reutilizamos el contexto de la clase base - }); - } catch (error: unknown) { - throw new Error(`Error initializing SalesContext: ${(error as Error).message}`); - } - } - - return SalesContext.instance.context; - } - - private constructor(context: ISalesContext) { - super(context); // Llamamos al constructor de la clase base - } -} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/updateQuote/UpdateQuote.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/updateQuote/UpdateQuote.controller.ts index f9a8a87..bad9b47 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/updateQuote/UpdateQuote.controller.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/updateQuote/UpdateQuote.controller.ts @@ -82,53 +82,33 @@ export class UpdateQuoteController extends ExpressController { } private _handleExecuteError(error: IUseCaseError) { + const createInfraError = (infrastructureCode: string, message: string) => { + return InfrastructureError.create(infrastructureCode, message, error); + }; + let errorMessage: string; let infraError: IInfrastructureError; switch (error.code) { case UseCaseError.NOT_FOUND_ERROR: errorMessage = "Quote not found"; - - infraError = InfrastructureError.create( - InfrastructureError.RESOURCE_NOT_FOUND_ERROR, - errorMessage, - error - ); - + infraError = createInfraError(InfrastructureError.RESOURCE_NOT_FOUND_ERROR, errorMessage); return this.notFoundError(errorMessage, infraError); - break; case UseCaseError.INVALID_INPUT_DATA: errorMessage = "Quote data not valid"; - - infraError = InfrastructureError.create( - InfrastructureError.INVALID_INPUT_DATA, - "Datos del cliente a actulizar erróneos", - error - ); + infraError = createInfraError(InfrastructureError.INVALID_INPUT_DATA, errorMessage); return this.invalidInputError(errorMessage, infraError); - break; case UseCaseError.REPOSITORY_ERROR: errorMessage = "Error updating quote"; - infraError = InfrastructureError.create( - InfrastructureError.UNEXCEPTED_ERROR, - errorMessage, - error - ); + infraError = createInfraError(InfrastructureError.UNEXCEPTED_ERROR, errorMessage); return this.conflictError(errorMessage, infraError); - break; case UseCaseError.UNEXCEPTED_ERROR: errorMessage = error.message; - - infraError = InfrastructureError.create( - InfrastructureError.UNEXCEPTED_ERROR, - errorMessage, - error - ); + infraError = createInfraError(InfrastructureError.UNEXCEPTED_ERROR, errorMessage); return this.internalServerError(errorMessage, infraError); - break; default: errorMessage = error.message; diff --git a/server/src/contexts/sales/infrastructure/express/middlewares/Dealer.middleware.ts b/server/src/contexts/sales/infrastructure/express/middlewares/Dealer.middleware.ts new file mode 100644 index 0000000..940bbe8 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/middlewares/Dealer.middleware.ts @@ -0,0 +1,94 @@ +import { AuthUser } from "@/contexts/auth/domain"; +import { IUseCaseError, UseCaseError } from "@/contexts/common/application"; +import { ICommonContext, InfrastructureError } from "@/contexts/common/infrastructure"; +import { generateExpressError } from "@/contexts/common/infrastructure/express"; +import { GetDealerByUserUseCase } from "@/contexts/sales/application"; +import * as express from "express"; +import httpStatus from "http-status"; +import { registerDealerRepository } from "../../Dealer.repository"; + +interface AuthenticatedRequest extends express.Request { + user?: AuthUser; +} + +export const getDealerMiddleware = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction +) => { + const _req = req as AuthenticatedRequest; + const user = _req.user as AuthUser; + const { context } = res.locals as { context: ICommonContext }; + + if (!user || !user.id) { + return handleError( + req, + res, + UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "User not found") + ); + } + + registerDealerRepository(context); + + try { + const dealerOrError = await new GetDealerByUserUseCase({ + adapter: context.adapter, + repositoryManager: context.repositoryManager, + }).execute({ + userId: user.id, + }); + + if (dealerOrError.isFailure) { + return handleError(req, res, dealerOrError.error); + } + + res.locals.context = { + ...context, + dealer: dealerOrError.object, + }; + + next(); + } catch (e: unknown) { + console.error("Error in getDealerMiddleware:", e as Error); + return handleError(req, res, e as UseCaseError); + } +}; + +function handleError(req: express.Request, res: express.Response, error?: IUseCaseError) { + const errorMappings: { + [key: string]: { message: string; status: number; infraErrorCode: string }; + } = { + [UseCaseError.NOT_FOUND_ERROR]: { + message: "Dealer not found", + status: httpStatus.NOT_FOUND, + infraErrorCode: InfrastructureError.RESOURCE_NOT_FOUND_ERROR, + }, + [UseCaseError.INVALID_INPUT_DATA]: { + message: "Dealer data not valid", + status: httpStatus.UNPROCESSABLE_ENTITY, + infraErrorCode: InfrastructureError.INVALID_INPUT_DATA, + }, + [UseCaseError.REPOSITORY_ERROR]: { + message: "Unexpected error", + status: httpStatus.CONFLICT, + infraErrorCode: InfrastructureError.UNEXCEPTED_ERROR, + }, + [UseCaseError.UNEXCEPTED_ERROR]: { + message: error?.message ?? "Unexcepted error", + status: httpStatus.INTERNAL_SERVER_ERROR, + infraErrorCode: InfrastructureError.UNEXCEPTED_ERROR, + }, + }; + + const { message, status, infraErrorCode } = error + ? errorMappings[error.code] + : { + message: "Bad request", + status: httpStatus.BAD_REQUEST, + infraErrorCode: InfrastructureError.BAD_REQUEST, + }; + + const infraError = InfrastructureError.create(infraErrorCode, message, error); + + return generateExpressError(req, res, status, message, infraError); +} diff --git a/server/src/contexts/sales/infrastructure/express/middlewares/dealerMiddleware.ts b/server/src/contexts/sales/infrastructure/express/middlewares/dealerMiddleware.ts deleted file mode 100644 index 1913171..0000000 --- a/server/src/contexts/sales/infrastructure/express/middlewares/dealerMiddleware.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { AuthUser } from "@/contexts/auth/domain"; -import { GetDealerByUserUseCase } from "@/contexts/sales/application"; -import * as express from "express"; -import { registerDealerRepository } from "../../Dealer.repository"; -import { ISalesContext } from "../../Sales.context"; - -interface AuthenticatedRequest extends express.Request { - user?: AuthUser; -} - -export const getDealerMiddleware = async ( - req: express.Request, - res: express.Response, - next: express.NextFunction -) => { - const _req = req as AuthenticatedRequest; - const user = _req.user; - const context: ISalesContext = res.locals.context; - - registerDealerRepository(context); - - try { - const dealerOrError = await new GetDealerByUserUseCase(context).execute({ - userId: user.id, - }); - - if (dealerOrError.isFailure) { - return res.status(500).json().send(); - //return this._handleExecuteError(result.error); - } - - context.dealer = dealerOrError.object; - - return next(); - } catch (e: unknown) { - //return this.fail(e as IServerError); - return res.status(500).json().send(); - } -}; diff --git a/server/src/contexts/support/application/SendIncidence.useCase.ts b/server/src/contexts/support/application/SendIncidence.useCase.ts new file mode 100644 index 0000000..1edb34e --- /dev/null +++ b/server/src/contexts/support/application/SendIncidence.useCase.ts @@ -0,0 +1,119 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, +} from "@/contexts/common/application"; +import { + DomainError, + IDomainError, + ISendIncidence_Request_DTO, + Result, + TextValueObject, + UTCDateValue, + UniqueID, +} from "@shared/contexts"; + +import { Dealer } from "@/contexts/sales/domain"; +import { Incidence } from "../domain"; +import { ISupportContext } from "../infrastructure"; + +export interface ISendIncidenceUseCaseRequest extends IUseCaseRequest { + incidenceDTO: ISendIncidence_Request_DTO; +} + +export type SendIncidenceResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class SendIncidenceUseCase + implements IUseCase> +{ + private _context: ISupportContext; + + constructor(context: ISupportContext) { + this._context = context; + } + + async execute(request: ISendIncidenceUseCaseRequest): Promise { + const { incidenceDTO } = request; + //const QuoteRepository = this._getQuoteRepository(); + + // Validaciones de datos + if (!this._context.user) { + const message = "Error. Missing User"; + return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message)); + } + + // Validaciones de datos + if (!this._context.dealer) { + const message = "Error. Missing Dealer"; + return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message)); + } + + const dealer = this._context.dealer; + const user = this._context.user; + + // Send incidence + const incidenceOrError = this._tryIncidenceInstance(incidenceDTO, dealer); + + if (incidenceOrError.isFailure) { + const { error: domainError } = incidenceOrError; + let errorCode = ""; + let message = ""; + + switch (domainError.code) { + // Errores manuales + case DomainError.INVALID_INPUT_DATA: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = "The issue has some erroneous data."; + break; + + default: + errorCode = UseCaseError.UNEXCEPTED_ERROR; + message = domainError.message; + break; + } + + return Result.fail(UseCaseError.create(errorCode, message, domainError)); + } + + const incidence = incidenceOrError.object; + + this._context.services?.emailService.sendMail({ + from: { + name: user.name.toString(), + address: user.email.toString(), + }, + to: this._context.defaults.support.from, + subject: this._context.defaults.support.subject, + html: incidence.description.toString(), + }); + + return Result.ok(); + } + + private _tryIncidenceInstance( + incidenceDTO: ISendIncidence_Request_DTO, + dealer: Dealer + ): Result { + const dateOrError = UTCDateValue.createCurrentDate(); + if (dateOrError.isFailure) { + return Result.fail(dateOrError.error); + } + + const descriptionOrError = TextValueObject.create(incidenceDTO.incidence); + if (descriptionOrError.isFailure) { + return Result.fail(descriptionOrError.error); + } + + return Incidence.create( + { + date: dateOrError.object, + description: descriptionOrError.object, + dealerId: dealer.id, + }, + UniqueID.generateNewID().object + ); + } +} diff --git a/server/src/contexts/support/application/index.ts b/server/src/contexts/support/application/index.ts new file mode 100644 index 0000000..058e65d --- /dev/null +++ b/server/src/contexts/support/application/index.ts @@ -0,0 +1 @@ +export * from "./SendIncidence.useCase"; diff --git a/server/src/contexts/support/domain/Incidence.ts b/server/src/contexts/support/domain/Incidence.ts new file mode 100644 index 0000000..e9defde --- /dev/null +++ b/server/src/contexts/support/domain/Incidence.ts @@ -0,0 +1,45 @@ +import { + AggregateRoot, + IDomainError, + Note, + Result, + UTCDateValue, + UniqueID, +} from "@shared/contexts"; + +export interface IIncidenceProps { + dealerId: UniqueID; + date: UTCDateValue; + description: Note; +} + +export interface IIncidence { + id: UniqueID; + + dealerId: UniqueID; + date: UTCDateValue; + description: Note; +} + +export class Incidence extends AggregateRoot implements IIncidence { + public static create(props: IIncidenceProps, id?: UniqueID): Result { + const incidence = new Incidence(props, id); + return Result.ok(incidence); + } + + get id(): UniqueID { + return this._id; + } + + get dealerId() { + return this.props.dealerId; + } + + get date() { + return this.props.date; + } + + get description() { + return this.props.description; + } +} diff --git a/server/src/contexts/support/domain/index.ts b/server/src/contexts/support/domain/index.ts new file mode 100644 index 0000000..ba94662 --- /dev/null +++ b/server/src/contexts/support/domain/index.ts @@ -0,0 +1 @@ +export * from "./Incidence"; diff --git a/server/src/contexts/support/infrastructure/Support.context.ts b/server/src/contexts/support/infrastructure/Support.context.ts new file mode 100644 index 0000000..2f7dfa3 --- /dev/null +++ b/server/src/contexts/support/infrastructure/Support.context.ts @@ -0,0 +1,10 @@ +import { IEmailService } from "@/contexts/common/application"; +import { ICommonContext } from "@/contexts/common/infrastructure"; +import { Dealer } from "@/contexts/sales/domain"; + +export interface ISupportContext extends ICommonContext { + services?: { + emailService: IEmailService; + }; + dealer?: Dealer; +} diff --git a/server/src/contexts/support/infrastructure/express/controllers/index.ts b/server/src/contexts/support/infrastructure/express/controllers/index.ts new file mode 100644 index 0000000..d9f412b --- /dev/null +++ b/server/src/contexts/support/infrastructure/express/controllers/index.ts @@ -0,0 +1 @@ +export * from "./sendIncidence"; diff --git a/server/src/contexts/support/infrastructure/express/controllers/sendIncidence/SendIncidence.controller.ts b/server/src/contexts/support/infrastructure/express/controllers/sendIncidence/SendIncidence.controller.ts new file mode 100644 index 0000000..33855ea --- /dev/null +++ b/server/src/contexts/support/infrastructure/express/controllers/sendIncidence/SendIncidence.controller.ts @@ -0,0 +1,76 @@ +import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { SendIncidenceUseCase } from "@/contexts/support/application/SendIncidence.useCase"; +import { + ensureSendIncidence_Request_DTOIsValid, + ISendIncidence_Request_DTO, +} from "@shared/contexts"; +import { ISupportContext } from "../../../Support.context"; + +export class SendIncidenceController extends ExpressController { + private useCase: SendIncidenceUseCase; + private context: ISupportContext; + + constructor(props: { useCase: SendIncidenceUseCase }, context: ISupportContext) { + super(); + + const { useCase } = props; + this.useCase = useCase; + this.context = context; + } + + async executeImpl(): Promise { + try { + const incidenceDTO: ISendIncidence_Request_DTO = this.req.body; + + // Validar DTO de datos + const incidenceDTOOrError = ensureSendIncidence_Request_DTOIsValid(incidenceDTO); + + if (incidenceDTOOrError.isFailure) { + const errorMessage = "Incidence data not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + incidenceDTOOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + // Llamar al caso de uso + const result = await this.useCase.execute({ + incidenceDTO: incidenceDTOOrError.object, + }); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + return this.noContent(); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } + + private _handleExecuteError(error: IUseCaseError) { + let errorMessage: string; + let infraError: IInfrastructureError; + + switch (error.code) { + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.internalServerError(errorMessage, infraError); + break; + + default: + errorMessage = error.message; + return this.clientError(errorMessage); + } + } +} diff --git a/server/src/contexts/support/infrastructure/express/controllers/sendIncidence/index.ts b/server/src/contexts/support/infrastructure/express/controllers/sendIncidence/index.ts new file mode 100644 index 0000000..c097714 --- /dev/null +++ b/server/src/contexts/support/infrastructure/express/controllers/sendIncidence/index.ts @@ -0,0 +1,16 @@ +import { SendIncidenceUseCase } from "@/contexts/support/application/SendIncidence.useCase"; +import { ISupportContext } from "../../../Support.context"; +import { SendIncidenceController } from "./SendIncidence.controller"; + +export const createSendIncidenceController = (context: ISupportContext) => { + if (!context) { + throw new Error("Support context is required"); + } + + return new SendIncidenceController( + { + useCase: new SendIncidenceUseCase(context), + }, + context + ); +}; diff --git a/server/src/contexts/support/infrastructure/express/index.ts b/server/src/contexts/support/infrastructure/express/index.ts new file mode 100644 index 0000000..6b67c80 --- /dev/null +++ b/server/src/contexts/support/infrastructure/express/index.ts @@ -0,0 +1 @@ +export * from "./controllers"; diff --git a/server/src/contexts/support/infrastructure/index.ts b/server/src/contexts/support/infrastructure/index.ts new file mode 100644 index 0000000..3980072 --- /dev/null +++ b/server/src/contexts/support/infrastructure/index.ts @@ -0,0 +1,2 @@ +export * from "./express"; +export * from "./Support.context"; diff --git a/server/src/contexts/users/infrastructure/User.context.ts b/server/src/contexts/users/infrastructure/User.context.ts index 76920ba..5511508 100644 --- a/server/src/contexts/users/infrastructure/User.context.ts +++ b/server/src/contexts/users/infrastructure/User.context.ts @@ -1,35 +1,3 @@ -import { - IRepositoryManager, - RepositoryManager, -} from "@/contexts/common/domain"; -import { - ISequelizeAdapter, - createSequelizeAdapter, -} from "@/contexts/common/infrastructure/sequelize"; +import { ICommonContext } from "@/contexts/common/infrastructure"; -export interface IUserContext { - adapter: ISequelizeAdapter; - repositoryManager: IRepositoryManager; - //services: IApplicationService; -} - -export class UserContext { - private static instance: UserContext | null = null; - - public static getInstance(): IUserContext { - if (!UserContext.instance) { - UserContext.instance = new UserContext({ - adapter: createSequelizeAdapter(), - repositoryManager: RepositoryManager.getInstance(), - }); - } - - return UserContext.instance.context; - } - - private context: IUserContext; - - private constructor(context: IUserContext) { - this.context = context; - } -} +export interface IUserContext extends ICommonContext {} diff --git a/server/src/infrastructure/express/api/CommonContext.middleware.ts b/server/src/infrastructure/express/api/CommonContext.middleware.ts new file mode 100644 index 0000000..4a85d7d --- /dev/null +++ b/server/src/infrastructure/express/api/CommonContext.middleware.ts @@ -0,0 +1,8 @@ +import { createCommonContext } from "@/contexts/common/infrastructure"; +import { NextFunction, Request, Response } from "express"; + +export const commonContextMiddleware = (req: Request, res: Response, next: NextFunction) => { + // Almacenar el contexto en res.locals + res.locals.context = createCommonContext(); + next(); +}; diff --git a/server/src/infrastructure/express/api/upload.middleware.ts b/server/src/infrastructure/express/api/Upload.middleware.ts similarity index 100% rename from server/src/infrastructure/express/api/upload.middleware.ts rename to server/src/infrastructure/express/api/Upload.middleware.ts diff --git a/server/src/infrastructure/express/api/context.middleware.ts b/server/src/infrastructure/express/api/context.middleware.ts deleted file mode 100644 index 9573ba0..0000000 --- a/server/src/infrastructure/express/api/context.middleware.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ContextFactory } from "@/contexts/common/infrastructure"; - -export const createContextMiddleware = () => ContextFactory.getInstance(); diff --git a/server/src/infrastructure/express/api/routes/catalog.routes.ts b/server/src/infrastructure/express/api/routes/catalog.routes.ts index 59084f9..979c52e 100644 --- a/server/src/infrastructure/express/api/routes/catalog.routes.ts +++ b/server/src/infrastructure/express/api/routes/catalog.routes.ts @@ -1,6 +1,6 @@ import { checkUser } from "@/contexts/auth"; +import { listArticlesController } from "@/contexts/catalog"; import { NextFunction, Request, Response, Router } from "express"; -import { listArticlesController } from "../../../../contexts/catalog/infrastructure/express/controllers"; export const catalogRouter = (appRouter: Router) => { const catalogRoutes: Router = Router({ mergeParams: true }); diff --git a/server/src/infrastructure/express/api/routes/dealers.routes.ts b/server/src/infrastructure/express/api/routes/dealers.routes.ts index 0020c70..1f6e120 100644 --- a/server/src/infrastructure/express/api/routes/dealers.routes.ts +++ b/server/src/infrastructure/express/api/routes/dealers.routes.ts @@ -1,15 +1,15 @@ -import { checkUser, checkisAdmin } from "@/contexts/auth"; +import { checkIsAdmin, checkUser } from "@/contexts/auth"; import { getDealerController, listDealersController, } from "@/contexts/sales/infrastructure/express/controllers/dealers"; -import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware"; +import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/Dealer.middleware"; import { Router } from "express"; -export const DealerRouter = (appRouter: Router) => { +export const dealerRouter = (appRouter: Router) => { const dealerRoutes: Router = Router({ mergeParams: true }); - dealerRoutes.get("/", checkisAdmin, listDealersController); + dealerRoutes.get("/", checkIsAdmin, listDealersController); dealerRoutes.get("/:dealerId", checkUser, getDealerMiddleware, getDealerController); ///dealerRoutes.post("/", checkisAdmin, createDealerController); //dealerRoutes.put("/:dealerId", checkisAdmin, updateDealerController); diff --git a/server/src/infrastructure/express/api/routes/profile.routes.ts b/server/src/infrastructure/express/api/routes/profile.routes.ts index 9fd94f1..bfc81cc 100644 --- a/server/src/infrastructure/express/api/routes/profile.routes.ts +++ b/server/src/infrastructure/express/api/routes/profile.routes.ts @@ -3,12 +3,11 @@ import { handleRequest } from "@/contexts/common/infrastructure/express"; import { createGetProfileController, createUpdateProfileController, - ProfileContext, } from "@/contexts/profile/infrastructure"; import { createGetProfileLogoController } from "@/contexts/profile/infrastructure/express/controllers/getProfileLogo"; import { createUploadProfileLogoController } from "@/contexts/profile/infrastructure/express/controllers/uploadProfileLogo"; -import { NextFunction, Request, Response, Router } from "express"; -import { createMulterMiddleware } from "../upload.middleware"; +import { Router } from "express"; +import { createMulterMiddleware } from "../Upload.middleware"; const uploadProfileLogo = createMulterMiddleware({ uploadFolder: "uploads/dealer-logos", @@ -19,13 +18,6 @@ const uploadProfileLogo = createMulterMiddleware({ export const profileRouter = (appRouter: Router) => { const profileRoutes: Router = Router({ mergeParams: true }); - const profileContextMiddleware = (req: Request, res: Response, next: NextFunction) => { - res.locals["context"] = ProfileContext.getInstance(); - next(); - }; - - profileRoutes.use(profileContextMiddleware); - profileRoutes.get("/", checkUser, handleRequest(createGetProfileController)); profileRoutes.put("/", checkUser, handleRequest(createUpdateProfileController)); profileRoutes.get("/logo", checkUser, handleRequest(createGetProfileLogoController)); diff --git a/server/src/infrastructure/express/api/routes/quote.routes.ts b/server/src/infrastructure/express/api/routes/quote.routes.ts index 8e2d868..1f84cec 100644 --- a/server/src/infrastructure/express/api/routes/quote.routes.ts +++ b/server/src/infrastructure/express/api/routes/quote.routes.ts @@ -7,10 +7,10 @@ import { setStatusQuoteController, updateQuoteController, } from "@/contexts/sales/infrastructure/express/controllers"; -import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware"; +import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/Dealer.middleware"; import { Router } from "express"; -export const QuoteRouter = (appRouter: Router) => { +export const quoteRouter = (appRouter: Router): void => { const quoteRoutes: Router = Router({ mergeParams: true }); // Users CRUD diff --git a/server/src/infrastructure/express/api/routes/support.routes.ts b/server/src/infrastructure/express/api/routes/support.routes.ts new file mode 100644 index 0000000..f03c62f --- /dev/null +++ b/server/src/infrastructure/express/api/routes/support.routes.ts @@ -0,0 +1,32 @@ +import { NextFunction, Request, Response, Router } from "express"; + +import { config } from "@/config"; +import { checkUser } from "@/contexts/auth"; +import { handleRequest } from "@/contexts/common/infrastructure/express"; +import { BrevoMailService, LoggerMailService } from "@/contexts/common/infrastructure/nodemailer"; +import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/Dealer.middleware"; +import { createSendIncidenceController, ISupportContext } from "@/contexts/support/infrastructure"; + +export const supportRouter = (appRouter: Router): void => { + const supportRoutes: Router = Router({ mergeParams: true }); + + supportRoutes.use((req: Request, res: Response, next: NextFunction) => { + const context = res.locals["context"]; + res.locals["context"] = { + ...context, + services: { + emailService: config.isProduction ? new BrevoMailService() : new LoggerMailService(), + }, + } as ISupportContext; + next(); + }); + + supportRoutes.post( + "/", + checkUser, + getDealerMiddleware, + handleRequest(createSendIncidenceController) + ); + + appRouter.use("/support", supportRoutes); +}; diff --git a/server/src/infrastructure/express/api/routes/users.routes.ts b/server/src/infrastructure/express/api/routes/users.routes.ts index e9dc776..3be8aad 100644 --- a/server/src/infrastructure/express/api/routes/users.routes.ts +++ b/server/src/infrastructure/express/api/routes/users.routes.ts @@ -1,4 +1,4 @@ -import { checkAdminOrSelf, checkisAdmin } from "@/contexts/auth"; +import { checkAdminOrSelf, checkIsAdmin } from "@/contexts/auth"; import { NextFunction, Request, Response, Router } from "express"; import { createCreateUserController, @@ -11,7 +11,7 @@ import { export const usersRouter = (appRouter: Router) => { const userRoutes: Router = Router({ mergeParams: true }); - userRoutes.get("/", checkisAdmin, (req: Request, res: Response, next: NextFunction) => + userRoutes.get("/", checkIsAdmin, (req: Request, res: Response, next: NextFunction) => createListUsersController(res.locals["context"]).execute(req, res, next) ); @@ -19,15 +19,15 @@ export const usersRouter = (appRouter: Router) => { createGetUserController(res.locals["context"]).execute(req, res, next) ); - userRoutes.post("/", checkisAdmin, (req: Request, res: Response, next: NextFunction) => + userRoutes.post("/", checkIsAdmin, (req: Request, res: Response, next: NextFunction) => createCreateUserController(res.locals["context"]).execute(req, res, next) ); - userRoutes.put("/:userId", checkisAdmin, (req: Request, res: Response, next: NextFunction) => + userRoutes.put("/:userId", checkIsAdmin, (req: Request, res: Response, next: NextFunction) => createUpdateUserController(res.locals["context"]).execute(req, res, next) ); - userRoutes.delete("/:userId", checkisAdmin, (req: Request, res: Response, next: NextFunction) => + userRoutes.delete("/:userId", checkIsAdmin, (req: Request, res: Response, next: NextFunction) => createDeleteUserController(res.locals["context"]).execute(req, res, next) ); diff --git a/server/src/infrastructure/express/api/v1.ts b/server/src/infrastructure/express/api/v1.ts index 5e5b5fc..2090e08 100644 --- a/server/src/infrastructure/express/api/v1.ts +++ b/server/src/infrastructure/express/api/v1.ts @@ -1,13 +1,14 @@ -import { NextFunction, Request, Response, Router } from "express"; -import { createContextMiddleware } from "./context.middleware"; +import { Router } from "express"; +import { commonContextMiddleware } from "./CommonContext.middleware"; import { - DealerRouter, - QuoteRouter, authRouter, catalogRouter, + dealerRouter, profileRouter, + quoteRouter, usersRouter, } from "./routes"; +import { supportRouter } from "./routes/support.routes"; export const v1Routes = () => { const routes = Router({ mergeParams: true }); @@ -16,24 +17,22 @@ export const v1Routes = () => { res.send("Hello world!"); }); - routes.use((req: Request, res: Response, next: NextFunction) => { - res.locals["context"] = createContextMiddleware(); - //res.locals["middlewares"] = createMiddlewareMap(); - - return next(); - }); - routes.use((req, res, next) => { - console.log(`[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}`); + console.log( + `[${new Date().toLocaleTimeString()}] Incoming request ${req.method} to ${req.path}` + ); next(); }); + routes.use(commonContextMiddleware); + authRouter(routes); profileRouter(routes); usersRouter(routes); catalogRouter(routes); - DealerRouter(routes); - QuoteRouter(routes); + dealerRouter(routes); + quoteRouter(routes); + supportRouter(routes); return routes; }; diff --git a/server/src/infrastructure/express/app.ts b/server/src/infrastructure/express/app.ts index ef01d99..133c376 100644 --- a/server/src/infrastructure/express/app.ts +++ b/server/src/infrastructure/express/app.ts @@ -63,10 +63,6 @@ app.use(helmet()); //app.use(morgan('common')); app.use(morgan("dev")); -// Autentication -app.use(passport.initialize()); -configurePassportAuth(passport); - // Express configuration app.set("port", process.env.PORT ?? 3001); @@ -85,6 +81,10 @@ app.use((err: any, req: any, res: any, next: any) => { next(); }); +// Autentication +app.use(passport.initialize()); +configurePassportAuth(passport); + // API app.use("/api/v1", v1Routes()); diff --git a/server/src/infrastructure/logger/index.ts b/server/src/infrastructure/logger/index.ts index 2c499a9..92811ec 100644 --- a/server/src/infrastructure/logger/index.ts +++ b/server/src/infrastructure/logger/index.ts @@ -5,7 +5,7 @@ import { createLogger, format, transports } from "winston"; import DailyRotateFile from "winston-daily-rotate-file"; import { config } from "../../config"; -function initLogger(rTracer) { +export const initLogger = (rTracer) => { // a custom format that outputs request id const consoleFormat = format.combine( @@ -83,9 +83,7 @@ function initLogger(rTracer) { //} return logger; -} - -export { initLogger }; +}; export const logger = () => { return initLogger(rTracer); diff --git a/server/src/infrastructure/sequelize/initData.ts b/server/src/infrastructure/sequelize/initData.ts index a98c63d..b95d34c 100644 --- a/server/src/infrastructure/sequelize/initData.ts +++ b/server/src/infrastructure/sequelize/initData.ts @@ -1,4 +1,7 @@ -import { SalesContext } from "@/contexts/sales/infrastructure"; +import { config } from "@/config"; +import { RepositoryManager } from "@/contexts/common/domain"; +import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; + import { registerDealerRepository } from "@/contexts/sales/infrastructure/Dealer.repository"; import { initializeAdmin, @@ -8,7 +11,11 @@ import { import { registerUserRepository } from "@/contexts/users/infrastructure/User.repository"; export const insertUsers = async () => { - const context = SalesContext.getInstance(); + const context = { + defaults: config.defaults, + adapter: createSequelizeAdapter(), + repositoryManager: RepositoryManager.getInstance(), + } as any; registerUserRepository(context); registerDealerRepository(context); diff --git a/server/yarn.lock b/server/yarn.lock index 3b0ce6d..87e7ad7 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1038,6 +1038,13 @@ dependencies: undici-types "~6.19.2" +"@types/nodemailer@^6.4.16": + version "6.4.16" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.16.tgz#db006abcb1e1c8e6ea2fb53b27fefec3c03eaa6c" + integrity sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ== + dependencies: + "@types/node" "*" + "@types/passport-jwt@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-4.0.1.tgz#080fbe934fb9f6954fb88ec4cdf4bb2cc7c4d435" @@ -4598,6 +4605,11 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +nodemailer@^6.9.15: + version "6.9.15" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.15.tgz#57b79dc522be27e0e47ac16cc860aa0673e62e04" + integrity sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ== + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" diff --git a/shared/lib/contexts/index.ts b/shared/lib/contexts/index.ts index 65a0afc..4a32f6d 100644 --- a/shared/lib/contexts/index.ts +++ b/shared/lib/contexts/index.ts @@ -3,4 +3,5 @@ export * from "./catalog"; export * from "./common"; export * from "./profile"; export * from "./sales"; +export * from "./support"; export * from "./users"; diff --git a/shared/lib/contexts/support/dto/SendIncidence.dto/ISendIncidence_Request.dto.ts b/shared/lib/contexts/support/dto/SendIncidence.dto/ISendIncidence_Request.dto.ts new file mode 100644 index 0000000..33dcb6c --- /dev/null +++ b/shared/lib/contexts/support/dto/SendIncidence.dto/ISendIncidence_Request.dto.ts @@ -0,0 +1,20 @@ +import Joi from "joi"; +import { Result, RuleValidator } from "../../../common"; + +export interface ISendIncidence_Request_DTO { + incidence: string; +} + +export function ensureSendIncidence_Request_DTOIsValid(SupportDTO: ISendIncidence_Request_DTO) { + const schema = Joi.object({ + incidence: Joi.string().min(10).max(1000).required(), + }); + + const result = RuleValidator.validate(schema, SupportDTO); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok(result.object); +} diff --git a/shared/lib/contexts/support/dto/SendIncidence.dto/index.ts b/shared/lib/contexts/support/dto/SendIncidence.dto/index.ts new file mode 100644 index 0000000..1cb25e2 --- /dev/null +++ b/shared/lib/contexts/support/dto/SendIncidence.dto/index.ts @@ -0,0 +1 @@ +export * from "./ISendIncidence_Request.dto"; diff --git a/shared/lib/contexts/support/dto/index.ts b/shared/lib/contexts/support/dto/index.ts new file mode 100644 index 0000000..cfa0409 --- /dev/null +++ b/shared/lib/contexts/support/dto/index.ts @@ -0,0 +1 @@ +export * from "./SendIncidence.dto"; diff --git a/shared/lib/contexts/support/index.ts b/shared/lib/contexts/support/index.ts new file mode 100644 index 0000000..0392b1b --- /dev/null +++ b/shared/lib/contexts/support/index.ts @@ -0,0 +1 @@ +export * from "./dto";