This commit is contained in:
David Arranz 2024-05-27 11:55:04 +02:00
parent 8d280cbb48
commit 27717cc9b8
22 changed files with 180 additions and 136 deletions

View File

@ -1,17 +1,12 @@
import { Password } from "@/contexts/common/domain";
import {
AggregateRoot,
Email,
IDomainError,
Name,
Result,
UniqueID,
} from "@shared/contexts";
import { AggregateRoot, Email, IDomainError, Name, Result, UniqueID } from "@shared/contexts";
import { AuthUserRole } from "./AuthUserRole";
export interface IAuthUserProps {
name: Name;
email: Email;
password: Password;
roles: AuthUserRole[];
}
export interface IAuthUser {
@ -21,18 +16,13 @@ export interface IAuthUser {
password: Password;
isUser: boolean;
isAdmin: boolean;
getRoles: () => AuthUserRole[];
verifyPassword: (candidatePassword: string) => boolean;
}
export class AuthUser
extends AggregateRoot<IAuthUserProps>
implements IAuthUser
{
public static create(
props: IAuthUserProps,
id?: UniqueID,
): Result<AuthUser, IDomainError> {
export class AuthUser extends AggregateRoot<IAuthUserProps> implements IAuthUser {
public static create(props: IAuthUserProps, id?: UniqueID): Result<AuthUser, IDomainError> {
const user = new AuthUser(props, id);
return Result.ok<AuthUser>(user);
}
@ -41,6 +31,14 @@ export class AuthUser
return Password.hashPassword(password);
}
private roles: AuthUserRole[];
constructor(props: IAuthUserProps, id?: UniqueID) {
const { roles } = props;
super(props, id);
this.roles = roles;
}
get name(): Name {
return this.props.name;
}
@ -54,14 +52,22 @@ export class AuthUser
}
get isUser(): boolean {
return true;
return this._hasRole(AuthUserRole.ROLE_USER);
}
get isAdmin(): boolean {
return true;
return this._hasRole(AuthUserRole.ROLE_ADMIN);
}
public getRoles(): AuthUserRole[] {
return this.roles;
}
public verifyPassword(candidatePassword: string): boolean {
return this.props.password.verifyPassword(candidatePassword);
}
private _hasRole(role: AuthUserRole): boolean {
return this.roles.some((r) => r.equals(role));
}
}

View File

@ -0,0 +1,21 @@
import { DomainError, Result, ValueObject, handleDomainError } from "@shared/contexts";
export class AuthUserRole extends ValueObject<string> {
static ROLE_ADMIN = new AuthUserRole("ROLE_ADMIN");
static ROLE_USER = new AuthUserRole("ROLE_USER");
public static create(value: string) {
switch (value) {
case "ROLE_ADMIN":
return Result.ok(AuthUserRole.ROLE_ADMIN);
case "ROLE_USER":
return Result.ok(AuthUserRole.ROLE_USER);
default:
return Result.fail(handleDomainError(DomainError.INVALID_INPUT_DATA));
}
}
public toPrimitive(): string {
return this.toString();
}
}

View File

@ -1 +1,2 @@
export * from "./AuthUser";
export * from "./AuthUserRole";

View File

@ -21,7 +21,7 @@ export const loginPresenter: ILoginPresenter = {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
roles: ["ROLE_USER"],
roles: user.getRoles().map((rol) => rol.toString()),
token,
refresh_token: refreshToken,
};

View File

@ -6,7 +6,7 @@ import {
import { Name, UniqueID } from "@shared/contexts";
import { Dealer, IDealerProps } from "../../domain/entities";
import { ISalesContext } from "../Sales.context";
import { DealerCreationAttributes, DealerStatus, Dealer_Model } from "../sequelize";
import { DEALER_STATUS, DealerCreationAttributes, Dealer_Model } from "../sequelize";
export interface IDealerMapper
extends ISequelizeMapper<Dealer_Model, DealerCreationAttributes, Dealer> {}
@ -44,7 +44,7 @@ class DealerMapper
default_notes: "",
default_legal_terms: "",
default_quote_validity: "",
status: DealerStatus.ACTIVE,
status: DEALER_STATUS.STATUS_ACTIVE,
};
}
}

