This commit is contained in:
David Arranz 2024-06-14 14:07:20 +02:00
parent f39dbe95cc
commit 82fdc6de13
64 changed files with 688 additions and 296 deletions

View File

@ -1,5 +1,13 @@
import { Outlet, RouterProvider, createBrowserRouter } from "react-router-dom"; import { Outlet, RouterProvider, createBrowserRouter } from "react-router-dom";
import { DealerLayout, DealersList, LoginPage, LogoutPage, SettingsPage, StartPage } from "./app"; import {
DealerLayout,
DealersList,
LoginPage,
LogoutPage,
SettingsEditor,
SettingsLayout,
StartPage,
} from "./app";
import { CatalogLayout, CatalogList } from "./app/catalog"; import { CatalogLayout, CatalogList } from "./app/catalog";
import { DashboardPage } from "./app/dashboard"; import { DashboardPage } from "./app/dashboard";
import { QuotesList } from "./app/quotes/list"; import { QuotesList } from "./app/quotes/list";
@ -68,9 +76,17 @@ export const Routes = () => {
path: "/settings", path: "/settings",
element: ( element: (
<ProtectedRoute> <ProtectedRoute>
<SettingsPage /> <SettingsLayout>
<Outlet />
</SettingsLayout>
</ProtectedRoute> </ProtectedRoute>
), ),
children: [
{
index: true,
element: <SettingsEditor />,
},
],
}, },
{ {
path: "/logout", path: "/logout",

View File

@ -15,7 +15,6 @@ import { useCatalogList } from "../hooks";
export const CatalogDataTable = () => { export const CatalogDataTable = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { pagination, globalFilter, isFiltered } = useDataTableContext(); const { pagination, globalFilter, isFiltered } = useDataTableContext();
console.log("pagination PADRE => ", pagination);
const { data, isPending, isError, error } = useCatalogList({ const { data, isPending, isError, error } = useCatalogList({
pagination: { pagination: {
@ -27,12 +26,30 @@ export const CatalogDataTable = () => {
const columns = useMemo<ColumnDef<IListArticles_Response_DTO, any>[]>( const columns = useMemo<ColumnDef<IListArticles_Response_DTO, any>[]>(
() => [ () => [
{
id: "id" as const,
accessorKey: "id",
enableResizing: false,
size: 10,
},
{
id: "article_id" as const,
accessorKey: "id_article",
enableResizing: false,
size: 10,
},
{
id: "catalog_name" as const,
accessorKey: "catalog_name",
enableResizing: false,
size: 10,
},
{ {
id: "description" as const, id: "description" as const,
accessorKey: "description", accessorKey: "description",
header: () => <>{t("catalog.list.columns.description")}</>, header: () => <>{t("catalog.list.columns.description")}</>,
enableResizing: false, enableResizing: false,
size: 300, size: 100,
}, },
{ {
id: "points" as const, id: "points" as const,

View File

@ -16,7 +16,6 @@ export const CatalogLayout = ({ children }: PropsWithChildren) => {
</div> </div>
{children} {children}
</LayoutContent> </LayoutContent>
1
</Layout> </Layout>
</CatalogProvider> </CatalogProvider>
); );

View File

@ -0,0 +1,72 @@
import {
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Input,
} from "@/ui";
import { Checkbox } from "@radix-ui/react-checkbox";
import { Link } from "react-router-dom";
export const SettingsEditor = () => {
return (
<div className='mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]'>
<nav className='grid gap-4 text-sm text-muted-foreground' x-chunk='dashboard-04-chunk-0'>
<Link to='#' className='font-semibold text-primary'>
General
</Link>
<Link to='#'>Security</Link>
<Link to='#'>Integrations</Link>
<Link to='#'>Support</Link>
<Link to='#'>Organizations</Link>
<Link to='#'>Advanced</Link>
</nav>
<div className='grid gap-6'>
<Card x-chunk='dashboard-04-chunk-1'>
<CardHeader>
<CardTitle>Store Name</CardTitle>
<CardDescription>Used to identify your store in the marketplace.</CardDescription>
</CardHeader>
<CardContent>
<form>
<Input placeholder='Store Name' />
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
<Card x-chunk='dashboard-04-chunk-2'>
<CardHeader>
<CardTitle>Plugins Directory</CardTitle>
<CardDescription>
The directory within your project, in which your plugins are located.
</CardDescription>
</CardHeader>
<CardContent>
<form className='flex flex-col gap-4'>
<Input placeholder='Project Name' defaultValue='/content/plugins' />
<div className='flex items-center space-x-2'>
<Checkbox id='include' defaultChecked />
<label
htmlFor='include'
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Allow administrators to change the directory.
</label>
</div>
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
</div>
</div>
);
};

View File

@ -1,81 +1,2 @@
import { Layout, LayoutHeader } from "@/components"; export * from "./edit";
import { export * from "./layout";
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Input,
} from "@/ui";
import { Checkbox } from "@radix-ui/react-checkbox";
import { Link } from "react-router-dom";
export const SettingsPage = () => {
return (
<Layout>
<LayoutHeader />
<main className='flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10'>
<div className='grid w-full max-w-6xl gap-2 mx-auto'>
<h1 className='text-3xl font-semibold'>Settings</h1>
</div>
<div className='mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]'>
<nav className='grid gap-4 text-sm text-muted-foreground' x-chunk='dashboard-04-chunk-0'>
<Link to='#' className='font-semibold text-primary'>
General
</Link>
<Link to='#'>Security</Link>
<Link to='#'>Integrations</Link>
<Link to='#'>Support</Link>
<Link to='#'>Organizations</Link>
<Link to='#'>Advanced</Link>
</nav>
<div className='grid gap-6'>
<Card x-chunk='dashboard-04-chunk-1'>
<CardHeader>
<CardTitle>Store Name</CardTitle>
<CardDescription>Used to identify your store in the marketplace.</CardDescription>
</CardHeader>
<CardContent>
<form>
<Input placeholder='Store Name' />
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
<Card x-chunk='dashboard-04-chunk-2'>
<CardHeader>
<CardTitle>Plugins Directory</CardTitle>
<CardDescription>
The directory within your project, in which your plugins are located.
</CardDescription>
</CardHeader>
<CardContent>
<form className='flex flex-col gap-4'>
<Input placeholder='Project Name' defaultValue='/content/plugins' />
<div className='flex items-center space-x-2'>
<Checkbox id='include' defaultChecked />
<label
htmlFor='include'
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Allow administrators to change the directory.
</label>
</div>
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
</div>
</div>
</main>
</Layout>
);
};

View File

@ -0,0 +1,19 @@
import { Layout, LayoutContent, LayoutHeader } from "@/components";
import { PropsWithChildren } from "react";
import { Trans } from "react-i18next";
export const SettingsLayout = ({ children }: PropsWithChildren) => {
return (
<Layout>
<LayoutHeader />
<LayoutContent>
<div className='grid w-full max-w-6xl gap-2 mx-auto'>
<h1 className='text-2xl font-semibold md:text-3xl'>
<Trans i18nKey='settings.title' />
</h1>
</div>
{children}
</LayoutContent>
</Layout>
);
};

View File

@ -1,6 +1,7 @@
import { DEFAULT_PAGE_SIZES, INITIAL_PAGE_INDEX } from "@/lib/hooks"; import { DEFAULT_PAGE_SIZES } from "@/lib/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui"; import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
import { INITIAL_PAGE_INDEX } from "@shared/contexts";
import { Table } from "@tanstack/react-table"; import { Table } from "@tanstack/react-table";
import { t } from "i18next"; import { t } from "i18next";
import { import {

View File

@ -9,7 +9,7 @@ import { UserButton } from "./components/UserButton";
export const LayoutHeader = () => { export const LayoutHeader = () => {
return ( return (
<header className='sticky top-0 flex items-center h-16 gap-8 px-4 border-b bg-background md:px-6'> <header className='sticky top-0 z-10 flex items-center h-16 gap-8 px-4 border-b bg-background md:px-6'>
<nav className='flex-col hidden gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'> <nav className='flex-col hidden gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'>
<Link to='/' className='flex items-center font-semibold'> <Link to='/' className='flex items-center font-semibold'>
<UeckoLogo className='w-24' /> <UeckoLogo className='w-24' />

View File

@ -1,4 +1,4 @@
import { IListResponse_DTO } from "@shared/contexts"; import { IListResponse_DTO, INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@shared/contexts";
import { import {
ICreateOneDataProviderParams, ICreateOneDataProviderParams,
IDataSource, IDataSource,
@ -10,7 +10,6 @@ import {
ISortItemDataProviderParam, ISortItemDataProviderParam,
IUpdateOneDataProviderParams, IUpdateOneDataProviderParams,
} from "../hooks/useDataSource/DataSource"; } from "../hooks/useDataSource/DataSource";
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "../hooks/usePagination";
import { createAxiosInstance } from "./axiosInstance"; import { createAxiosInstance } from "./axiosInstance";
export const createAxiosDataProvider = ( export const createAxiosDataProvider = (

View File

@ -125,7 +125,7 @@ export function useDataTable<TData, TValue>({
enableHiding = false, enableHiding = false,
enableRowSelection = false, enableRowSelection = false,
}: UseDataTableProps<TData, TValue>) { }: UseDataTableProps<TData, TValue>) {
const { pagination, setPagination, sorting, setSorting } = useDataTableContext(); const { pagination, setPagination, sorting } = useDataTableContext();
// Table states // Table states
const [rowSelection, setRowSelection] = React.useState({}); const [rowSelection, setRowSelection] = React.useState({});
@ -142,7 +142,7 @@ export function useDataTable<TData, TValue>({
if (typeof updater === "function") { if (typeof updater === "function") {
const newSorting = updater(sorting); const newSorting = updater(sorting);
console.log(newSorting); console.log(newSorting);
setSorting(newSorting); //setSorting(newSorting);
} }
}; };

View File

@ -1,12 +1,11 @@
import { act, renderHook } from "@testing-library/react-hooks";
import { import {
INITIAL_PAGE_INDEX, INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE, INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE, MAX_PAGE_SIZE,
MIN_PAGE_SIZE, MIN_PAGE_SIZE,
PaginationState, } from "@shared/contexts";
usePagination, import { act, renderHook } from "@testing-library/react-hooks";
} from "./usePagination"; import { PaginationState, usePagination } from "./usePagination";
describe("usePagination", () => { describe("usePagination", () => {
it("should initialize with default values", () => { it("should initialize with default values", () => {

View File

@ -1,14 +1,13 @@
import {
INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE,
MIN_PAGE_SIZE,
} from "@shared/contexts";
import { useState } from "react"; import { useState } from "react";
export const INITIAL_PAGE_INDEX = 0; export const DEFAULT_PAGE_SIZES = [15, 30, 50, 75, 100];
export const INITIAL_PAGE_SIZE = 10;
export const MIN_PAGE_INDEX = 0;
export const MIN_PAGE_SIZE = 1;
export const MAX_PAGE_SIZE = 100; //Number.MAX_SAFE_INTEGER;
export const DEFAULT_PAGE_SIZES = [10, 25, 50, 100];
export interface PaginationState { export interface PaginationState {
pageIndex: number; pageIndex: number;

View File

@ -1,13 +1,13 @@
import { PaginationState } from "@tanstack/react-table";
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { import {
INITIAL_PAGE_INDEX, INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE, INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE, MAX_PAGE_SIZE,
MIN_PAGE_SIZE, MIN_PAGE_SIZE,
usePagination, } from "@shared/contexts";
} from "./usePagination"; import { PaginationState } from "@tanstack/react-table";
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { usePagination } from "./usePagination";
export const usePaginationParams = ( export const usePaginationParams = (
initialPageIndex: number = INITIAL_PAGE_INDEX, initialPageIndex: number = INITIAL_PAGE_INDEX,

View File

@ -62,6 +62,9 @@
"retail_price": "PVP" "retail_price": "PVP"
} }
} }
},
"settings": {
"title": "Ajustes"
} }
} }
} }

View File

@ -28,6 +28,9 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["src"], "include": [
"src",
"../shared/lib/contexts/common/domain/entities/QueryCriteria/Pagination/defaults.ts"
],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -43,6 +43,13 @@ module.exports = {
language: "es", language: "es",
}, },
sample_dealer: {
name: "Roberto",
email: "rblanco@rodax-software.com",
password: "123456",
language: "en",
},
uploads: { uploads: {
imports: process.env.UPLOAD_PATH || "/home/rodax/Documentos/BBDD/server/uploads/imports", imports: process.env.UPLOAD_PATH || "/home/rodax/Documentos/BBDD/server/uploads/imports",
documents: process.env.UPLOAD_PATH || "/home/rodax/Documentos/BBDD/server/uploads/documents", documents: process.env.UPLOAD_PATH || "/home/rodax/Documentos/BBDD/server/uploads/documents",

View File

@ -1,3 +1,2 @@
export * from "./controllers"; export * from "./controllers";
export * from "./passport"; export * from "./passport";
export * from "./routes";

View File

@ -1,21 +1,16 @@
import { AuthUser } from "@/contexts/auth/domain"; import { AuthUser } from "@/contexts/auth/domain";
import { composeMiddleware, generateExpressError } from "@/contexts/common/infrastructure/express"; import { composeMiddleware, generateExpressError } from "@/contexts/common/infrastructure/express";
import { UniqueID } from "@shared/contexts"; import { ensureIdIsValid } from "@shared/contexts";
import Express from "express"; import Express from "express";
import httpStatus from "http-status"; import httpStatus from "http-status";
import passport from "passport"; import passport from "passport";
export const isUser = composeMiddleware([ export const checkUser = composeMiddleware([
passport.authenticate("local-jwt", { passport.authenticate("local-jwt", {
session: false, session: false,
}), }),
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => { (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
console.log(<AuthUser>req.user);
/*const user = <AuthUser>req.user;
if (!user.isUser) {
return generateExpressError(req, res, httpStatus.UNAUTHORIZED);
}*/
return next(); return next();
} }
@ -23,25 +18,34 @@ export const isUser = composeMiddleware([
}, },
]); ]);
export const isAdmin = composeMiddleware([ export const checkisAdmin = composeMiddleware([
isUser, checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => { (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
const user = <AuthUser>req.user; const user = <AuthUser>req.user;
if (!user.isAdmin) { if (!user.isAdmin) {
generateExpressError(req, res, httpStatus.UNAUTHORIZED); generateExpressError(req, res, httpStatus.UNAUTHORIZED);
} }
next(); return next();
}, },
]); ]);
export const isAdminOrMe = composeMiddleware([ export const checkAdminOrSelf = composeMiddleware([
isUser, checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => { (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
const user = <AuthUser>req.user; const user = <AuthUser>req.user;
const { userId } = req.params; const { userId } = req.params;
if (user.isAdmin || user.id.equals(UniqueID.create(userId).object)) { if (user.isAdmin) {
next(); return next();
} else generateExpressError(req, res, httpStatus.UNAUTHORIZED); }
if (userId) {
const paramIdOrError = ensureIdIsValid(userId);
if (paramIdOrError.isSuccess && user.id.equals(paramIdOrError.object)) {
return next();
}
}
return generateExpressError(req, res, httpStatus.UNAUTHORIZED);
}, },
]); ]);

View File

@ -14,8 +14,6 @@ export interface IArticleProps {
catalog_name: Slug; catalog_name: Slug;
id_article: ArticleIdentifier; id_article: ArticleIdentifier;
reference: Slug; reference: Slug;
//family: Description;
//subfamily: Description;
description: Description; description: Description;
points: Quantity; points: Quantity;
retail_price: UnitPrice; retail_price: UnitPrice;
@ -26,8 +24,6 @@ export interface IArticle {
catalog_name: Slug; catalog_name: Slug;
id_article: ArticleIdentifier; id_article: ArticleIdentifier;
reference: Slug; reference: Slug;
//family: Description;
//subfamily: Description;
description: Description; description: Description;
points: Quantity; points: Quantity;
retail_price: UnitPrice; retail_price: UnitPrice;
@ -67,14 +63,6 @@ export class Article extends AggregateRoot<IArticleProps> implements IArticle {
return this.props.reference; return this.props.reference;
} }
/*get family(): Description {
return this.props.family;
}
get subfamily(): Description {
return this.props.subfamily;
}*/
get description(): Description { get description(): Description {
return this.props.description; return this.props.description;
} }

View File

@ -1,9 +1,6 @@
import Joi from "joi"; import Joi from "joi";
import { import { ListArticlesResult, ListArticlesUseCase } from "@/contexts/catalog/application";
ListArticlesResult,
ListArticlesUseCase,
} from "@/contexts/catalog/application";
import { Article } from "@/contexts/catalog/domain"; import { Article } from "@/contexts/catalog/domain";
import { QueryCriteriaService } from "@/contexts/common/application/services"; import { QueryCriteriaService } from "@/contexts/common/application/services";
import { IServerError } from "@/contexts/common/domain/errors"; import { IServerError } from "@/contexts/common/domain/errors";
@ -29,7 +26,7 @@ export class ListArticlesController extends ExpressController {
useCase: ListArticlesUseCase; useCase: ListArticlesUseCase;
presenter: IListArticlesPresenter; presenter: IListArticlesPresenter;
}, },
context: ICatalogContext, context: ICatalogContext
) { ) {
super(); super();
@ -60,10 +57,7 @@ export class ListArticlesController extends ExpressController {
const queryParams = queryOrError.object; const queryParams = queryOrError.object;
try { try {
const queryCriteria: IQueryCriteria = const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
QueryCriteriaService.parse(queryParams);
console.log(queryCriteria);
const result: ListArticlesResult = await this.useCase.execute({ const result: ListArticlesResult = await this.useCase.execute({
queryCriteria, queryCriteria,
@ -79,7 +73,7 @@ export class ListArticlesController extends ExpressController {
this.presenter.mapArray(customers, this.context, { this.presenter.mapArray(customers, this.context, {
page: queryCriteria.pagination.offset, page: queryCriteria.pagination.offset,
limit: queryCriteria.pagination.limit, limit: queryCriteria.pagination.limit,
}), })
); );
} catch (e: unknown) { } catch (e: unknown) {
return this.fail(e as IServerError); return this.fail(e as IServerError);

View File

@ -23,8 +23,6 @@ export const listArticlesPresenter: IListArticlesPresenter = {
id_article: article.id_article.toString(), id_article: article.id_article.toString(),
reference: article.reference.toString(), reference: article.reference.toString(),
description: article.description.toString(), description: article.description.toString(),
//family: article.family.toString(),
//subfamily: article.subfamily.toString(),
points: article.points.toNumber(), points: article.points.toNumber(),
retail_price: article.retail_price.toObject(), retail_price: article.retail_price.toObject(),
}; };

View File

@ -1,2 +1 @@
export * from "./controllers"; export * from "./controllers";
export * from "./routes";

View File

@ -20,8 +20,6 @@ class ArticleMapper
catalog_name: this.mapsValue(source, "catalog_name", Slug.create), catalog_name: this.mapsValue(source, "catalog_name", Slug.create),
id_article: this.mapsValue(source, "id_article", ArticleIdentifier.create), id_article: this.mapsValue(source, "id_article", ArticleIdentifier.create),
reference: this.mapsValue(source, "reference", Slug.create), reference: this.mapsValue(source, "reference", Slug.create),
//family: this.mapsValue(source, "family", Description.create),
//subfamily: this.mapsValue(source, "subfamily", Description.create),
description: this.mapsValue(source, "description", Description.create), description: this.mapsValue(source, "description", Description.create),
points: this.mapsValue(source, "points", Quantity.create), points: this.mapsValue(source, "points", Quantity.create),
retail_price: this.mapsValue(source, "retail_price", (value: any) => retail_price: this.mapsValue(source, "retail_price", (value: any) =>

View File

@ -26,8 +26,6 @@ export class Article_Model extends Model<
declare catalog_name: string; declare catalog_name: string;
declare id_article: string; // number ?? declare id_article: string; // number ??
declare reference: CreationOptional<string>; declare reference: CreationOptional<string>;
//declare family: CreationOptional<string>;
//declare subfamily: CreationOptional<string>;
declare description: CreationOptional<string>; declare description: CreationOptional<string>;
declare points: CreationOptional<number>; declare points: CreationOptional<number>;
declare retail_price: CreationOptional<number>; declare retail_price: CreationOptional<number>;
@ -50,8 +48,6 @@ export default (sequelize: Sequelize) => {
allowNull: false, allowNull: false,
}, },
reference: DataTypes.STRING(), reference: DataTypes.STRING(),
//family: DataTypes.STRING(),
//subfamily: DataTypes.STRING(),
description: DataTypes.STRING(), description: DataTypes.STRING(),
points: { points: {
type: DataTypes.SMALLINT().UNSIGNED, type: DataTypes.SMALLINT().UNSIGNED,
@ -74,8 +70,6 @@ export default (sequelize: Sequelize) => {
indexes: [ indexes: [
{ name: "catalog_name_idx", fields: ["catalog_name"] }, { name: "catalog_name_idx", fields: ["catalog_name"] },
{ name: "id_article_idx", fields: ["id_article"] }, { name: "id_article_idx", fields: ["id_article"] },
//{ name: "family_idx", fields: ["family"] },
//{ name: "family_subfamily_idx", fields: ["family", "subfamily"] },
{ name: "updated_at_idx", fields: ["updated_at"] }, { name: "updated_at_idx", fields: ["updated_at"] },
], ],
@ -88,12 +82,6 @@ export default (sequelize: Sequelize) => {
reference: { reference: {
[Op.like]: `%${value}%`, [Op.like]: `%${value}%`,
}, },
/*family: {
[Op.like]: `%${value}%`,
},
subfamily: {
[Op.like]: `%${value}%`,
},*/
description: { description: {
[Op.like]: `%${value}%`, [Op.like]: `%${value}%`,
}, },

View File

@ -0,0 +1,31 @@
import { IRepositoryManager, RepositoryManager } from "@/contexts/common/domain";
import {
ISequelizeAdapter,
createSequelizeAdapter,
} from "@/contexts/common/infrastructure/sequelize";
export interface IProfileContext {
adapter: ISequelizeAdapter;
repositoryManager: IRepositoryManager;
}
export class ProfileContext {
private static instance: ProfileContext | null = null;
public static getInstance(): IProfileContext {
if (!ProfileContext.instance) {
ProfileContext.instance = new ProfileContext({
adapter: createSequelizeAdapter(),
repositoryManager: RepositoryManager.getInstance(),
});
}
return ProfileContext.instance.context;
}
private context: IProfileContext;
private constructor(context: IProfileContext) {
this.context = context;
}
}

View File

@ -0,0 +1,95 @@
import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { User } from "@/contexts/users/domain/entities/User";
import { IGetUserResponse_DTO } from "@shared/contexts";
import { IServerError } from "@/contexts/common/domain/errors";
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
import { GetDealerByUserUseCase } from "@/contexts/sales/application";
import { Dealer } from "@/contexts/sales/domain";
import { IProfileContext } from "../../../Profile.context";
import { IGetProfilePresenter } from "./presenter";
export class GetProfileController extends ExpressController {
private useCase: GetDealerByUserUseCase;
private presenter: IGetProfilePresenter;
private context: IProfileContext;
constructor(
props: {
useCase: GetDealerByUserUseCase;
presenter: IGetProfilePresenter;
},
context: IProfileContext
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
async executeImpl(): Promise<any> {
const user = <User | undefined>this.req.user;
if (!user) {
const errorMessage = "Unexpected missing user data";
const infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage
);
return this.internalServerError(errorMessage, infraError);
}
try {
const result = await this.useCase.execute({
userId: user.id,
});
if (result.isFailure) {
return this._handleExecuteError(result.error);
}
const dealer = <Dealer>result.object;
return this.ok<IGetUserResponse_DTO>(this.presenter.map(user, dealer, this.context));
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
private _handleExecuteError(error: IUseCaseError) {
let errorMessage: string;
let infraError: IInfrastructureError;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
errorMessage = "User has no associated profile";
infraError = InfrastructureError.create(
InfrastructureError.RESOURCE_NOT_READY,
errorMessage,
error
);
return this.notFoundError(errorMessage, infraError);
break;
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);
}
}
}

View File

@ -0,0 +1,17 @@
import { GetDealerByUserUseCase } from "@/contexts/sales/application";
import { registerDealerRepository } from "@/contexts/sales/infrastructure/Dealer.repository";
import { IProfileContext } from "../../../Profile.context";
import { GetProfileController } from "./GetProfile.controller";
import { GetUserPresenter } from "./presenter";
export const createGetProfileController = (context: IProfileContext) => {
registerDealerRepository(context);
return new GetProfileController(
{
useCase: new GetDealerByUserUseCase(context),
presenter: GetUserPresenter,
},
context
);
};

View File

@ -0,0 +1,26 @@
import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context";
import { Dealer } from "@/contexts/sales/domain";
import { User } from "@/contexts/users/domain";
import { IGetProfileResponse_DTO } from "@shared/contexts";
export interface IGetProfilePresenter {
map: (user: User, dealer: Dealer, context: IProfileContext) => IGetProfileResponse_DTO;
}
export const GetUserPresenter: IGetProfilePresenter = {
map: (user: User, dealer: Dealer, context: IProfileContext): IGetProfileResponse_DTO => {
return {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
language: "es",
roles: user.getRoles().map((rol) => rol.toString()),
contact_information: "",
default_payment_method: "",
default_notes: "",
default_legal_terms: "",
default_quote_validity: "",
status: "active",
};
},
};

View File

@ -0,0 +1 @@
export * from "./GetUser.presenter";

View File

@ -0,0 +1,2 @@
export * from "./getProfile";
//export * from "./updateProfile";

View File

@ -0,0 +1 @@
export * from "./controllers";

View File

@ -0,0 +1,2 @@
export * from "./Profile.context";
export * from "./express";

View File

@ -2,11 +2,13 @@ import { AggregateRoot, IDomainError, Name, Result, UniqueID } from "@shared/con
export interface IDealerProps { export interface IDealerProps {
name: Name; name: Name;
user_id: UniqueID;
} }
export interface IDealer { export interface IDealer {
id: UniqueID; id: UniqueID;
name: Name; name: Name;
user_id: UniqueID;
} }
export class Dealer extends AggregateRoot<IDealerProps> implements IDealer { export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
@ -18,4 +20,8 @@ export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
get name(): Name { get name(): Name {
return this.props.name; return this.props.name;
} }
get user_id(): UniqueID {
return this.props.user_id;
}
} }

View File

@ -53,20 +53,7 @@ export class DealerRepository extends SequelizeRepository<Dealer> implements IDe
} }
public async getByUserId(userId: UniqueID): Promise<Dealer | null> { public async getByUserId(userId: UniqueID): Promise<Dealer | null> {
const _dealer_model = this._adapter.getModel("Dealer_Model"); const rawDealer: any = await this._getBy("Dealer_Model", "user_id", userId.toPrimitive());
const _user_model = this._adapter.getModel("User_Model");
const rawDealer: any = await _dealer_model.findOne({
include: [
{
attributes: [],
model: _user_model,
as: "users",
required: true,
where: { id: userId.toPrimitive() },
},
],
});
if (!rawDealer === true) { if (!rawDealer === true) {
return null; return null;

View File

@ -1 +1,2 @@
export * from "./dealers"; export * from "./dealers";
export * from "./quotes";

View File

@ -1,26 +0,0 @@
import { isAdmin, isUser } from "@/contexts/auth";
import Express from "express";
import {
createDealerController,
deleteDealerController,
getDealerController,
updateDealerController,
} from "./controllers/dealers";
import { listDealersController } from "./controllers/dealers/listDealers";
import { getDealerMiddleware } from "./middlewares/dealerMiddleware";
import { quoteRoutes } from "./quote.routes";
export const DealerRouter = (appRouter: Express.Router) => {
const dealerRoutes: Express.Router = Express.Router({ mergeParams: true });
dealerRoutes.get("/", isAdmin, listDealersController);
dealerRoutes.get("/:dealerId", isUser, getDealerMiddleware, getDealerController);
dealerRoutes.post("/", isAdmin, createDealerController);
dealerRoutes.put("/:dealerId", isAdmin, updateDealerController);
dealerRoutes.delete("/:dealerId", isAdmin, deleteDealerController);
// Anidar quotes en /dealers/:dealerId
dealerRoutes.use("/:dealerId/quotes", quoteRoutes);
appRouter.use("/dealers", dealerRoutes);
};

View File

@ -1 +1 @@
export * from "./routes"; export * from "../../../../infrastructure/express/api/routes/sales.routes";

View File

@ -1 +1,2 @@
export * from "./Sales.context"; export * from "./Sales.context";
export * from "./express";

View File

@ -22,6 +22,7 @@ class DealerMapper
protected toDomainMappingImpl(source: Dealer_Model, params: any): Dealer { protected toDomainMappingImpl(source: Dealer_Model, params: any): Dealer {
const props: IDealerProps = { const props: IDealerProps = {
name: this.mapsValue(source, "name", Name.create), name: this.mapsValue(source, "name", Name.create),
user_id: this.mapsValue(source, "user_id", UniqueID.create),
}; };
const id = this.mapsValue(source, "id", UniqueID.create); const id = this.mapsValue(source, "id", UniqueID.create);
@ -37,7 +38,8 @@ class DealerMapper
protected toPersistenceMappingImpl(source: Dealer, params?: MapperParamsType | undefined) { protected toPersistenceMappingImpl(source: Dealer, params?: MapperParamsType | undefined) {
return { return {
id: source.id.toPrimitive(), id: source.id.toPrimitive(),
id_contact: undefined, user_id: source.user_id.toPrimitive(),
contact_id: undefined,
name: source.name.toPrimitive(), name: source.name.toPrimitive(),
contact_information: "", contact_information: "",
default_payment_method: "", default_payment_method: "",
@ -45,6 +47,7 @@ class DealerMapper
default_legal_terms: "", default_legal_terms: "",
default_quote_validity: "", default_quote_validity: "",
status: DEALER_STATUS.STATUS_ACTIVE, status: DEALER_STATUS.STATUS_ACTIVE,
language: "",
}; };
} }
} }

View File

@ -1,8 +1,10 @@
import { User_Model } from "@/contexts/users";
import { import {
DataTypes, DataTypes,
InferAttributes, InferAttributes,
InferCreationAttributes, InferCreationAttributes,
Model, Model,
NonAttribute,
Op, Op,
Sequelize, Sequelize,
} from "sequelize"; } from "sequelize";
@ -13,11 +15,14 @@ export enum DEALER_STATUS {
STATUS_BLOCKED = "blocked", STATUS_BLOCKED = "blocked",
} }
export type DealerCreationAttributes = InferCreationAttributes<Dealer_Model>; export type DealerCreationAttributes = InferCreationAttributes<
Dealer_Model,
{ omit: "user" | "quotes" }
>;
export class Dealer_Model extends Model< export class Dealer_Model extends Model<
InferAttributes<Dealer_Model>, InferAttributes<Dealer_Model, { omit: "user" | "quotes" }>,
InferCreationAttributes<Dealer_Model> InferCreationAttributes<Dealer_Model, { omit: "user" | "quotes" }>
> { > {
// To avoid table creation // To avoid table creation
/*static async sync(): Promise<any> { /*static async sync(): Promise<any> {
@ -28,9 +33,9 @@ export class Dealer_Model extends Model<
static associate(connection: Sequelize) { static associate(connection: Sequelize) {
const { Dealer_Model, User_Model } = connection.models; const { Dealer_Model, User_Model } = connection.models;
Dealer_Model.hasMany(User_Model, { Dealer_Model.belongsTo(User_Model, {
as: "users", as: "user",
foreignKey: "dealer_id", foreignKey: "user_id",
onDelete: "RESTRICT", onDelete: "RESTRICT",
}); });
@ -42,7 +47,7 @@ export class Dealer_Model extends Model<
} }
declare id: string; declare id: string;
declare id_contact?: string; // number ?? declare contact_id?: string; // number ??
declare name: string; declare name: string;
declare contact_information: string; declare contact_information: string;
declare default_payment_method: string; declare default_payment_method: string;
@ -51,6 +56,9 @@ export class Dealer_Model extends Model<
declare default_quote_validity: string; declare default_quote_validity: string;
declare status: DEALER_STATUS; declare status: DEALER_STATUS;
declare language: string; declare language: string;
declare user?: NonAttribute<User_Model>;
declare quotes?: NonAttribute<Quote_Model>;
} }
export default (sequelize: Sequelize) => { export default (sequelize: Sequelize) => {
@ -61,7 +69,7 @@ export default (sequelize: Sequelize) => {
primaryKey: true, primaryKey: true,
}, },
id_contact: { contact_id: {
type: DataTypes.BIGINT().UNSIGNED, type: DataTypes.BIGINT().UNSIGNED,
allowNull: true, allowNull: true,
}, },
@ -96,7 +104,7 @@ export default (sequelize: Sequelize) => {
deletedAt: "deleted_at", deletedAt: "deleted_at",
indexes: [ indexes: [
{ name: "id_contact_idx", fields: ["id_contact"] }, { name: "contact_id_idx", fields: ["contact_id"] },
{ name: "status_idx", fields: ["status"] }, { name: "status_idx", fields: ["status"] },
], ],

View File

@ -1,5 +1,6 @@
import { config } from "@/config"; import { config } from "@/config";
import { IAdapter, Password, RepositoryBuilder } from "@/contexts/common/domain"; import { IAdapter, Password, RepositoryBuilder } from "@/contexts/common/domain";
import { Dealer, IDealerRepository } from "@/contexts/sales/domain";
import { Email, Language, Name, UniqueID } from "@shared/contexts"; import { Email, Language, Name, UniqueID } from "@shared/contexts";
import { IUserRepository, User, UserRole } from "../domain"; import { IUserRepository, User, UserRole } from "../domain";
@ -76,3 +77,51 @@ export const initializeAdmin = async (
} }
}); });
}; };
export const initializeSampleUser = async (
adapter: IAdapter,
repository: RepositoryBuilder<IUserRepository>
) => {
return await adapter.startTransaction().complete(async (t) => {
const email = Email.create(config.sample_dealer.email).object;
const userExists = await repository({ transaction: t }).existsUserWithEmail(email);
if (!userExists) {
const user = User.create(
{
name: Name.create(config.sample_dealer.name).object,
email,
password: Password.createFromPlainTextPassword(config.sample_dealer.password).object,
language: Language.createFromCode(config.sample_dealer.language).object,
roles: [UserRole.ROLE_USER],
},
UniqueID.generateNewID().object
).object;
await repository({ transaction: t }).create(user);
console.log("Usuario creado");
return user;
}
});
};
export const initializeSampleDealer = async (
user: User,
adapter: IAdapter,
repository: RepositoryBuilder<IDealerRepository>
) => {
return await adapter.startTransaction().complete(async (t) => {
const dealerExists = await repository({ transaction: t }).getByUserId(user.id);
if (!dealerExists) {
const dealer = Dealer.create(
{
name: Name.create(config.sample_dealer.name).object,
user_id: user.id,
},
UniqueID.generateNewID().object
).object;
await repository({ transaction: t }).create(dealer);
console.log("Dealer creado");
}
});
};

View File

@ -1,2 +1,2 @@
export * from "../../../../infrastructure/express/api/routes/users.routes";
export * from "./controllers"; export * from "./controllers";
export * from "./routes";

View File

@ -27,9 +27,9 @@ export class User_Model extends Model<
static associate(connection: Sequelize) { static associate(connection: Sequelize) {
const { User_Model, Dealer_Model } = connection.models; const { User_Model, Dealer_Model } = connection.models;
User_Model.belongsTo(Dealer_Model, { User_Model.hasOne(Dealer_Model, {
as: "dealer", as: "dealer",
foreignKey: "dealer_id", foreignKey: "user_id",
onDelete: "RESTRICT", onDelete: "RESTRICT",
}); });
} }

View File

@ -1,10 +1,10 @@
import Express from "express"; import Express from "express";
import passport from "passport"; import passport from "passport";
import { createLoginController } from "./controllers"; import { createLoginController } from "../../../../contexts/auth/infrastructure/express/controllers";
import { createIdentityController } from "./controllers/identity"; import { createIdentityController } from "../../../../contexts/auth/infrastructure/express/controllers/identity";
import { isUser } from "./passport"; import { checkUser } from "../../../../contexts/auth/infrastructure/express/passport";
export const AuthRouter = (appRouter: Express.Router) => { export const authRouter = (appRouter: Express.Router) => {
const authRoutes: Express.Router = Express.Router({ mergeParams: true }); const authRoutes: Express.Router = Express.Router({ mergeParams: true });
//appRouter.use(registerMiddleware("isUser", isUser)); //appRouter.use(registerMiddleware("isUser", isUser));
@ -17,16 +17,20 @@ export const AuthRouter = (appRouter: Express.Router) => {
createLoginController(res.locals["context"]).execute(req, res, next) createLoginController(res.locals["context"]).execute(req, res, next)
); );
authRoutes.post("/logout", isUser, (req: Express.Request, res: Express.Response) => { authRoutes.post("/logout", checkUser, (req: Express.Request, res: Express.Response) => {
//req.logout(); <-- ?? req.logout(function (err) {
return res.status(200).json(); if (err) {
return res.status(500).json();
}
return res.status(200).json();
});
}); });
authRoutes.post("/register"); authRoutes.post("/register");
authRoutes.get( authRoutes.get(
"/identity", "/identity",
isUser, checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => (req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createIdentityController(res.locals["context"]).execute(req, res, next) createIdentityController(res.locals["context"]).execute(req, res, next)
); );

View File

@ -1,19 +1,13 @@
import { isUser } from "@/contexts/auth"; import { checkUser } from "@/contexts/auth";
import Express from "express"; import Express from "express";
import { listArticlesController } from "./controllers"; import { listArticlesController } from "../../../../contexts/catalog/infrastructure/express/controllers";
/*catalogRoutes.get( export const catalogRouter = (appRouter: Express.Router) => {
"/:articleId",
(req: Request, res: Response, next: NextFunction) =>
createGetCustomerController(res.locals["context"]).execute(req, res, next)
);*/
export const CatalogRouter = (appRouter: Express.Router) => {
const catalogRoutes: Express.Router = Express.Router({ mergeParams: true }); const catalogRoutes: Express.Router = Express.Router({ mergeParams: true });
catalogRoutes.get( catalogRoutes.get(
"/", "/",
isUser, checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => (req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
listArticlesController(res.locals["context"]).execute(req, res, next) listArticlesController(res.locals["context"]).execute(req, res, next)
); );

View File

@ -0,0 +1,27 @@
import { checkUser, checkisAdmin } from "@/contexts/auth";
import {
createDealerController,
deleteDealerController,
getDealerController,
listDealersController,
updateDealerController,
} from "@/contexts/sales/infrastructure/express/controllers/dealers";
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
import Express from "express";
import { quoteRoutes } from "./quote.routes";
5;
export const DealerRouter = (appRouter: Express.Router) => {
const dealerRoutes: Express.Router = Express.Router({ mergeParams: true });
dealerRoutes.get("/", checkisAdmin, listDealersController);
dealerRoutes.get("/:dealerId", checkUser, getDealerMiddleware, getDealerController);
dealerRoutes.post("/", checkisAdmin, createDealerController);
dealerRoutes.put("/:dealerId", checkisAdmin, updateDealerController);
dealerRoutes.delete("/:dealerId", checkisAdmin, deleteDealerController);
// Anidar quotes en /dealers/:dealerId
dealerRoutes.use("/:dealerId/quotes", quoteRoutes);
appRouter.use("/dealers", dealerRoutes);
};

View File

@ -0,0 +1,7 @@
export * from "./auth.routes";
export * from "./catalog.routes";
export * from "./dealers.routes";
export * from "./profile.routes";
export * from "./quote.routes";
export * from "./sales.routes";
export * from "./users.routes";

View File

@ -0,0 +1,24 @@
import { checkUser } from "@/contexts/auth";
import { createGetProfileController } from "@/contexts/profile/infrastructure";
import { createUpdateUserController } from "@/contexts/users";
import Express from "express";
export const profileRouter = (appRouter: Express.Router) => {
const profileRoutes: Express.Router = Express.Router({ mergeParams: true });
profileRoutes.get(
"/",
checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createGetProfileController(res.locals["context"]).execute(req, res, next)
);
profileRoutes.put(
"/",
checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createUpdateUserController(res.locals["context"]).execute(req, res, next)
);
appRouter.use("/profile", profileRoutes);
};

View File

@ -1,4 +1,4 @@
import { isAdmin } from "@/contexts/auth"; import { checkisAdmin } from "@/contexts/auth";
import Express from "express"; import Express from "express";
export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true }); export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
@ -9,7 +9,7 @@ quoteRoutes.post("/", isAdmin, createQuoteController);
quoteRoutes.put("/:quoteId", isAdmin, updateQuoteController); quoteRoutes.put("/:quoteId", isAdmin, updateQuoteController);
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/ quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
quoteRoutes.get("/", isAdmin, (req, res) => { quoteRoutes.get("/", checkisAdmin, (req, res) => {
console.log(req.params); console.log(req.params);
res.status(200).json(); res.status(200).json();
}); });

View File

@ -2,7 +2,7 @@ import Express from "express";
import { DealerRouter } from "./dealers.routes"; import { DealerRouter } from "./dealers.routes";
import { QuoteRouter } from "./quote.routes"; import { QuoteRouter } from "./quote.routes";
export const SalesRouter = (appRouter: Express.Router) => { export const salesRouter = (appRouter: Express.Router) => {
DealerRouter(appRouter); DealerRouter(appRouter);
QuoteRouter(appRouter); QuoteRouter(appRouter);
}; };

View File

@ -1,4 +1,4 @@
import { isAdmin, isAdminOrMe } from "@/contexts/auth"; import { checkAdminOrSelf, checkisAdmin } from "@/contexts/auth";
import Express from "express"; import Express from "express";
import { import {
createCreateUserController, createCreateUserController,
@ -6,42 +6,42 @@ import {
createGetUserController, createGetUserController,
createListUsersController, createListUsersController,
createUpdateUserController, createUpdateUserController,
} from "./controllers"; } from "../../../../contexts/users/infrastructure/express/controllers";
export const UserRouter = (appRouter: Express.Router) => { export const usersRouter = (appRouter: Express.Router) => {
const userRoutes: Express.Router = Express.Router({ mergeParams: true }); const userRoutes: Express.Router = Express.Router({ mergeParams: true });
userRoutes.get( userRoutes.get(
"/", "/",
isAdmin, checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => (req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createListUsersController(res.locals["context"]).execute(req, res, next) createListUsersController(res.locals["context"]).execute(req, res, next)
); );
userRoutes.get( userRoutes.get(
"/:userId", "/:userId",
isAdminOrMe, checkAdminOrSelf,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => (req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createGetUserController(res.locals["context"]).execute(req, res, next) createGetUserController(res.locals["context"]).execute(req, res, next)
); );
userRoutes.post( userRoutes.post(
"/", "/",
isAdmin, checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => (req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createCreateUserController(res.locals["context"]).execute(req, res, next) createCreateUserController(res.locals["context"]).execute(req, res, next)
); );
userRoutes.put( userRoutes.put(
"/:userId", "/:userId",
isAdmin, checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => (req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createUpdateUserController(res.locals["context"]).execute(req, res, next) createUpdateUserController(res.locals["context"]).execute(req, res, next)
); );
userRoutes.delete( userRoutes.delete(
"/:userId", "/:userId",
isAdmin, checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => (req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createDeleteUserController(res.locals["context"]).execute(req, res, next) createDeleteUserController(res.locals["context"]).execute(req, res, next)
); );

View File

@ -1,9 +1,7 @@
import { AuthRouter } from "@/contexts/auth"; import { salesRouter } from "@/contexts/sales/infrastructure/express";
import { CatalogRouter } from "@/contexts/catalog";
import { SalesRouter } from "@/contexts/sales/infrastructure/express";
import { UserRouter } from "@/contexts/users";
import Express from "express"; import Express from "express";
import { createContextMiddleware } from "./context.middleware"; import { createContextMiddleware } from "./context.middleware";
import { authRouter, catalogRouter, profileRouter, usersRouter } from "./routes";
export const v1Routes = () => { export const v1Routes = () => {
const routes = Express.Router({ mergeParams: true }); const routes = Express.Router({ mergeParams: true });
@ -23,10 +21,83 @@ export const v1Routes = () => {
next(); next();
}); });
AuthRouter(routes); authRouter(routes);
UserRouter(routes); profileRouter(routes);
CatalogRouter(routes); usersRouter(routes);
SalesRouter(routes); catalogRouter(routes);
salesRouter(routes);
return routes; return routes;
}; };
/**
*
*
Comentarios: Cada usuario que no sea administrador tiene un registro asociado en "dealers" a modo de perfil
* Endpoints Públicos
/auth
POST /auth/login: Permite a los usuarios autenticarse y recibir un token JWT.
* Endpoints para Usuarios Registrados
/auth
POST /auth/logout: Permite a los usuarios cerrar sesión (podría implicar invalidar el token JWT).
GET /auth/identity: Devuelve la identidad del usuario autenticado, incluyendo id, name, email, language y roles.
/profile
GET /profile: Obtiene el perfil del usuario autenticado.
POST /profile: Actualiza el perfil del usuario autenticado.
/catalog
GET /catalog: Devuelve la lista de artículos del catálogo.
/quotes
GET /quotes: Devuelve las cotizaciones del usuario autenticado.
POST /quotes: Permite al usuario crear una nueva cotización.
PUT /quotes/
: Permite al usuario actualizar una cotización existente.
DELETE /quotes/
: Permite al usuario eliminar una cotización existente.
* Endpoints para Administradores
/auth
POST /auth/register: Permite al administrador registrar nuevos usuarios.
/users
GET /users: Devuelve la lista de todos los usuarios.
POST /users: Permite crear un nuevo usuario.
GET /users/
: Devuelve los detalles de un usuario específico.
PUT /users/
: Permite actualizar los detalles de un usuario específico.
DELETE /users/
: Permite eliminar un usuario específico.
/dealers
GET /dealers: Devuelve la lista de todos los distribuidores.
POST /dealers: Permite crear un nuevo distribuidor.
GET /dealers/
: Devuelve los detalles de un distribuidor específico.
PUT /dealers/
: Permite actualizar los detalles de un distribuidor específico.
DELETE /dealers/
: Permite eliminar un distribuidor específico.
/quotes
GET /quotes: Devuelve la lista de todas las cotizaciones.
POST /quotes: Permite crear una nueva cotización.
PUT /quotes/
: Permite actualizar una cotización existente.
DELETE /quotes/
: Permite eliminar una cotización existente.
*/

View File

@ -9,7 +9,7 @@ import { trace } from "console";
import { config } from "../../config"; import { config } from "../../config";
import { app } from "../express/app"; import { app } from "../express/app";
import { initLogger } from "../logger"; import { initLogger } from "../logger";
import { initializeAdminUser } from "../sequelize/initializeAdminUser"; import { insertUsers } from "../sequelize/initData";
process.env.TZ = "UTC"; process.env.TZ = "UTC";
Settings.defaultLocale = "es-ES"; Settings.defaultLocale = "es-ES";
@ -109,7 +109,7 @@ try {
sequelizeConn.sync({ force: false, alter: true }).then(() => { sequelizeConn.sync({ force: false, alter: true }).then(() => {
// //
initializeAdminUser(); insertUsers();
// Launch server // Launch server
server.listen(currentState.server.port, () => { server.listen(currentState.server.port, () => {

View File

@ -0,0 +1,28 @@
import { registerDealerRepository } from "@/contexts/sales/infrastructure/Dealer.repository";
import {
initializeAdmin,
initializeSampleDealer,
initializeSampleUser,
} from "@/contexts/users/application/userServices";
import { UserContext } from "@/contexts/users/infrastructure/User.context";
import { registerUserRepository } from "@/contexts/users/infrastructure/User.repository";
export const insertUsers = async () => {
const context = UserContext.getInstance();
registerUserRepository(context);
registerDealerRepository(context);
initializeAdmin(context.adapter, context.repositoryManager.getRepository("User"));
const user = await initializeSampleUser(
context.adapter,
context.repositoryManager.getRepository("User")
);
if (user) {
initializeSampleDealer(
user,
context.adapter,
context.repositoryManager.getRepository("Dealer")
);
}
};

View File

@ -1,10 +0,0 @@
import { initializeAdmin } from "@/contexts/users/application/userServices";
import { UserContext } from "@/contexts/users/infrastructure/User.context";
import { registerUserRepository } from "@/contexts/users/infrastructure/User.repository";
export const initializeAdminUser = () => {
const context = UserContext.getInstance();
registerUserRepository(context);
initializeAdmin(context.adapter, context.repositoryManager.getRepository("User"));
};

View File

@ -2,6 +2,13 @@ import Joi from "joi";
import { RuleValidator } from "../../../RuleValidator"; import { RuleValidator } from "../../../RuleValidator";
import { Result } from "../../Result"; import { Result } from "../../Result";
import { ValueObject } from "../../ValueObject"; import { ValueObject } from "../../ValueObject";
import {
INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE,
MIN_PAGE_INDEX,
MIN_PAGE_SIZE,
} from "./defaults";
export interface IOffsetPagingProps { export interface IOffsetPagingProps {
offset: number | string | undefined; offset: number | string | undefined;
@ -14,12 +21,12 @@ export interface IOffsetPaging {
} }
export class OffsetPaging extends ValueObject<IOffsetPaging> { export class OffsetPaging extends ValueObject<IOffsetPaging> {
public static readonly LIMIT_DEFAULT_VALUE: number = 10; public static readonly LIMIT_DEFAULT_VALUE: number = INITIAL_PAGE_SIZE;
public static readonly LIMIT_MINIMAL_VALUE: number = 1; public static readonly LIMIT_MINIMAL_VALUE: number = MIN_PAGE_SIZE;
public static readonly LIMIT_MAXIMAL_VALUE: number = 100; public static readonly LIMIT_MAXIMAL_VALUE: number = MAX_PAGE_SIZE;
public static readonly OFFSET_DEFAULT_VALUE: number = 0; public static readonly OFFSET_DEFAULT_VALUE: number = INITIAL_PAGE_INDEX;
public static readonly OFFSET_MINIMAL_VALUE: number = 0; public static readonly OFFSET_MINIMAL_VALUE: number = MIN_PAGE_INDEX;
public static readonly OFFSET_MAXIMAL_VALUE: number = Number.MAX_SAFE_INTEGER; public static readonly OFFSET_MAXIMAL_VALUE: number = Number.MAX_SAFE_INTEGER;
public static createWithMaxLimit(): Result<OffsetPaging> { public static createWithMaxLimit(): Result<OffsetPaging> {
@ -52,10 +59,7 @@ export class OffsetPaging extends ValueObject<IOffsetPaging> {
} }
private static validate(offset: string | number, limit: string | number) { private static validate(offset: string | number, limit: string | number) {
const numberOrError = RuleValidator.validate( const numberOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_NUMBER, offset);
RuleValidator.RULE_IS_TYPE_NUMBER,
offset,
);
if (numberOrError.isFailure) { if (numberOrError.isFailure) {
return numberOrError; return numberOrError;
@ -64,25 +68,18 @@ export class OffsetPaging extends ValueObject<IOffsetPaging> {
const _offset = typeof offset === "string" ? parseInt(offset, 10) : offset; const _offset = typeof offset === "string" ? parseInt(offset, 10) : offset;
const offsetValidate = RuleValidator.validate( const offsetValidate = RuleValidator.validate(
Joi.number() Joi.number().min(OffsetPaging.OFFSET_MINIMAL_VALUE).max(OffsetPaging.OFFSET_MAXIMAL_VALUE),
.min(OffsetPaging.OFFSET_MINIMAL_VALUE) offset
.max(OffsetPaging.OFFSET_MAXIMAL_VALUE),
offset,
); );
if (offsetValidate.isFailure) { if (offsetValidate.isFailure) {
return Result.fail( return Result.fail(
new Error( new Error(`Page need to be larger than or equal to ${OffsetPaging.OFFSET_MINIMAL_VALUE}.`)
`Page need to be larger than or equal to ${OffsetPaging.OFFSET_MINIMAL_VALUE}.`,
),
); );
} }
// limit // limit
const limitNumberOrError = RuleValidator.validate( const limitNumberOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_NUMBER, limit);
RuleValidator.RULE_IS_TYPE_NUMBER,
limit,
);
if (limitNumberOrError.isFailure) { if (limitNumberOrError.isFailure) {
return limitNumberOrError; return limitNumberOrError;
@ -92,14 +89,12 @@ export class OffsetPaging extends ValueObject<IOffsetPaging> {
const limitValidate = RuleValidator.validate( const limitValidate = RuleValidator.validate(
Joi.number().min(0).max(OffsetPaging.LIMIT_MAXIMAL_VALUE), Joi.number().min(0).max(OffsetPaging.LIMIT_MAXIMAL_VALUE),
offset, offset
); );
if (limitValidate.isFailure) { if (limitValidate.isFailure) {
return Result.fail( return Result.fail(
new Error( new Error(`Page size need to be smaller than ${OffsetPaging.LIMIT_MAXIMAL_VALUE}`)
`Page size need to be smaller than ${OffsetPaging.LIMIT_MAXIMAL_VALUE}`,
),
); );
} }

View File

@ -0,0 +1,7 @@
export const INITIAL_PAGE_INDEX = 0;
export const INITIAL_PAGE_SIZE = 15;
export const MIN_PAGE_INDEX = 0;
export const MIN_PAGE_SIZE = 1;
export const MAX_PAGE_SIZE = 100; //Number.MAX_SAFE_INTEGER;

View File

@ -1 +1,2 @@
export * from './OffsetPaging'; export * from "./OffsetPaging";
export * from "./defaults";

View File

@ -1,6 +1,6 @@
export * from "./common";
export * from "./auth"; export * from "./auth";
export * from "./catalog"; export * from "./catalog";
export * from "./common";
export * from "./profile";
export * from "./sales"; export * from "./sales";
export * from "./users"; export * from "./users";

View File

@ -0,0 +1,13 @@
export interface IGetProfileResponse_DTO {
id: string;
name: string;
email: string;
language: string;
roles: string[];
contact_information: string;
default_payment_method: string;
default_notes: string;
default_legal_terms: string;
default_quote_validity: string;
status: string;
}

View File

@ -0,0 +1 @@
export * from "./IGetProfile_Response.dto";

View File

@ -0,0 +1 @@
export * from "./GetProfile.dto";

View File

@ -0,0 +1 @@
export * from "./dto";

View File

@ -0,0 +1 @@
export * from "./application";