diff --git a/apps/orchestration/src/store/entity/__test__/e2e/user-wallet.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/user-wallet.spec.ts new file mode 100644 index 000000000..6cc6c288f --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/user-wallet.spec.ts @@ -0,0 +1,100 @@ +import { Action, OrganizationEntity, Signature } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' + +const API_RESOURCE_USER_ENTITY = '/store/user-wallets' + +describe('Wallet Group Store', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const walletId = 'c7eac7d1-7572-4756-b52e-0caebe208364' + + const userId = '2a1509ad-ea87-422e-bebd-974547cd4fee' + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + orgRepository = module.get(OrganizationRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('assigns a wallet to a user', async () => { + const payload = { + authentication, + approvals, + request: { + nonce, + action: Action.ASSIGN_USER_WALLET, + data: { userId, walletId } + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + .send(payload) + + expect(body).toEqual({ + data: { + walletId, + userId + } + }) + expect(status).toEqual(HttpStatus.CREATED) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/core/service/user.service.ts b/apps/orchestration/src/store/entity/core/service/user.service.ts index 48915cb4d..6a5667d2b 100644 --- a/apps/orchestration/src/store/entity/core/service/user.service.ts +++ b/apps/orchestration/src/store/entity/core/service/user.service.ts @@ -1,10 +1,11 @@ -import { AuthCredential, UserEntity, UserRole } from '@narval/authz-shared' +import { AuthCredential, UserEntity, UserRole, UserWallet } from '@narval/authz-shared' import { Injectable } from '@nestjs/common' +import { UserWalletRepository } from '../../persistence/repository/user-wallet.repository' import { UserRepository } from '../../persistence/repository/user.repository' @Injectable() export class UserService { - constructor(private userRepository: UserRepository) {} + constructor(private userRepository: UserRepository, private userWalletRepository: UserWalletRepository) {} create(orgId: string, user: UserEntity, credential?: AuthCredential): Promise { return this.userRepository.create(orgId, user, credential) @@ -20,4 +21,8 @@ export class UserService { role }) } + + async assignWallet(assignment: UserWallet): Promise { + return this.userWalletRepository.assign(assignment) + } } diff --git a/apps/orchestration/src/store/entity/entity-store.module.ts b/apps/orchestration/src/store/entity/entity-store.module.ts index 5876ca1c9..104976969 100644 --- a/apps/orchestration/src/store/entity/entity-store.module.ts +++ b/apps/orchestration/src/store/entity/entity-store.module.ts @@ -10,30 +10,40 @@ import { UserService } from './core/service/user.service' import { WalletService } from './core/service/wallet.service' import { OrganizationController } from './http/rest/controller/organization.controller' import { UserGroupController } from './http/rest/controller/user-group.controller' +import { UserWalletController } from './http/rest/controller/user-wallet.controller' import { UserController } from './http/rest/controller/user.controller' import { WalletGroupController } from './http/rest/controller/wallet-group.controller' import { WalletController } from './http/rest/controller/wallet.controller' import { AuthCredentialRepository } from './persistence/repository/auth-credential.repository' import { OrganizationRepository } from './persistence/repository/organization.repository' import { UserGroupRepository } from './persistence/repository/user-group.repository' +import { UserWalletRepository } from './persistence/repository/user-wallet.repository' import { UserRepository } from './persistence/repository/user.repository' import { WalletGroupRepository } from './persistence/repository/wallet-group.repository' import { WalletRepository } from './persistence/repository/wallet.repository' @Module({ imports: [ConfigModule.forRoot({ load: [load] }), PersistenceModule, PolicyEngineModule], - controllers: [OrganizationController, UserController, UserGroupController, WalletController, WalletGroupController], + controllers: [ + OrganizationController, + UserController, + UserGroupController, + UserWalletController, + WalletController, + WalletGroupController + ], providers: [ - OrganizationService, + AuthCredentialRepository, OrganizationRepository, - UserService, - UserRepository, - UserGroupService, + OrganizationService, UserGroupRepository, - AuthCredentialRepository, - WalletService, - WalletRepository, + UserGroupService, + UserRepository, + UserService, + UserWalletRepository, WalletGroupRepository, + WalletRepository, + WalletService, { provide: APP_INTERCEPTOR, useClass: ClassSerializerInterceptor diff --git a/apps/orchestration/src/store/entity/http/rest/controller/user-wallet.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/user-wallet.controller.ts new file mode 100644 index 000000000..f810b7b55 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/user-wallet.controller.ts @@ -0,0 +1,32 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { UserService } from '../../../core/service/user.service' +import { AssignUserWalletRequestDto } from '../dto/assign-user-wallet-request.dto' +import { AssignUserWalletResponseDto } from '../dto/assign-user-wallet-response.dto' + +@Controller('/store/user-wallets') +@ApiTags('Entity Store') +export class UserWalletController { + constructor(private userService: UserService) {} + + @Post() + @ApiOperation({ + summary: 'Assigns a wallet to a user.' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: AssignUserWalletResponseDto + }) + async assign(@OrgId() orgId: string, @Body() body: AssignUserWalletRequestDto) { + const { data } = body.request + + await this.userService.assignWallet(body.request.data) + + return new AssignUserWalletResponseDto({ data }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-request.dto.ts new file mode 100644 index 000000000..a32e3ce04 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-request.dto.ts @@ -0,0 +1,33 @@ +import { Action, BaseAdminRequestPayloadDto } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, IsEnum, ValidateNested } from 'class-validator' +import { BaseActionDto } from './base-action.dto' +import { UserWalletDto } from './user-wallet.dto' + +class AssignUserWalletActionDto extends BaseActionDto { + @IsEnum(Object.values(Action)) + @ApiProperty({ + enum: Object.values(Action), + default: Action.ASSIGN_USER_WALLET + }) + action: typeof Action.ASSIGN_USER_WALLET + + @IsDefined() + @Type(() => UserWalletDto) + @ValidateNested() + @ApiProperty({ + type: UserWalletDto + }) + data: UserWalletDto +} + +export class AssignUserWalletRequestDto extends BaseAdminRequestPayloadDto { + @IsDefined() + @Type(() => AssignUserWalletActionDto) + @ValidateNested() + @ApiProperty({ + type: AssignUserWalletActionDto + }) + request: AssignUserWalletActionDto +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-response.dto.ts new file mode 100644 index 000000000..0c85d15f4 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-response.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, ValidateNested } from 'class-validator' +import { UserWalletDto } from './user-wallet.dto' + +export class AssignUserWalletResponseDto { + @IsDefined() + @Type(() => UserWalletDto) + @ValidateNested() + @ApiProperty({ + type: UserWalletDto + }) + data: UserWalletDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/user-wallet.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/user-wallet.dto.ts new file mode 100644 index 000000000..192ece581 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/user-wallet.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, IsString } from 'class-validator' + +export class UserWalletDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + walletId: string + + @IsString() + @IsNotEmpty() + @ApiProperty() + userId: string + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/user-wallet.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/user-wallet.repository.ts new file mode 100644 index 000000000..430bf4eae --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/user-wallet.repository.ts @@ -0,0 +1,16 @@ +import { UserWallet } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' + +@Injectable() +export class UserWalletRepository { + constructor(private prismaService: PrismaService) {} + + async assign(assignment: UserWallet): Promise { + await this.prismaService.userWalletAssignment.create({ + data: assignment + }) + + return assignment + } +}