View File

@ -8,9 +8,9 @@ import {
} from "sequelize";
import { Quote_Model } from "./quote.model";
export enum DealerStatus {
ACTIVE = "active",
BLOCKED = "blocked",
export enum DEALER_STATUS {
STATUS_ACTIVE = "active",
STATUS_BLOCKED = "blocked",
}
export type DealerCreationAttributes = InferCreationAttributes<Dealer_Model>;
@ -49,7 +49,7 @@ export class Dealer_Model extends Model<
declare default_notes: string;
declare default_legal_terms: string;
declare default_quote_validity: string;
declare status: DealerStatus;
declare status: DEALER_STATUS;
}
export default (sequelize: Sequelize) => {
@ -77,7 +77,7 @@ export default (sequelize: Sequelize) => {
default_quote_validity: DataTypes.STRING,
status: {
type: DataTypes.ENUM(...Object.values(DealerStatus)),
type: DataTypes.ENUM(...Object.values(DEALER_STATUS)),
allowNull: false,
},
},

View File

@ -1,8 +1,4 @@
import {
IUseCase,
IUseCaseError,
UseCaseError,
} from "@/contexts/common/application";
import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application";
import { IRepositoryManager, Password } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
@ -18,23 +14,19 @@ import {
ensureIdIsValid,
ensureNameIsValid,
} from "@shared/contexts";
import { IUserRepository, User } from "../domain";
import { IUserRepository, User, UserRole } from "../domain";
export type CreateUserResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<User, never>; // Success!
export class CreateUserUseCase
implements
IUseCase<ICreateUser_Request_DTO, Promise<CreateUserResponseOrError>>
implements IUseCase<ICreateUser_Request_DTO, Promise<CreateUserResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: {
adapter: ISequelizeAdapter;
repositoryManager: IRepositoryManager;
}) {
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
@ -47,9 +39,7 @@ export class CreateUserUseCase
if (idOrError.isFailure) {
const message = idOrError.error.message; //`User ID ${userDTO.id} is not valid`;
return Result.fail(
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [
{ path: "id" },
]),
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "id" }])
);
}
@ -57,9 +47,7 @@ export class CreateUserUseCase
if (nameOrError.isFailure) {
const message = nameOrError.error.message; //`User ID ${userDTO.id} is not valid`;
return Result.fail(
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [
{ path: "name" },
]),
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "name" }])
);
}
@ -67,9 +55,7 @@ export class CreateUserUseCase
if (emailOrError.isFailure) {
const message = emailOrError.error.message;
return Result.fail(
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [
{ path: "email" },
]),
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "email" }])
);
}
@ -82,32 +68,28 @@ export class CreateUserUseCase
return Result.fail(
UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
path: "id",
}),
})
);
}
const nameExists = await userRepository().existsUserWithName(
nameOrError.object,
);
const nameExists = await userRepository().existsUserWithName(nameOrError.object);
if (nameExists) {
const message = `Another user with same name exists`;
return Result.fail(
UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
path: "name",
}),
})
);
}
const emailExists = await userRepository().existsUserWithEmail(
emailOrError.object,
);
const emailExists = await userRepository().existsUserWithEmail(emailOrError.object);
if (emailExists) {
const message = `Another user with same email exists`;
return Result.fail(
UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
path: "email",
}),
})
);
}
@ -157,15 +139,13 @@ export class CreateUserUseCase
return Result.ok<User>(user);
} catch (error: unknown) {
const _error = error as IInfrastructureError;
return Result.fail(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message),
);
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
}
}
private _tryCreateUserInstance(
userDTO: ICreateUser_Request_DTO,
userId: UniqueID,
userId: UniqueID
): Result<User, IDomainError> {
const nameOrError = Name.create(userDTO.name);
if (nameOrError.isFailure) {
@ -177,9 +157,7 @@ export class CreateUserUseCase
return Result.fail(emailOrError.error);
}
const passwordOrError = Password.createFromPlainTextPassword(
userDTO.password,
);
const passwordOrError = Password.createFromPlainTextPassword(userDTO.password);
if (passwordOrError.isFailure) {
return Result.fail(passwordOrError.error);
}
@ -189,8 +167,9 @@ export class CreateUserUseCase
name: nameOrError.object,
email: emailOrError.object,
password: passwordOrError.object,
roles: [UserRole.ROLE_USER],
},
userId,
userId
);
}

