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 (
+ <>
+
+
+
+
+
+ ¿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";