From 84babaeee8bd589858d2f97db78afd4de13f0213 Mon Sep 17 00:00:00 2001 From: Ha Minh Chien Date: Mon, 22 Jan 2024 15:55:27 +0700 Subject: [PATCH] impl user and auth --- .../src/domain/entity/auth/action.ts | 4 +- .../src/domain/entity/auth/index.ts | 2 - .../src/domain/entity/auth/subject.ts | 3 + .../src/domain/entity/bio-product/auth.ts | 1 - .../src/domain/entity/branch/auth.ts | 1 - .../src/domain/entity/index.ts | 1 + .../{auth => permission-rule}/entity.ts | 4 +- .../{auth => permission-rule}/example.ts | 11 +-- .../domain/entity/permission-rule/index.ts | 2 + .../src/domain/entity/role/entity.ts | 2 +- .../src/domain/entity/user/auth.ts | 15 ++++ .../src/domain/entity/user/entity.ts | 10 ++- .../src/domain/entity/user/example.ts | 17 +++-- .../src/domain/entity/user/index.ts | 1 + .../domain/use-case/auth/populate-context.ts | 54 +++----------- .../domain/use-case/bio-product/find-one.ts | 8 +- .../src/domain/use-case/branch/find-one.ts | 9 ++- .../src/domain/use-case/index.ts | 7 ++ .../src/domain/use-case/module.ts | 15 +++- .../src/domain/use-case/role/find-one.ts | 10 ++- .../src/domain/use-case/user/assert-exists.ts | 27 +++++++ .../src/domain/use-case/user/create.ts | 51 +++++++++++++ .../src/domain/use-case/user/delete.ts | 40 ++++++++++ .../src/domain/use-case/user/find-one.ts | 31 +++++++- .../src/domain/use-case/user/search.ts | 41 +++++++++++ .../src/domain/use-case/user/update.ts | 64 ++++++++++++++++ .../src/infrastructure/mongo/auth/index.ts | 1 + .../src/infrastructure/mongo/auth/schema.ts | 29 ++++++++ .../src/infrastructure/mongo/role/schema.ts | 32 +------- .../src/infrastructure/mongo/user/schema.ts | 24 +++++- .../presentation/http/v1/auth/controller.ts | 10 +-- ...{login.request.ts => login.request-dto.ts} | 2 +- .../http/v1/auth/dto/login.response-dto.ts | 3 + .../http/v1/auth/dto/login.response.ts | 3 - .../http/v1/auth/dto/me.response-dto.ts | 18 +++++ .../http/v1/auth/dto/me.response.ts | 7 -- ....request-dto.ts => permission-rule.dto.ts} | 0 .../src/presentation/http/v1/auth/routes.ts | 22 ++---- .../src/presentation/http/v1/module.ts | 2 + .../http/v1/role/dto/create.request-dto.ts | 2 +- .../presentation/http/v1/user/controller.ts | 64 ++++++++++++++++ .../http/v1/user/dto/create.request-dto.ts | 54 ++++++++++++++ .../http/v1/user/dto/response-dto.ts | 28 +++++++ .../http/v1/user/dto/search.request-dto.ts | 5 ++ .../http/v1/user/dto/search.response-dto.ts | 5 ++ .../http/v1/user/dto/update.request-dto.ts | 5 ++ .../http/v1/user/dto/user.response.ts | 27 ------- .../src/presentation/http/v1/user/routes.ts | 73 +++++++++++++++++++ 48 files changed, 679 insertions(+), 168 deletions(-) rename apps/hcdc-access-service/src/domain/entity/{auth => permission-rule}/entity.ts (72%) rename apps/hcdc-access-service/src/domain/entity/{auth => permission-rule}/example.ts (54%) create mode 100644 apps/hcdc-access-service/src/domain/entity/permission-rule/index.ts create mode 100644 apps/hcdc-access-service/src/domain/entity/user/auth.ts create mode 100644 apps/hcdc-access-service/src/domain/use-case/user/assert-exists.ts create mode 100644 apps/hcdc-access-service/src/domain/use-case/user/create.ts create mode 100644 apps/hcdc-access-service/src/domain/use-case/user/delete.ts create mode 100644 apps/hcdc-access-service/src/domain/use-case/user/search.ts create mode 100644 apps/hcdc-access-service/src/domain/use-case/user/update.ts create mode 100644 apps/hcdc-access-service/src/infrastructure/mongo/auth/index.ts create mode 100644 apps/hcdc-access-service/src/infrastructure/mongo/auth/schema.ts rename apps/hcdc-access-service/src/presentation/http/v1/auth/dto/{login.request.ts => login.request-dto.ts} (88%) create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response-dto.ts delete mode 100644 apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response.ts create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response-dto.ts delete mode 100644 apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response.ts rename apps/hcdc-access-service/src/presentation/http/v1/auth/dto/{permission-rule.request-dto.ts => permission-rule.dto.ts} (100%) create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/controller.ts create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/dto/create.request-dto.ts create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/dto/response-dto.ts create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.request-dto.ts create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.response-dto.ts create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/dto/update.request-dto.ts delete mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/dto/user.response.ts create mode 100644 apps/hcdc-access-service/src/presentation/http/v1/user/routes.ts diff --git a/apps/hcdc-access-service/src/domain/entity/auth/action.ts b/apps/hcdc-access-service/src/domain/entity/auth/action.ts index def388c3..5fe2e190 100644 --- a/apps/hcdc-access-service/src/domain/entity/auth/action.ts +++ b/apps/hcdc-access-service/src/domain/entity/auth/action.ts @@ -7,15 +7,17 @@ import { BioProductAction } from '../bio-product/auth' import { TestCategoryAction } from '../test-category/auth' import { BranchAction } from '../branch/auth' import { RoleAction } from '../role/auth' +import { UserAction } from '../user/auth' export const AuthAction = { BioProduct: stringEnumValues(BioProductAction), TestCategory: stringEnumValues(TestCategoryAction), Branch: stringEnumValues(BranchAction), Role: stringEnumValues(RoleAction), + User: stringEnumValues(UserAction), } satisfies Record -export const AuthActionValues = Object.values(AuthAction).flat() +export const AuthActionValues = [...new Set(Object.values(AuthAction).flat())] export type AuthActionUnionType = (typeof AuthAction)[AuthSubjectUnionType][number] diff --git a/apps/hcdc-access-service/src/domain/entity/auth/index.ts b/apps/hcdc-access-service/src/domain/entity/auth/index.ts index e98e86a7..dcbb13b0 100644 --- a/apps/hcdc-access-service/src/domain/entity/auth/index.ts +++ b/apps/hcdc-access-service/src/domain/entity/auth/index.ts @@ -1,5 +1,3 @@ export * from './action' export * from './subject' export * from './utils' -export * from './entity' -export * from './example' diff --git a/apps/hcdc-access-service/src/domain/entity/auth/subject.ts b/apps/hcdc-access-service/src/domain/entity/auth/subject.ts index c0e22cd1..cd8aae11 100644 --- a/apps/hcdc-access-service/src/domain/entity/auth/subject.ts +++ b/apps/hcdc-access-service/src/domain/entity/auth/subject.ts @@ -4,6 +4,7 @@ import { BioProduct } from '../bio-product' import { TestCategory } from '../test-category' import { Branch } from '../branch' import { Role } from '../role' +import { User } from '../user' // key-value must be identical for working with '@casl/mongoose'.accessibleBy() export const AuthSubject = { @@ -11,6 +12,7 @@ export const AuthSubject = { TestCategory: 'TestCategory', Branch: 'Branch', Role: 'Role', + User: 'User', } satisfies Record export type AuthSubjectUnionType = keyof typeof AuthSubject @@ -24,4 +26,5 @@ export type SubjectEntityMapping = { TestCategory: TestCategory Branch: Branch Role: Role + User: User } diff --git a/apps/hcdc-access-service/src/domain/entity/bio-product/auth.ts b/apps/hcdc-access-service/src/domain/entity/bio-product/auth.ts index 53117bcd..f06a229a 100644 --- a/apps/hcdc-access-service/src/domain/entity/bio-product/auth.ts +++ b/apps/hcdc-access-service/src/domain/entity/bio-product/auth.ts @@ -1,7 +1,6 @@ import '@casl/mongoose' export enum BioProductAction { - Manage = 'Manage', Create = 'Create', Read = 'Read', Update = 'Update', diff --git a/apps/hcdc-access-service/src/domain/entity/branch/auth.ts b/apps/hcdc-access-service/src/domain/entity/branch/auth.ts index 46f5b023..176dd473 100644 --- a/apps/hcdc-access-service/src/domain/entity/branch/auth.ts +++ b/apps/hcdc-access-service/src/domain/entity/branch/auth.ts @@ -1,7 +1,6 @@ import '@casl/mongoose' export enum BranchAction { - Manage = 'Manage', Create = 'Create', Read = 'Read', Update = 'Update', diff --git a/apps/hcdc-access-service/src/domain/entity/index.ts b/apps/hcdc-access-service/src/domain/entity/index.ts index cf7085d0..d55c485c 100644 --- a/apps/hcdc-access-service/src/domain/entity/index.ts +++ b/apps/hcdc-access-service/src/domain/entity/index.ts @@ -6,3 +6,4 @@ export * from './test-category' export * from './role' export * from './user' export * from './branch' +export * from './permission-rule' diff --git a/apps/hcdc-access-service/src/domain/entity/auth/entity.ts b/apps/hcdc-access-service/src/domain/entity/permission-rule/entity.ts similarity index 72% rename from apps/hcdc-access-service/src/domain/entity/auth/entity.ts rename to apps/hcdc-access-service/src/domain/entity/permission-rule/entity.ts index f25e13ba..b47f1cd1 100644 --- a/apps/hcdc-access-service/src/domain/entity/auth/entity.ts +++ b/apps/hcdc-access-service/src/domain/entity/permission-rule/entity.ts @@ -1,7 +1,7 @@ import { MongoQuery } from '@casl/ability' -import { AuthSubject, SubjectEntityMapping } from './subject' -import { AuthAction } from './action' +import { AuthSubject, SubjectEntityMapping } from '../auth/subject' +import { AuthAction } from '../auth/action' export type PermissionRule< TSubject extends keyof typeof AuthSubject = keyof typeof AuthSubject, diff --git a/apps/hcdc-access-service/src/domain/entity/auth/example.ts b/apps/hcdc-access-service/src/domain/entity/permission-rule/example.ts similarity index 54% rename from apps/hcdc-access-service/src/domain/entity/auth/example.ts rename to apps/hcdc-access-service/src/domain/entity/permission-rule/example.ts index 3e3b7bc7..c5410e80 100644 --- a/apps/hcdc-access-service/src/domain/entity/auth/example.ts +++ b/apps/hcdc-access-service/src/domain/entity/permission-rule/example.ts @@ -1,25 +1,20 @@ -import { exampleMongoObjectId } from '@diut/nest-core' - import { EntityDataExample } from '../base-entity' -import { AuthActionValues } from './action' +import { AuthActionValues } from '../auth/action' import { PermissionRule } from './entity' -import { AuthSubjectValues } from './subject' +import { AuthSubjectValues } from '../auth/subject' export const examplePermissionRule = { subject: { - example: AuthSubjectValues[0], enum: AuthSubjectValues, }, action: { - example: AuthActionValues[0], enum: AuthActionValues, }, inverted: { example: false, - default: false, required: false, }, conditions: { - example: { _id: { $eq: exampleMongoObjectId.example } }, + example: {}, }, } satisfies EntityDataExample diff --git a/apps/hcdc-access-service/src/domain/entity/permission-rule/index.ts b/apps/hcdc-access-service/src/domain/entity/permission-rule/index.ts new file mode 100644 index 00000000..7ef54e26 --- /dev/null +++ b/apps/hcdc-access-service/src/domain/entity/permission-rule/index.ts @@ -0,0 +1,2 @@ +export * from './entity' +export * from './example' diff --git a/apps/hcdc-access-service/src/domain/entity/role/entity.ts b/apps/hcdc-access-service/src/domain/entity/role/entity.ts index 4166c6be..fe17b3c7 100644 --- a/apps/hcdc-access-service/src/domain/entity/role/entity.ts +++ b/apps/hcdc-access-service/src/domain/entity/role/entity.ts @@ -1,6 +1,6 @@ import { BaseEntity } from '../base-entity' import { Branch } from '../branch' -import { PermissionRule } from '../auth' +import { PermissionRule } from '../permission-rule' export type Role = BaseEntity & { index: number diff --git a/apps/hcdc-access-service/src/domain/entity/user/auth.ts b/apps/hcdc-access-service/src/domain/entity/user/auth.ts new file mode 100644 index 00000000..cbdf7456 --- /dev/null +++ b/apps/hcdc-access-service/src/domain/entity/user/auth.ts @@ -0,0 +1,15 @@ +import '@casl/mongoose' + +export enum UserAction { + Manage = 'Manage', + Create = 'Create', + Read = 'Read', + Update = 'Update', + Delete = 'Delete', +} + +declare module '@casl/mongoose' { + interface RecordTypes { + User: true + } +} diff --git a/apps/hcdc-access-service/src/domain/entity/user/entity.ts b/apps/hcdc-access-service/src/domain/entity/user/entity.ts index 8265e383..623ac1f9 100644 --- a/apps/hcdc-access-service/src/domain/entity/user/entity.ts +++ b/apps/hcdc-access-service/src/domain/entity/user/entity.ts @@ -1,7 +1,7 @@ +import { PermissionRule } from '../permission-rule' import { BaseEntity } from '../base-entity' import { Branch } from '../branch' -// import { Permission } from './permission' -// import { Role } from './role' +import { Role } from '../role' export type User = BaseEntity & { username: string @@ -10,9 +10,11 @@ export type User = BaseEntity & { name: string phoneNumber: string + inlinePermissions: PermissionRule[] + branchIds: string[] branches?: (Branch | null)[] - // roles: string[] | Role[] - // inlinePermissions: string[] | Permission[] + roleIds: string[] + roles?: (Role | null)[] } diff --git a/apps/hcdc-access-service/src/domain/entity/user/example.ts b/apps/hcdc-access-service/src/domain/entity/user/example.ts index 01ce0420..0c56413b 100644 --- a/apps/hcdc-access-service/src/domain/entity/user/example.ts +++ b/apps/hcdc-access-service/src/domain/entity/user/example.ts @@ -1,25 +1,32 @@ import { exampleMongoObjectIds } from '@diut/nest-core' -import { EntityDataExample, extractExampleEntity } from '../base-entity' +import { EntityDataExample } from '../base-entity' import { User } from './entity' -import { exampleBranch } from '../branch' export const exampleUser = { username: { example: 'levana', }, + passwordHash: { + example: 'hashed_password', + }, name: { example: 'Lê Văn A', }, phoneNumber: { example: '1234567890', }, - passwordHash: { - example: 'hashed_password', + inlinePermissions: { + isArray: true, }, branchIds: exampleMongoObjectIds, branches: { - example: [extractExampleEntity(exampleBranch)], + required: false, + isArray: true, + }, + roleIds: exampleMongoObjectIds, + roles: { + required: false, isArray: true, }, } satisfies EntityDataExample diff --git a/apps/hcdc-access-service/src/domain/entity/user/index.ts b/apps/hcdc-access-service/src/domain/entity/user/index.ts index 7ef54e26..3b1b1117 100644 --- a/apps/hcdc-access-service/src/domain/entity/user/index.ts +++ b/apps/hcdc-access-service/src/domain/entity/user/index.ts @@ -1,2 +1,3 @@ export * from './entity' export * from './example' +export * from './auth' diff --git a/apps/hcdc-access-service/src/domain/use-case/auth/populate-context.ts b/apps/hcdc-access-service/src/domain/use-case/auth/populate-context.ts index 60f01de1..fbb41715 100644 --- a/apps/hcdc-access-service/src/domain/use-case/auth/populate-context.ts +++ b/apps/hcdc-access-service/src/domain/use-case/auth/populate-context.ts @@ -1,7 +1,7 @@ import { createMongoAbility } from '@casl/ability' import { Inject } from '@nestjs/common' -import { AuthSubject, BioProductAction } from 'src/domain/entity' +import { PermissionRule, Role } from 'src/domain/entity' import { EAuthnPayloadUserNotFound } from 'src/domain/exception' import { AuthContextData, @@ -19,54 +19,24 @@ export class AuthPopulateContextUseCase { async execute(input: AuthPayload): Promise { const user = await this.userRepository.findOne({ filter: { _id: input.userId }, - populates: [{ path: 'branches' }], + populates: [{ path: 'roles', fields: ['permissions'] as (keyof Role)[] }], }) if (!user) { throw new EAuthnPayloadUserNotFound() } + const rolesPermissions: PermissionRule[] = [] + user.roles?.forEach((role) => { + if (role != null) { + rolesPermissions.push(...role.permissions) + } + }) + const { inlinePermissions } = user + const permissions = [...rolesPermissions, ...inlinePermissions] + // create from user role and direct ability - const ability = createMongoAbility([ - { - action: 'manage', - subject: 'all', - }, - { - action: BioProductAction.Create, - subject: AuthSubject.BioProduct, - conditions: { - index: { - $lt: 1000, - }, - }, - }, - { - action: BioProductAction.Read, - subject: AuthSubject.BioProduct, - conditions: { - index: { - $gt: 0, - }, - }, - }, - { - action: BioProductAction.Update, - subject: AuthSubject.BioProduct, - conditions: { - index: { - $gt: 2, - }, - }, - }, - { - action: BioProductAction.Delete, - subject: AuthSubject.BioProduct, - conditions: { - index: 5, - }, - }, - ]) + const ability = createMongoAbility(permissions) return { user, ability } } diff --git a/apps/hcdc-access-service/src/domain/use-case/bio-product/find-one.ts b/apps/hcdc-access-service/src/domain/use-case/bio-product/find-one.ts index 89291898..2d93bd00 100644 --- a/apps/hcdc-access-service/src/domain/use-case/bio-product/find-one.ts +++ b/apps/hcdc-access-service/src/domain/use-case/bio-product/find-one.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common' import { + AuthSubject, BioProduct, BioProductAction, assertPermission, @@ -28,7 +29,12 @@ export class BioProductFindOneUseCase { const entity = await this.bioProductRepository.findOne(input) if (entity != null) { - assertPermission(ability, 'BioProduct', BioProductAction.Read, entity) + assertPermission( + ability, + AuthSubject.BioProduct, + BioProductAction.Read, + entity, + ) } return entity diff --git a/apps/hcdc-access-service/src/domain/use-case/branch/find-one.ts b/apps/hcdc-access-service/src/domain/use-case/branch/find-one.ts index af3c5a49..fc3d7cdf 100644 --- a/apps/hcdc-access-service/src/domain/use-case/branch/find-one.ts +++ b/apps/hcdc-access-service/src/domain/use-case/branch/find-one.ts @@ -1,6 +1,11 @@ import { Inject, Injectable } from '@nestjs/common' -import { Branch, BranchAction, assertPermission } from 'src/domain/entity' +import { + AuthSubject, + Branch, + BranchAction, + assertPermission, +} from 'src/domain/entity' import { AuthContextToken, BranchRepositoryToken, @@ -24,7 +29,7 @@ export class BranchFindOneUseCase { const entity = await this.branchRepository.findOne(input) if (entity != null) { - assertPermission(ability, 'Branch', BranchAction.Read, entity) + assertPermission(ability, AuthSubject.Branch, BranchAction.Read, entity) } return entity diff --git a/apps/hcdc-access-service/src/domain/use-case/index.ts b/apps/hcdc-access-service/src/domain/use-case/index.ts index cb9afc5b..57ccf8f2 100644 --- a/apps/hcdc-access-service/src/domain/use-case/index.ts +++ b/apps/hcdc-access-service/src/domain/use-case/index.ts @@ -26,3 +26,10 @@ export * from './branch/update' export * from './branch/delete' export * from './branch/search' export * from './branch/assert-exists' + +export * from './user/create' +export * from './user/find-one' +export * from './user/update' +export * from './user/delete' +export * from './user/search' +export * from './user/assert-exists' diff --git a/apps/hcdc-access-service/src/domain/use-case/module.ts b/apps/hcdc-access-service/src/domain/use-case/module.ts index 23e38e4f..5ec49bac 100644 --- a/apps/hcdc-access-service/src/domain/use-case/module.ts +++ b/apps/hcdc-access-service/src/domain/use-case/module.ts @@ -21,8 +21,14 @@ import { BranchDeleteUseCase } from './branch/delete' import { BranchSearchUseCase } from './branch/search' import { BranchAssertExistsUseCase } from './branch/assert-exists' -import { AuthMeUseCase } from './auth/me' +import { UserCreateUseCase } from './user/create' +import { UserUpdateUseCase } from './user/update' import { UserFindOneUseCase } from './user/find-one' +import { UserDeleteUseCase } from './user/delete' +import { UserSearchUseCase } from './user/search' +import { UserAssertExistsUseCase } from './user/assert-exists' + +import { AuthMeUseCase } from './auth/me' import { AuthLoginUseCase } from './auth/login' import { AuthPopulateContextUseCase } from './auth/populate-context' @@ -54,5 +60,12 @@ export const useCaseMetadata: ModuleMetadata = { RoleDeleteUseCase, RoleSearchUseCase, RoleAssertExistsUseCase, + + UserCreateUseCase, + UserFindOneUseCase, + UserUpdateUseCase, + UserDeleteUseCase, + UserSearchUseCase, + UserAssertExistsUseCase, ], } diff --git a/apps/hcdc-access-service/src/domain/use-case/role/find-one.ts b/apps/hcdc-access-service/src/domain/use-case/role/find-one.ts index fd87eb4d..f5a51cb5 100644 --- a/apps/hcdc-access-service/src/domain/use-case/role/find-one.ts +++ b/apps/hcdc-access-service/src/domain/use-case/role/find-one.ts @@ -1,6 +1,11 @@ import { Inject, Injectable } from '@nestjs/common' -import { Role, RoleAction, assertPermission } from 'src/domain/entity' +import { + AuthSubject, + Role, + RoleAction, + assertPermission, +} from 'src/domain/entity' import { AuthContextToken, RoleRepositoryToken, @@ -24,8 +29,7 @@ export class RoleFindOneUseCase { const entity = await this.roleRepository.findOne(input) if (entity != null) { - // TODO: check this in assert exists - assertPermission(ability, 'Role', RoleAction.Read, entity) + assertPermission(ability, AuthSubject.Role, RoleAction.Read, entity) } return entity diff --git a/apps/hcdc-access-service/src/domain/use-case/user/assert-exists.ts b/apps/hcdc-access-service/src/domain/use-case/user/assert-exists.ts new file mode 100644 index 00000000..2abc9b3a --- /dev/null +++ b/apps/hcdc-access-service/src/domain/use-case/user/assert-exists.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@nestjs/common' + +import { User } from 'src/domain/entity' +import { EEntityNotFound } from 'src/domain/exception' +import { + UserRepositoryToken, + EntityFindOneOptions, + IUserRepository, +} from 'src/domain/interface' + +@Injectable() +export class UserAssertExistsUseCase { + constructor( + @Inject(UserRepositoryToken) + private readonly userRepository: IUserRepository, + ) {} + + async execute(input: EntityFindOneOptions['filter']) { + const rv = await this.userRepository.findOne({ filter: input }) + + if (rv == null) { + throw new EEntityNotFound(`User ${JSON.stringify(input)}`) + } + + return rv + } +} diff --git a/apps/hcdc-access-service/src/domain/use-case/user/create.ts b/apps/hcdc-access-service/src/domain/use-case/user/create.ts new file mode 100644 index 00000000..13c3a8ed --- /dev/null +++ b/apps/hcdc-access-service/src/domain/use-case/user/create.ts @@ -0,0 +1,51 @@ +import { Inject, Injectable } from '@nestjs/common' +import * as argon2 from 'argon2' + +import { + AuthContextToken, + UserRepositoryToken, + IAuthContext, + IUserRepository, +} from 'src/domain/interface' +import { + AuthSubject, + User, + UserAction, + EntityData, + assertPermission, +} from 'src/domain/entity' +import { BranchAssertExistsUseCase } from '../branch/assert-exists' +import { RoleAssertExistsUseCase } from '../role/assert-exists' + +@Injectable() +export class UserCreateUseCase { + constructor( + @Inject(AuthContextToken) + private readonly authContext: IAuthContext, + @Inject(UserRepositoryToken) + private readonly userRepository: IUserRepository, + private readonly branchAssertExistsUseCase: BranchAssertExistsUseCase, + private readonly roleAssertExistsUseCase: RoleAssertExistsUseCase, + ) {} + + async execute( + input: Omit, 'passwordHash'> & { password: string }, + ) { + const { ability } = this.authContext.getData() + assertPermission(ability, AuthSubject.User, UserAction.Create, input) + + for (const branchId of input.branchIds) { + await this.branchAssertExistsUseCase.execute({ _id: branchId }) + } + + for (const roleId of input.roleIds) { + await this.roleAssertExistsUseCase.execute({ _id: roleId }) + } + + const passwordHash = await argon2.hash(input.password) + + const entity = await this.userRepository.create({ ...input, passwordHash }) + + return entity + } +} diff --git a/apps/hcdc-access-service/src/domain/use-case/user/delete.ts b/apps/hcdc-access-service/src/domain/use-case/user/delete.ts new file mode 100644 index 00000000..8f10d7cd --- /dev/null +++ b/apps/hcdc-access-service/src/domain/use-case/user/delete.ts @@ -0,0 +1,40 @@ +import { Inject, Injectable } from '@nestjs/common' + +import { AuthSubject, UserAction, assertPermission } from 'src/domain/entity' +import { + AuthContextToken, + UserRepositoryToken, + IAuthContext, + IUserRepository, +} from 'src/domain/interface' +import { UserFindOneUseCase } from './find-one' +import { EEntityNotFound } from 'src/domain/exception' + +@Injectable() +export class UserDeleteUseCase { + constructor( + @Inject(AuthContextToken) + private readonly authContext: IAuthContext, + @Inject(UserRepositoryToken) + private readonly userRepository: IUserRepository, + private readonly userFindOneUseCase: UserFindOneUseCase, + ) {} + + async execute(input: { id: string }) { + const { ability } = this.authContext.getData() + + const entity = await this.userFindOneUseCase.execute({ + filter: { _id: input.id }, + }) + + if (entity == null) { + throw new EEntityNotFound(`User ${JSON.stringify(input)}`) + } + + assertPermission(ability, AuthSubject.User, UserAction.Delete, entity) + + await this.userRepository.deleteById(input.id) + + return entity + } +} diff --git a/apps/hcdc-access-service/src/domain/use-case/user/find-one.ts b/apps/hcdc-access-service/src/domain/use-case/user/find-one.ts index d8eb7267..7d4a2b71 100644 --- a/apps/hcdc-access-service/src/domain/use-case/user/find-one.ts +++ b/apps/hcdc-access-service/src/domain/use-case/user/find-one.ts @@ -1,14 +1,37 @@ -import { Inject } from '@nestjs/common' +import { Inject, Injectable } from '@nestjs/common' -import { IUserRepository, UserRepositoryToken } from 'src/domain/interface' +import { + AuthSubject, + User, + UserAction, + assertPermission, +} from 'src/domain/entity' +import { + AuthContextToken, + UserRepositoryToken, + EntityFindOneOptions, + IAuthContext, + IUserRepository, +} from 'src/domain/interface' +@Injectable() export class UserFindOneUseCase { constructor( @Inject(UserRepositoryToken) private readonly userRepository: IUserRepository, + @Inject(AuthContextToken) + private readonly authContext: IAuthContext, ) {} - async execute(input: Parameters[0]) { - return await this.userRepository.findOne(input) + async execute(input: EntityFindOneOptions) { + const { ability } = this.authContext.getData() + + const entity = await this.userRepository.findOne(input) + + if (entity != null) { + assertPermission(ability, AuthSubject.User, UserAction.Read, entity) + } + + return entity } } diff --git a/apps/hcdc-access-service/src/domain/use-case/user/search.ts b/apps/hcdc-access-service/src/domain/use-case/user/search.ts new file mode 100644 index 00000000..e66fe320 --- /dev/null +++ b/apps/hcdc-access-service/src/domain/use-case/user/search.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common' +import { accessibleBy } from '@casl/mongoose' + +import { + AuthContextToken, + UserRepositoryToken, + IAuthContext, + IUserRepository, + EntitySearchOptions, +} from 'src/domain/interface' +import { + AuthSubject, + User, + UserAction, + assertPermission, +} from 'src/domain/entity' + +@Injectable() +export class UserSearchUseCase { + constructor( + @Inject(UserRepositoryToken) + private readonly userRepository: IUserRepository, + @Inject(AuthContextToken) + private readonly authContext: IAuthContext, + ) {} + + async execute(input: EntitySearchOptions) { + const { ability } = this.authContext.getData() + + assertPermission(ability, AuthSubject.User, UserAction.Read) + + const paginationResult = await this.userRepository.search({ + ...input, + filter: { + $and: [input.filter ?? {}, accessibleBy(ability, UserAction.Read).User], + }, + }) + + return paginationResult + } +} diff --git a/apps/hcdc-access-service/src/domain/use-case/user/update.ts b/apps/hcdc-access-service/src/domain/use-case/user/update.ts new file mode 100644 index 00000000..6e8fc306 --- /dev/null +++ b/apps/hcdc-access-service/src/domain/use-case/user/update.ts @@ -0,0 +1,64 @@ +import { Inject, Injectable } from '@nestjs/common' + +import { AuthSubject, UserAction, assertPermission } from 'src/domain/entity' +import { + AuthContextToken, + UserRepositoryToken, + IAuthContext, + IUserRepository, +} from 'src/domain/interface' +import { EEntityNotFound } from 'src/domain/exception' +import { BranchAssertExistsUseCase } from '../branch/assert-exists' +import { RoleAssertExistsUseCase } from '../role/assert-exists' + +type InputFilter = Parameters[0] +type InputData = Parameters[1] +type InputOptions = Parameters[2] + +@Injectable() +export class UserUpdateUseCase { + constructor( + @Inject(UserRepositoryToken) + private readonly userRepository: IUserRepository, + @Inject(AuthContextToken) + private readonly authContext: IAuthContext, + private readonly branchAssertExistsUseCase: BranchAssertExistsUseCase, + private readonly roleAssertExistsUseCase: RoleAssertExistsUseCase, + ) {} + + async execute( + filter: Omit, + data: Omit, + options?: InputOptions, + ) { + const { ability } = this.authContext.getData() + + const entity = await this.userRepository.findOne({ + filter, + }) + + if (entity === null) { + throw new EEntityNotFound(`User ${JSON.stringify(filter)}`) + } + + assertPermission(ability, AuthSubject.User, UserAction.Update, entity) + + if (data?.branchIds?.length! > 0) { + for (const branchId of data.branchIds) { + await this.branchAssertExistsUseCase.execute({ + _id: branchId, + }) + } + } + + if (data?.roleIds?.length! > 0) { + for (const roleId of data.roleIds) { + await this.roleAssertExistsUseCase.execute({ + _id: roleId, + }) + } + } + + return this.userRepository.update(filter, data, options) + } +} diff --git a/apps/hcdc-access-service/src/infrastructure/mongo/auth/index.ts b/apps/hcdc-access-service/src/infrastructure/mongo/auth/index.ts new file mode 100644 index 00000000..cb7cdd48 --- /dev/null +++ b/apps/hcdc-access-service/src/infrastructure/mongo/auth/index.ts @@ -0,0 +1 @@ +export * from './schema' diff --git a/apps/hcdc-access-service/src/infrastructure/mongo/auth/schema.ts b/apps/hcdc-access-service/src/infrastructure/mongo/auth/schema.ts new file mode 100644 index 00000000..8e1eb819 --- /dev/null +++ b/apps/hcdc-access-service/src/infrastructure/mongo/auth/schema.ts @@ -0,0 +1,29 @@ +import { MongoQuery } from '@casl/ability' +import { Prop, Schema } from '@nestjs/mongoose' +import { Schema as MongooseSchema } from 'mongoose' + +import { + AuthActionUnionType, + AuthActionValues, + AuthSubjectUnionType, + AuthSubjectValues, +} from 'src/domain' + +@Schema({ _id: false }) +export class PermissionRuleSchema { + @Prop({ required: true, type: String, enum: AuthSubjectValues }) + subject: AuthSubjectUnionType + + @Prop({ + required: true, + type: String, + enum: AuthActionValues, + }) + action: AuthActionUnionType + + @Prop({}) + inverted?: boolean + + @Prop({ type: MongooseSchema.Types.Mixed }) + conditions?: MongoQuery +} diff --git a/apps/hcdc-access-service/src/infrastructure/mongo/role/schema.ts b/apps/hcdc-access-service/src/infrastructure/mongo/role/schema.ts index be3362e8..9d188fa9 100644 --- a/apps/hcdc-access-service/src/infrastructure/mongo/role/schema.ts +++ b/apps/hcdc-access-service/src/infrastructure/mongo/role/schema.ts @@ -1,36 +1,11 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { BaseSchema, baseSchemaOptions } from '@diut/nest-core' -import { Types, Schema as MongooseSchema } from 'mongoose' -import { MongoQuery } from '@casl/ability' +import { Types } from 'mongoose' import { COLLECTION } from '../collections' -import { - AuthActionUnionType, - AuthActionValues, - AuthSubjectUnionType, - AuthSubjectValues, - PermissionRule, -} from 'src/domain' +import { PermissionRule } from 'src/domain' import { BranchSchema } from '../branch' - -@Schema({ _id: false }) -export class PermissionRuleSchema { - @Prop({ required: true, type: String, enum: AuthSubjectValues }) - subject: AuthSubjectUnionType - - @Prop({ - required: true, - type: String, - enum: AuthActionValues, - }) - action: AuthActionUnionType - - @Prop({}) - inverted?: boolean - - @Prop({ type: MongooseSchema.Types.Mixed }) - conditions?: MongoQuery -} +import { PermissionRuleSchema } from '../auth' @Schema({ ...baseSchemaOptions, @@ -64,6 +39,5 @@ export class RoleSchema extends BaseSchema { @Prop({ required: true, type: [Types.ObjectId] }) branchIds: string[] - branches?: (BranchSchema | null)[] } diff --git a/apps/hcdc-access-service/src/infrastructure/mongo/user/schema.ts b/apps/hcdc-access-service/src/infrastructure/mongo/user/schema.ts index 57e26a46..69474892 100644 --- a/apps/hcdc-access-service/src/infrastructure/mongo/user/schema.ts +++ b/apps/hcdc-access-service/src/infrastructure/mongo/user/schema.ts @@ -1,9 +1,12 @@ -import { Prop, Schema } from '@nestjs/mongoose' +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { BaseSchema, baseSchemaOptions } from '@diut/nest-core' import { Types } from 'mongoose' import { COLLECTION } from '../collections' import { BranchSchema } from '../branch' +import { PermissionRuleSchema } from '../auth' +import { PermissionRule } from 'src/domain' +import { RoleSchema } from '../role' @Schema({ ...baseSchemaOptions, @@ -17,6 +20,14 @@ import { BranchSchema } from '../branch' justOne: false, }, }, + roles: { + options: { + ref: RoleSchema.name, + localField: 'roleIds', + foreignField: '_id', + justOne: false, + }, + }, }, }) export class UserSchema extends BaseSchema { @@ -32,8 +43,17 @@ export class UserSchema extends BaseSchema { @Prop({ required: true }) phoneNumber: string + @Prop({ + required: true, + type: [SchemaFactory.createForClass(PermissionRuleSchema)], + }) + inlinePermissions: PermissionRule[] + @Prop({ required: true, type: [Types.ObjectId] }) branchIds: string[] - branches?: (BranchSchema | null)[] + + @Prop({ required: true, type: [Types.ObjectId] }) + roleIds: string[] + roles?: (RoleSchema | null)[] } diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/controller.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/controller.ts index 8dc3f053..485c3168 100644 --- a/apps/hcdc-access-service/src/presentation/http/v1/auth/controller.ts +++ b/apps/hcdc-access-service/src/presentation/http/v1/auth/controller.ts @@ -2,10 +2,8 @@ import { Body, Res } from '@nestjs/common' import { Response } from 'express' import { AuthMeUseCase, AuthLoginUseCase } from 'src/domain' -import { AuthLoginRequestDto } from './dto/login.request' +import { AuthLoginRequestDto } from './dto/login.request-dto' import { authRoutes } from './routes' -import { LoginResponseDto } from './dto/login.response' -import { AuthMeResponseDto } from './dto/me.response' import { AuthCookieService, HttpController, @@ -13,7 +11,7 @@ import { HttpRoute, } from '../../common' -@HttpController(authRoutes.controller) +@HttpController({ basePath: 'v1/auth' }) export class AuthController { constructor( private authMeUseCase: AuthMeUseCase, @@ -26,7 +24,7 @@ export class AuthController { async login( @Body() body: AuthLoginRequestDto, @Res({ passthrough: true }) res: Response, - ): Promise { + ) { const { user, accessToken } = await this.authLoginUseCase.execute(body) this.authCookieService.setAuthCookie(res, { accessToken }) @@ -35,7 +33,7 @@ export class AuthController { } @HttpRoute(authRoutes.me) - me(): AuthMeResponseDto { + me() { return this.authMeUseCase.execute() } diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.request.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.request-dto.ts similarity index 88% rename from apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.request.ts rename to apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.request-dto.ts index 4fa03f12..40866b1c 100644 --- a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.request.ts +++ b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.request-dto.ts @@ -9,7 +9,7 @@ export class AuthLoginRequestDto { @IsNotEmpty() username: string - @ApiProperty(exampleUser.passwordHash) + @ApiProperty({ example: 'password' }) @IsString() @IsNotEmpty() password: string diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response-dto.ts new file mode 100644 index 00000000..e197b108 --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response-dto.ts @@ -0,0 +1,3 @@ +import { UserResponseDto } from '../../user/dto/response-dto' + +export class LoginResponseDto extends UserResponseDto {} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response.ts deleted file mode 100644 index 3deb1d4f..00000000 --- a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/login.response.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UserResponseDto } from '../../user/dto/user.response' - -export class LoginResponseDto extends UserResponseDto {} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response-dto.ts new file mode 100644 index 00000000..c0a50d62 --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response-dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Expose, Type } from 'class-transformer' +import { ValidateNested } from 'class-validator' +import { MongoAbility } from '@casl/ability' + +import { UserResponseDto } from '../../user/dto/response-dto' + +export class AuthMeResponseDto { + @Expose() + @ApiProperty({ type: () => UserResponseDto }) + @ValidateNested({ each: true }) + @Type(() => UserResponseDto) + user: UserResponseDto + + @Expose() + @ApiProperty() + ability: MongoAbility +} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response.ts deleted file mode 100644 index 306ca68d..00000000 --- a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/me.response.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Type } from 'class-transformer' -import { User } from 'src/domain' - -export class AuthMeResponseDto { - // @Type(() => ) - user: User -} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/permission-rule.request-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/dto/permission-rule.dto.ts similarity index 100% rename from apps/hcdc-access-service/src/presentation/http/v1/auth/dto/permission-rule.request-dto.ts rename to apps/hcdc-access-service/src/presentation/http/v1/auth/dto/permission-rule.dto.ts diff --git a/apps/hcdc-access-service/src/presentation/http/v1/auth/routes.ts b/apps/hcdc-access-service/src/presentation/http/v1/auth/routes.ts index f873a754..16ba89be 100644 --- a/apps/hcdc-access-service/src/presentation/http/v1/auth/routes.ts +++ b/apps/hcdc-access-service/src/presentation/http/v1/auth/routes.ts @@ -1,17 +1,11 @@ -import { - CustomHttpControllerOptions, - CustomHttpRouteOptions, -} from '@diut/nest-core' +import { CustomHttpRouteOptions } from '@diut/nest-core' import { HttpStatus, RequestMethod } from '@nestjs/common' -import { LoginResponseDto } from './dto/login.response' +import { LoginResponseDto } from './dto/login.response-dto' +import { AuthMeResponseDto } from './dto/me.response-dto' export const authRoutes = { - controller: { - basePath: 'v1/auth', - }, - - login: { + login: { isPublic: true, path: 'login', method: RequestMethod.POST, @@ -26,20 +20,20 @@ export const authRoutes = { }, }, - me: { + me: { path: 'me', openApi: { responses: [ { - type: LoginResponseDto, + type: AuthMeResponseDto, }, ], }, }, - logout: { + logout: { path: 'logout', method: RequestMethod.POST, code: HttpStatus.OK, }, -} +} satisfies Record diff --git a/apps/hcdc-access-service/src/presentation/http/v1/module.ts b/apps/hcdc-access-service/src/presentation/http/v1/module.ts index c5d59729..29b31590 100644 --- a/apps/hcdc-access-service/src/presentation/http/v1/module.ts +++ b/apps/hcdc-access-service/src/presentation/http/v1/module.ts @@ -4,6 +4,7 @@ import { BioProductController } from './bio-product/controller' import { AuthController } from './auth/controller' import { RoleController } from './role/controller' import { BranchController } from './branch/controller' +import { UserController } from './user/controller' export const httpControllerV1Metadata: ModuleMetadata = { controllers: [ @@ -11,6 +12,7 @@ export const httpControllerV1Metadata: ModuleMetadata = { AuthController, RoleController, BranchController, + UserController, ], providers: [], } diff --git a/apps/hcdc-access-service/src/presentation/http/v1/role/dto/create.request-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/role/dto/create.request-dto.ts index 03f3fe86..90876d63 100644 --- a/apps/hcdc-access-service/src/presentation/http/v1/role/dto/create.request-dto.ts +++ b/apps/hcdc-access-service/src/presentation/http/v1/role/dto/create.request-dto.ts @@ -11,7 +11,7 @@ import { import { IsObjectId } from '@diut/nest-core' import { PermissionRule, exampleRole } from 'src/domain' -import { PermissionRuleRequestDto } from '../../auth/dto/permission-rule.request-dto' +import { PermissionRuleRequestDto } from '../../auth/dto/permission-rule.dto' export class RoleCreateRequestDto { @Expose() diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/controller.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/controller.ts new file mode 100644 index 00000000..a32b45f1 --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/user/controller.ts @@ -0,0 +1,64 @@ +import { Body, Param } from '@nestjs/common' +import { ObjectIdPipe } from '@diut/nest-core' + +import { userRoutes } from './routes' +import { + UserCreateUseCase, + UserDeleteUseCase, + UserSearchUseCase, + UserUpdateUseCase, + UserFindOneUseCase, + EEntityNotFound, +} from 'src/domain' +import { UserCreateRequestDto } from './dto/create.request-dto' +import { UserUpdateRequestDto } from './dto/update.request-dto' +import { UserSearchRequestDto } from './dto/search.request-dto' +import { HttpController, HttpRoute } from '../../common' + +@HttpController({ basePath: 'v1/users' }) +export class UserController { + constructor( + private readonly userCreateUseCase: UserCreateUseCase, + private readonly userUpdateUseCase: UserUpdateUseCase, + private readonly userDeleteUseCase: UserDeleteUseCase, + private readonly userSearchUseCase: UserSearchUseCase, + private readonly userFindOneUseCase: UserFindOneUseCase, + ) {} + + @HttpRoute(userRoutes.search) + search(@Body() body: UserSearchRequestDto) { + return this.userSearchUseCase.execute(body) + } + + @HttpRoute(userRoutes.create) + create(@Body() body: UserCreateRequestDto) { + return this.userCreateUseCase.execute(body) + } + + @HttpRoute(userRoutes.findById) + async findById(@Param('id', ObjectIdPipe) id: string) { + const rv = await this.userFindOneUseCase.execute({ + filter: { _id: id }, + populates: [{ path: 'roles' }, { path: 'branches' }], + }) + + if (rv == null) { + throw new EEntityNotFound(`User id=${id}`) + } + + return rv + } + + @HttpRoute(userRoutes.updateById) + updateById( + @Param('id', ObjectIdPipe) id: string, + @Body() body: UserUpdateRequestDto, + ) { + return this.userUpdateUseCase.execute({ _id: id }, body) + } + + @HttpRoute(userRoutes.deleteById) + deleteById(@Param('id', ObjectIdPipe) id: string) { + return this.userDeleteUseCase.execute({ id }) + } +} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/dto/create.request-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/create.request-dto.ts new file mode 100644 index 00000000..422f70a8 --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/create.request-dto.ts @@ -0,0 +1,54 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Expose, Type } from 'class-transformer' +import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator' +import { IsObjectId } from '@diut/nest-core' + +import { PermissionRule, exampleUser } from 'src/domain' +import { PermissionRuleRequestDto } from '../../auth/dto/permission-rule.dto' + +export class UserCreateRequestDto { + @Expose() + @ApiProperty(exampleUser.username) + @IsString() + @IsNotEmpty() + username: string + + @ApiProperty({ example: 'password' }) + @IsString() + @IsNotEmpty() + password: string + + @Expose() + @ApiProperty(exampleUser.name) + @IsString() + @IsNotEmpty() + name: string + + @Expose() + @ApiProperty(exampleUser.phoneNumber) + @IsString() + @IsNotEmpty() + phoneNumber: string + + @Expose() + @ApiProperty({ + ...exampleUser.inlinePermissions, + type: () => PermissionRuleRequestDto, + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PermissionRuleRequestDto) + inlinePermissions: PermissionRule[] + + @Expose() + @ApiProperty(exampleUser.branchIds) + @IsArray() + @IsObjectId({ each: true }) + branchIds: string[] + + @Expose() + @ApiProperty(exampleUser.roleIds) + @IsArray() + @IsObjectId({ each: true }) + roleIds: string[] +} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/dto/response-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/response-dto.ts new file mode 100644 index 00000000..a5d0ddab --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/response-dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty, IntersectionType, OmitType } from '@nestjs/swagger' +import { BaseResourceResponseDto } from '@diut/nest-core' +import { Expose, Type } from 'class-transformer' +import { IsArray, ValidateNested } from 'class-validator' + +import { UserCreateRequestDto } from './create.request-dto' +import { Branch, Role, exampleUser } from 'src/domain' +import { BranchResponseDto } from '../../branch/dto/response-dto' +import { RoleResponseDto } from '../../role/dto/response-dto' + +export class UserResponseDto extends IntersectionType( + BaseResourceResponseDto, + OmitType(UserCreateRequestDto, ['password']), +) { + @Expose() + @ApiProperty({ ...exampleUser.branches, type: () => BranchResponseDto }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => BranchResponseDto) + branches?: (Branch | null)[] + + @Expose() + @ApiProperty({ ...exampleUser.roles, type: () => RoleResponseDto }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => RoleResponseDto) + roles?: (Role | null)[] +} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.request-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.request-dto.ts new file mode 100644 index 00000000..94ca4259 --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.request-dto.ts @@ -0,0 +1,5 @@ +import { SearchRequestDto } from '@diut/nest-core' + +import { User } from 'src/domain' + +export class UserSearchRequestDto extends SearchRequestDto {} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.response-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.response-dto.ts new file mode 100644 index 00000000..91007327 --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/search.response-dto.ts @@ -0,0 +1,5 @@ +import { PaginatedResponse } from '@diut/nest-core' + +import { UserResponseDto } from './response-dto' + +export class UserSearchResponseDto extends PaginatedResponse(UserResponseDto) {} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/dto/update.request-dto.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/update.request-dto.ts new file mode 100644 index 00000000..9d160ea3 --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/update.request-dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/swagger' + +import { UserCreateRequestDto } from './create.request-dto' + +export class UserUpdateRequestDto extends PartialType(UserCreateRequestDto) {} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/dto/user.response.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/dto/user.response.ts deleted file mode 100644 index 4be0d2cd..00000000 --- a/apps/hcdc-access-service/src/presentation/http/v1/user/dto/user.response.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' -import { Expose } from 'class-transformer' -import { IsNotEmpty, IsOptional, IsString } from 'class-validator' -import { BaseResourceResponseDto } from '@diut/nest-core' - -import { exampleUser } from 'src/domain' - -export class UserResponseDto extends BaseResourceResponseDto { - @Expose() - @ApiProperty(exampleUser.username) - @IsString() - @IsNotEmpty() - username: string - - @Expose() - @ApiProperty(exampleUser.name) - @IsString() - @IsNotEmpty() - name: string - - @Expose() - @ApiProperty(exampleUser.phoneNumber) - @IsOptional() - @IsString() - @IsNotEmpty() - phoneNumber?: string -} diff --git a/apps/hcdc-access-service/src/presentation/http/v1/user/routes.ts b/apps/hcdc-access-service/src/presentation/http/v1/user/routes.ts new file mode 100644 index 00000000..2f711eed --- /dev/null +++ b/apps/hcdc-access-service/src/presentation/http/v1/user/routes.ts @@ -0,0 +1,73 @@ +import { HttpStatus, RequestMethod } from '@nestjs/common' +import { CustomHttpRouteOptions } from '@diut/nest-core' + +import { UserSearchResponseDto } from './dto/search.response-dto' +import { UserResponseDto } from './dto/response-dto' + +export const userRoutes = { + search: { + path: 'search', + method: RequestMethod.POST, + code: HttpStatus.OK, + serialize: UserSearchResponseDto, + openApi: { + responses: [ + { + type: UserSearchResponseDto, + }, + ], + }, + }, + + create: { + method: RequestMethod.POST, + serialize: UserResponseDto, + openApi: { + responses: [ + { + type: UserResponseDto, + status: HttpStatus.CREATED, + }, + ], + }, + }, + + updateById: { + path: ':id', + method: RequestMethod.PATCH, + serialize: UserResponseDto, + openApi: { + responses: [ + { + type: UserResponseDto, + }, + ], + }, + }, + + findById: { + path: ':id', + method: RequestMethod.GET, + serialize: UserResponseDto, + openApi: { + responses: [ + { + type: UserResponseDto, + }, + ], + }, + }, + + deleteById: { + path: ':id', + method: RequestMethod.DELETE, + serialize: UserResponseDto, + openApi: { + responses: [ + { + type: UserResponseDto, + }, + ], + }, + }, +} satisfies Record