View File

@ -16,7 +16,7 @@ import {
Result,
UniqueID,
} from "@shared/contexts";
import { IUserRepository, User } from "../domain";
import { IUserRepository, User, UserRole } from "../domain";
export interface IUpdateUserUseCaseRequest extends IUseCaseRequest {
id: UniqueID;
@ -28,23 +28,17 @@ export type UpdateUserResponseOrError =
| Result<User, never>; // Success!
export class UpdateUserUseCase
implements
IUseCase<IUpdateUserUseCaseRequest, Promise<UpdateUserResponseOrError>>
implements IUseCase<IUpdateUserUseCaseRequest, Promise<UpdateUserResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: {
adapter: ISequelizeAdapter;
repositoryManager: IRepositoryManager;
}) {
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
async execute(
request: IUpdateUserUseCaseRequest,
): Promise<UpdateUserResponseOrError> {
async execute(request: IUpdateUserUseCaseRequest): Promise<UpdateUserResponseOrError> {
const { id, userDTO } = request;
const userRepository = this._getUserRepository();
@ -55,7 +49,7 @@ export class UpdateUserUseCase
return Result.fail(
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, {
path: "id",
}),
})
);
}
@ -106,15 +100,13 @@ export class UpdateUserUseCase
return Result.ok<User>(user);
} catch (error: unknown) {
const _error = error as IInfrastructureError;
return Result.fail(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message),
);
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
}
}
private _tryCreateUserInstance(
userDTO: IUpdateUser_Request_DTO,
userId: UniqueID,
userId: UniqueID
): Result<User, IDomainError> {
const nameOrError = Name.create(userDTO.name);
if (nameOrError.isFailure) {
@ -126,9 +118,7 @@ export class UpdateUserUseCase
return Result.fail(emailOrError.error);
}
const passwordOrError = Password.createFromPlainTextPassword(
userDTO.password,
);
const passwordOrError = Password.createFromPlainTextPassword(userDTO.password);
if (passwordOrError.isFailure) {
return Result.fail(passwordOrError.error);
}
@ -138,8 +128,9 @@ export class UpdateUserUseCase
name: nameOrError.object,
email: emailOrError.object,
password: passwordOrError.object,
roles: [UserRole.ROLE_USER],
},
userId,
userId
);
}

View File

