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 { 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 { DashboardPage } from "./app/dashboard";
import { QuotesList } from "./app/quotes/list";
@ -68,9 +76,17 @@ export const Routes = () => {
path: "/settings",
element: (
<ProtectedRoute>
<SettingsPage />
<SettingsLayout>
<Outlet />
</SettingsLayout>
</ProtectedRoute>
),
children: [
{
index: true,
element: <SettingsEditor />,
},
],
},
{
path: "/logout",

View File

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

View File

@ -16,7 +16,6 @@ export const CatalogLayout = ({ children }: PropsWithChildren) => {
</div>
{children}
</LayoutContent>
1
</Layout>
</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";
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 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>
);
};
export * from "./edit";
export * from "./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 { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
import { INITIAL_PAGE_INDEX } from "@shared/contexts";
import { Table } from "@tanstack/react-table";
import { t } from "i18next";
import {

View File

@ -9,7 +9,7 @@ import { UserButton } from "./components/UserButton";
export const LayoutHeader = () => {
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'>
<Link to='/' className='flex items-center font-semibold'>
<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 {
ICreateOneDataProviderParams,
IDataSource,
@ -10,7 +10,6 @@ import {
ISortItemDataProviderParam,
IUpdateOneDataProviderParams,
} from "../hooks/useDataSource/DataSource";
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "../hooks/usePagination";
import { createAxiosInstance } from "./axiosInstance";
export const createAxiosDataProvider = (

View File

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

View File

@ -1,12 +1,11 @@
import { act, renderHook } from "@testing-library/react-hooks";
import {
INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE,
MIN_PAGE_SIZE,
PaginationState,
usePagination,
} from "./usePagination";
} from "@shared/contexts";
import { act, renderHook } from "@testing-library/react-hooks";
import { PaginationState, usePagination } from "./usePagination";
describe("usePagination", () => {
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";
export const INITIAL_PAGE_INDEX = 0;
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 const DEFAULT_PAGE_SIZES = [15, 30, 50, 75, 100];
export interface PaginationState {
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 {
INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE,
MIN_PAGE_SIZE,
usePagination,
} from "./usePagination";
} from "@shared/contexts";
import { PaginationState } from "@tanstack/react-table";
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { usePagination } from "./usePagination";
export const usePaginationParams = (
initialPageIndex: number = INITIAL_PAGE_INDEX,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,16 @@
import { AuthUser } from "@/contexts/auth/domain";
import { composeMiddleware, generateExpressError } from "@/contexts/common/infrastructure/express";
import { UniqueID } from "@shared/contexts";
import { ensureIdIsValid } from "@shared/contexts";
import Express from "express";
import httpStatus from "http-status";
import passport from "passport";
export const isUser = composeMiddleware([
export const checkUser = composeMiddleware([
passport.authenticate("local-jwt", {
session: false,
}),
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
if (req.isAuthenticated()) {
console.log(<AuthUser>req.user);
/*const user = <AuthUser>req.user;
if (!user.isUser) {
return generateExpressError(req, res, httpStatus.UNAUTHORIZED);
}*/
return next();
}
@ -23,25 +18,34 @@ export const isUser = composeMiddleware([
},
]);
export const isAdmin = composeMiddleware([
isUser,
export const checkisAdmin = composeMiddleware([
checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
const user = <AuthUser>req.user;
if (!user.isAdmin) {
generateExpressError(req, res, httpStatus.UNAUTHORIZED);
}
next();
return next();
},
]);
export const isAdminOrMe = composeMiddleware([
isUser,
export const checkAdminOrSelf = composeMiddleware([
checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
const user = <AuthUser>req.user;
const { userId } = req.params;
if (user.isAdmin || user.id.equals(UniqueID.create(userId).object)) {
next();
} else generateExpressError(req, res, httpStatus.UNAUTHORIZED);
if (user.isAdmin) {
return next();
}
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;
id_article: ArticleIdentifier;
reference: Slug;
//family: Description;
//subfamily: Description;
description: Description;
points: Quantity;
retail_price: UnitPrice;
@ -26,8 +24,6 @@ export interface IArticle {
catalog_name: Slug;
id_article: ArticleIdentifier;
reference: Slug;
//family: Description;
//subfamily: Description;
description: Description;
points: Quantity;
retail_price: UnitPrice;
@ -67,14 +63,6 @@ export class Article extends AggregateRoot<IArticleProps> implements IArticle {
return this.props.reference;
}
/*get family(): Description {
return this.props.family;
}
get subfamily(): Description {
return this.props.subfamily;
}*/
get description(): Description {
return this.props.description;
}

View File

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

View File

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

View File

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

View File

@ -20,8 +20,6 @@ class ArticleMapper
catalog_name: this.mapsValue(source, "catalog_name", Slug.create),
id_article: this.mapsValue(source, "id_article", ArticleIdentifier.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),
points: this.mapsValue(source, "points", Quantity.create),
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 id_article: string; // number ??
declare reference: CreationOptional<string>;
//declare family: CreationOptional<string>;
//declare subfamily: CreationOptional<string>;
declare description: CreationOptional<string>;
declare points: CreationOptional<number>;
declare retail_price: CreationOptional<number>;
@ -50,8 +48,6 @@ export default (sequelize: Sequelize) => {
allowNull: false,
},
reference: DataTypes.STRING(),
//family: DataTypes.STRING(),
//subfamily: DataTypes.STRING(),
description: DataTypes.STRING(),
points: {
type: DataTypes.SMALLINT().UNSIGNED,
@ -74,8 +70,6 @@ export default (sequelize: Sequelize) => {
indexes: [
{ name: "catalog_name_idx", fields: ["catalog_name"] },
{ 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"] },
],
@ -88,12 +82,6 @@ export default (sequelize: Sequelize) => {
reference: {
[Op.like]: `%${value}%`,
},
/*family: {
[Op.like]: `%${value}%`,
},
subfamily: {
[Op.like]: `%${value}%`,
},*/
description: {
[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 {
name: Name;
user_id: UniqueID;
}
export interface IDealer {
id: UniqueID;
name: Name;
user_id: UniqueID;
}
export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
@ -18,4 +20,8 @@ export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
get name(): 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> {
const _dealer_model = this._adapter.getModel("Dealer_Model");
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() },
},
],
});
const rawDealer: any = await this._getBy("Dealer_Model", "user_id", userId.toPrimitive());
if (!rawDealer === true) {
return null;

View File

@ -1 +1,2 @@
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 "./express";

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { config } from "@/config";
import { IAdapter, Password, RepositoryBuilder } from "@/contexts/common/domain";
import { Dealer, IDealerRepository } from "@/contexts/sales/domain";
import { Email, Language, Name, UniqueID } from "@shared/contexts";
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 "./routes";

View File

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

View File

@ -1,10 +1,10 @@
import Express from "express";
import passport from "passport";
import { createLoginController } from "./controllers";
import { createIdentityController } from "./controllers/identity";
import { isUser } from "./passport";
import { createLoginController } from "../../../../contexts/auth/infrastructure/express/controllers";
import { createIdentityController } from "../../../../contexts/auth/infrastructure/express/controllers/identity";
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 });
//appRouter.use(registerMiddleware("isUser", isUser));
@ -17,16 +17,20 @@ export const AuthRouter = (appRouter: Express.Router) => {
createLoginController(res.locals["context"]).execute(req, res, next)
);
authRoutes.post("/logout", isUser, (req: Express.Request, res: Express.Response) => {
//req.logout(); <-- ??
return res.status(200).json();
authRoutes.post("/logout", checkUser, (req: Express.Request, res: Express.Response) => {
req.logout(function (err) {
if (err) {
return res.status(500).json();
}
return res.status(200).json();
});
});
authRoutes.post("/register");
authRoutes.get(
"/identity",
isUser,
checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
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 { listArticlesController } from "./controllers";
import { listArticlesController } from "../../../../contexts/catalog/infrastructure/express/controllers";
/*catalogRoutes.get(
"/:articleId",
(req: Request, res: Response, next: NextFunction) =>
createGetCustomerController(res.locals["context"]).execute(req, res, next)
);*/
export const CatalogRouter = (appRouter: Express.Router) => {
export const catalogRouter = (appRouter: Express.Router) => {
const catalogRoutes: Express.Router = Express.Router({ mergeParams: true });
catalogRoutes.get(
"/",
isUser,
checkUser,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
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";
export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
@ -9,7 +9,7 @@ quoteRoutes.post("/", isAdmin, createQuoteController);
quoteRoutes.put("/:quoteId", isAdmin, updateQuoteController);
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
quoteRoutes.get("/", isAdmin, (req, res) => {
quoteRoutes.get("/", checkisAdmin, (req, res) => {
console.log(req.params);
res.status(200).json();
});

View File

@ -2,7 +2,7 @@ import Express from "express";
import { DealerRouter } from "./dealers.routes";
import { QuoteRouter } from "./quote.routes";
export const SalesRouter = (appRouter: Express.Router) => {
export const salesRouter = (appRouter: Express.Router) => {
DealerRouter(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 {
createCreateUserController,
@ -6,42 +6,42 @@ import {
createGetUserController,
createListUsersController,
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 });
userRoutes.get(
"/",
isAdmin,
checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createListUsersController(res.locals["context"]).execute(req, res, next)
);
userRoutes.get(
"/:userId",
isAdminOrMe,
checkAdminOrSelf,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createGetUserController(res.locals["context"]).execute(req, res, next)
);
userRoutes.post(
"/",
isAdmin,
checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createCreateUserController(res.locals["context"]).execute(req, res, next)
);
userRoutes.put(
"/:userId",
isAdmin,
checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createUpdateUserController(res.locals["context"]).execute(req, res, next)
);
userRoutes.delete(
"/:userId",
isAdmin,
checkisAdmin,
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createDeleteUserController(res.locals["context"]).execute(req, res, next)
);

View File

@ -1,9 +1,7 @@
import { AuthRouter } from "@/contexts/auth";
import { CatalogRouter } from "@/contexts/catalog";
import { SalesRouter } from "@/contexts/sales/infrastructure/express";
import { UserRouter } from "@/contexts/users";
import { salesRouter } from "@/contexts/sales/infrastructure/express";
import Express from "express";
import { createContextMiddleware } from "./context.middleware";
import { authRouter, catalogRouter, profileRouter, usersRouter } from "./routes";
export const v1Routes = () => {
const routes = Express.Router({ mergeParams: true });
@ -23,10 +21,83 @@ export const v1Routes = () => {
next();
});
AuthRouter(routes);
UserRouter(routes);
CatalogRouter(routes);
SalesRouter(routes);
authRouter(routes);
profileRouter(routes);
usersRouter(routes);
catalogRouter(routes);
salesRouter(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 { app } from "../express/app";
import { initLogger } from "../logger";
import { initializeAdminUser } from "../sequelize/initializeAdminUser";
import { insertUsers } from "../sequelize/initData";
process.env.TZ = "UTC";
Settings.defaultLocale = "es-ES";
@ -109,7 +109,7 @@ try {
sequelizeConn.sync({ force: false, alter: true }).then(() => {
//
initializeAdminUser();
insertUsers();
// Launch server
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 { Result } from "../../Result";
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 {
offset: number | string | undefined;
@ -14,12 +21,12 @@ export interface IOffsetPaging {
}
export class OffsetPaging extends ValueObject<IOffsetPaging> {
public static readonly LIMIT_DEFAULT_VALUE: number = 10;
public static readonly LIMIT_MINIMAL_VALUE: number = 1;
public static readonly LIMIT_MAXIMAL_VALUE: number = 100;
public static readonly LIMIT_DEFAULT_VALUE: number = INITIAL_PAGE_SIZE;
public static readonly LIMIT_MINIMAL_VALUE: number = MIN_PAGE_SIZE;
public static readonly LIMIT_MAXIMAL_VALUE: number = MAX_PAGE_SIZE;
public static readonly OFFSET_DEFAULT_VALUE: number = 0;
public static readonly OFFSET_MINIMAL_VALUE: number = 0;
public static readonly OFFSET_DEFAULT_VALUE: number = INITIAL_PAGE_INDEX;
public static readonly OFFSET_MINIMAL_VALUE: number = MIN_PAGE_INDEX;
public static readonly OFFSET_MAXIMAL_VALUE: number = Number.MAX_SAFE_INTEGER;
public static createWithMaxLimit(): Result<OffsetPaging> {
@ -52,10 +59,7 @@ export class OffsetPaging extends ValueObject<IOffsetPaging> {
}
private static validate(offset: string | number, limit: string | number) {
const numberOrError = RuleValidator.validate(
RuleValidator.RULE_IS_TYPE_NUMBER,
offset,
);
const numberOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_NUMBER, offset);
if (numberOrError.isFailure) {
return numberOrError;
@ -64,25 +68,18 @@ export class OffsetPaging extends ValueObject<IOffsetPaging> {
const _offset = typeof offset === "string" ? parseInt(offset, 10) : offset;
const offsetValidate = RuleValidator.validate(
Joi.number()
.min(OffsetPaging.OFFSET_MINIMAL_VALUE)
.max(OffsetPaging.OFFSET_MAXIMAL_VALUE),
offset,
Joi.number().min(OffsetPaging.OFFSET_MINIMAL_VALUE).max(OffsetPaging.OFFSET_MAXIMAL_VALUE),
offset
);
if (offsetValidate.isFailure) {
return Result.fail(
new Error(
`Page need to be larger than or equal to ${OffsetPaging.OFFSET_MINIMAL_VALUE}.`,
),
new Error(`Page need to be larger than or equal to ${OffsetPaging.OFFSET_MINIMAL_VALUE}.`)
);
}
// limit
const limitNumberOrError = RuleValidator.validate(
RuleValidator.RULE_IS_TYPE_NUMBER,
limit,
);
const limitNumberOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_NUMBER, limit);
if (limitNumberOrError.isFailure) {
return limitNumberOrError;
@ -92,14 +89,12 @@ export class OffsetPaging extends ValueObject<IOffsetPaging> {
const limitValidate = RuleValidator.validate(
Joi.number().min(0).max(OffsetPaging.LIMIT_MAXIMAL_VALUE),
offset,
offset
);
if (limitValidate.isFailure) {
return Result.fail(
new Error(
`Page size need to be smaller than ${OffsetPaging.LIMIT_MAXIMAL_VALUE}`,
),
new Error(`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 "./catalog";
export * from "./common";
export * from "./profile";
export * from "./sales";
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";