@ -9,14 +9,16 @@ import {
handleDomainError,
} from "@shared/contexts";
import { UserHasName } from "./User.specifications";
import { UserRole } from "./UserRole";
export interface IUserProps {
name: Name;
email: Email;
password: Password;
roles: UserRole[];
}
//type ISecuredUserProps = Omit<IUserProps, "password">;
//type ISecuredUserProps = ;
export interface IUser {
id: UniqueID;
@ -30,10 +32,7 @@ export interface IUser {
export class User extends AggregateRoot<IUserProps> implements IUser {
static readonly ERROR_USER_WITHOUT_NAME = "ERROR_USER_WITHOUT_NAME";
public static create(
props: IUserProps,
id?: UniqueID,
): Result<User, IDomainError> {
public static create(props: IUserProps, id?: UniqueID): Result<User, IDomainError> {
const user = new User(props, id);
// Reglas de negocio / validaciones
@ -50,6 +49,14 @@ export class User extends AggregateRoot<IUserProps> implements IUser {
return Password.hashPassword(password);
}
private roles: UserRole[];
constructor(props: IUserProps, id?: UniqueID) {
const { roles } = props;
super(props, id);
this.roles = roles;
}
get name(): Name {
return this.props.name;
}
@ -63,10 +70,28 @@ export class User extends AggregateRoot<IUserProps> implements IUser {
}
get isUser(): boolean {
return true;
return this.hasRole(UserRole.ROLE_USER);
}
get isAdmin(): boolean {
return true;
return this.hasRole(UserRole.ROLE_ADMIN);
}
public hasRole(role: UserRole): boolean {
return this.roles.some((r) => r.equals(role));
}
public getRoles(): UserRole[] {
return this.roles;
}
public addRole(role: UserRole): void {
if (!this.roles.some((r) => r.equals(role))) {
this.roles.push(role);
}
}
public removeRole(role: UserRole): void {
this.roles = this.roles.filter((r) => !r.equals(role));
}
}

View File

@ -0,0 +1,21 @@
import { DomainError, Result, ValueObject, handleDomainError } from "@shared/contexts";
export class UserRole extends ValueObject<string> {
static ROLE_ADMIN = new UserRole("ROLE_ADMIN");
static ROLE_USER = new UserRole("ROLE_USER");
public static create(value: string) {
switch (value) {
case "ROLE_ADMIN":
return Result.ok(UserRole.ROLE_ADMIN);
case "ROLE_USER":
return Result.ok(UserRole.ROLE_USER);
default:
return Result.fail(handleDomainError(DomainError.INVALID_INPUT_DATA));
}
}
public toPrimitive(): string {
return this.toString();
}
}

View File

@ -1 +1,2 @@
export * from "./User";
export * from "./UserRole";

View File

@ -12,6 +12,7 @@ export const CreateUserPresenter: ICreateUserPresenter = {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
roles: user.getRoles().map((rol) => rol.toString()),
};
},
};

View File

@ -12,6 +12,7 @@ export const GetUserPresenter: IGetUserPresenter = {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
roles: user.getRoles().map((rol) => rol.toString()),
};
},
};

View File

@ -1,10 +1,6 @@
import { User } from "@/contexts/users/domain";
import { IUserContext } from "@/contexts/users/infrastructure/User.context";
import {
ICollection,
IListResponse_DTO,
IListUsers_Response_DTO,
} from "@shared/contexts";
import { ICollection, IListResponse_DTO, IListUsers_Response_DTO } from "@shared/contexts";
export interface IListUsersPresenter {
map: (user: User, context: IUserContext) => IListUsers_Response_DTO;
@ -15,7 +11,7 @@ export interface IListUsersPresenter {
params: {
page: number;
limit: number;
},
}
) => IListResponse_DTO<IListUsers_Response_DTO>;
}
@ -26,6 +22,7 @@ export const listUsersPresenter: IListUsersPresenter = {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
roles: user.getRoles().map((rol) => rol.toString()),
};
},
@ -35,14 +32,12 @@ export const listUsersPresenter: IListUsersPresenter = {
params: {
page: number;
limit: number;
},
}
): IListResponse_DTO<IListUsers_Response_DTO> => {
const { page, limit } = params;
const totalCount = users.totalCount ?? 0;
const items = users.items.map((user: User) =>
listUsersPresenter.map(user, context),
);
const items = users.items.map((user: User) => listUsersPresenter.map(user, context));
const result = {
page,

View File

@ -12,6 +12,7 @@ export const UpdateUserPresenter: IUpdateUserPresenter = {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
roles: user.getRoles().map((rol) => rol.toString()),
};
},
};

View File

@ -1,15 +1,11 @@
import { Password } from "@/contexts/common/domain";
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { Email, Name, UniqueID } from "@shared/contexts";
import { IUserProps, User } from "../../domain";
import { IUserProps, User, UserRole } from "../../domain";
import { IUserContext } from "../User.context";
import { UserCreationAttributes, User_Model } from "../sequelize/user.model";
export interface IUserMapper
extends ISequelizeMapper<User_Model, UserCreationAttributes, User> {}
export interface IUserMapper extends ISequelizeMapper<User_Model, UserCreationAttributes, User> {}
class UserMapper
extends SequelizeMapper<User_Model, UserCreationAttributes, User>
@ -23,11 +19,8 @@ class UserMapper
const props: IUserProps = {
name: this.mapsValue(source, "name", Name.create),
email: this.mapsValue(source, "email", Email.create),
password: this.mapsValue(
source,
"password",
Password.createFromHashedTextPassword,
),
password: this.mapsValue(source, "password", Password.createFromHashedTextPassword),
roles: source.roles.map((rol) => UserRole.create(rol).object),
};
const id = this.mapsValue(source, "id", UniqueID.create);
@ -40,15 +33,13 @@ class UserMapper
return userOrError.object;
}
protected toPersistenceMappingImpl(
source: User,
params?: Record<string, any> | undefined,
) {
protected toPersistenceMappingImpl(source: User, params?: Record<string, any> | undefined) {
return {
id: source.id.toPrimitive(),
name: source.name.toPrimitive(),
email: source.email.toPrimitive(),
password: source.password.toPrimitive(),
roles: source.getRoles().map((rol) => rol.toPrimitive()),
};
}
}

View File

@ -7,6 +7,11 @@ import {
Sequelize,
} from "sequelize";
export enum USER_ROLES {
ROLE_ADMIN = "ROLE_ADMIN",
ROLE_USER = "ROLE_USER",
}
export type UserCreationAttributes = InferCreationAttributes<User_Model>;
export class User_Model extends Model<
@ -33,6 +38,7 @@ export class User_Model extends Model<
declare name: string;
declare email: string;
declare password: string;
declare roles: string[];
}
export default (sequelize: Sequelize) => {
@ -56,6 +62,18 @@ export default (sequelize: Sequelize) => {
type: DataTypes.STRING,
allowNull: false,
},
roles: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: USER_ROLES.ROLE_USER,
get(this: User_Model): string[] {
return this.getDataValue("roles").split(";");
},
set(this: User_Model, value: string[]) {
this.setDataValue("roles", value.join(";"));
},
},
},
{
sequelize,

View File

@ -25,7 +25,7 @@ export const currentState = assign(
environment: config.enviroment,
connections: {},
},
config,
config
);
const serverStop = (server: http.Server) => {
@ -35,9 +35,7 @@ const serverStop = (server: http.Server) => {
logger.warn("⚡️ Shutting down server");
setTimeout(() => {
logger.error(
"Could not close connections in time, forcefully shutting down",
);
logger.error("Could not close connections in time, forcefully shutting down");
resolve();
}, forceTimeout).unref();
@ -67,9 +65,7 @@ const serverStop = (server: http.Server) => {
const serverError = (error: any) => {
if (error.code === "EADDRINUSE") {
logger.debug(`⛔️ Server wasn't able to start properly.`);
logger.error(
`The port ${error.port} is already used by another application.`,
);
logger.error(`The port ${error.port} is already used by another application.`);
} else {
logger.debug(`⛔️ Server wasn't able to start properly.`);
logger.error(error);
@ -99,12 +95,10 @@ const server: http.Server = http
process.on("SIGINT", () => {
//firebirdConn.disconnect();
serverStop(server);
}),
})
)
.on("close", () =>
logger.info(
`Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`,
),
logger.info(`Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`)
)
.on("connection", serverConnection)
.on("error", serverError);
@ -115,18 +109,12 @@ try {
// Launch server
server.listen(currentState.server.port, () => {
const now = DateTime.now();
logger.info(
`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`,
);
logger.info(
`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`,
);
logger.info(`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`);
logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`);
logger.info(`Environment: ${currentState.environment}`);
logger.info(`Process PID: ${process.pid}`);
logger.info("To shut down your server, press <CTRL> + C at any time");
logger.info(
`⚡️ Server: http://${currentState.server.hostname}:${currentState.server.port}`,
);
logger.info(`⚡️ Server: http://${currentState.server.hostname}:${currentState.server.port}`);
});
});
//});

View File

@ -2,4 +2,5 @@ export interface ICreateUser_Response_DTO {
id: string;
name: string;
email: string;
roles: string[];
}

View File

@ -2,4 +2,5 @@ export interface IGetUserResponse_DTO {
id: string;
name: string;
email: string;
roles: string[];
}

View File

@ -2,4 +2,5 @@ export interface IListUsers_Response_DTO {
id: string;
name: string;
email: string;
roles: string[];
}

View File

@ -2,4 +2,5 @@ export interface IUpdateUser_Response_DTO {
id: string;
name: string;
email: string;
roles: string[];
}