diff --git a/src/domain/organizations/organizations.repository.interface.ts b/src/domain/organizations/organizations.repository.interface.ts index 3c209747b46..ceb6d559be9 100644 --- a/src/domain/organizations/organizations.repository.interface.ts +++ b/src/domain/organizations/organizations.repository.interface.ts @@ -53,6 +53,16 @@ export interface IOrganizationsRepository { relations?: FindOptionsRelations; }): Promise>; + findOneByUserIdOrFail( + args: Parameters[0], + ): Promise; + + findOneByUserId(args: { + userId: User['id']; + select?: FindOptionsSelect; + relations?: FindOptionsRelations; + }): Promise; + update(args: { id: Organization['id']; updatePayload: Partial>; diff --git a/src/domain/organizations/organizations.repository.ts b/src/domain/organizations/organizations.repository.ts index 7df0cf0418d..77419357696 100644 --- a/src/domain/organizations/organizations.repository.ts +++ b/src/domain/organizations/organizations.repository.ts @@ -127,6 +127,32 @@ export class OrganizationsRepository implements IOrganizationsRepository { }); } + public async findOneByUserIdOrFail( + args: Parameters[0], + ): Promise { + const organization = await this.findOneByUserId(args); + + if (!organization) { + throw new NotFoundException( + 'Organization not found. UserId = ' + args.userId, + ); + } + + return organization; + } + + public async findOneByUserId(args: { + userId: number; + select?: FindOptionsSelect; + relations?: FindOptionsRelations; + }): Promise { + return await this.findOne({ + where: { + user_organizations: { user: { id: args.userId } }, + }, + }); + } + public async update(args: { id: Organization['id']; updatePayload: QueryDeepPartialEntity; diff --git a/src/routes/organizations/entities/create-organization.dto.entity.ts b/src/routes/organizations/entities/create-organization.dto.entity.ts index 3f476bb9a9b..b53c4b18cec 100644 --- a/src/routes/organizations/entities/create-organization.dto.entity.ts +++ b/src/routes/organizations/entities/create-organization.dto.entity.ts @@ -1,7 +1,14 @@ import { Organization } from '@/datasources/organizations/entities/organizations.entity.db'; import { ApiProperty } from '@nestjs/swagger'; +import { z } from 'zod'; -export class CreateOrganizationDto { - @ApiProperty() - name!: Organization['name']; +export const CreateOrganizationSchema = z.object({ + name: z.string(), +}); + +export class CreateOrganizationDto + implements z.infer +{ + @ApiProperty({ type: String }) + public readonly name!: Organization['name']; } diff --git a/src/routes/organizations/entities/create-organizations.dto.entity.ts b/src/routes/organizations/entities/create-organizations.dto.entity.ts index 81d7ead9877..f5bd9700986 100644 --- a/src/routes/organizations/entities/create-organizations.dto.entity.ts +++ b/src/routes/organizations/entities/create-organizations.dto.entity.ts @@ -1,6 +1,11 @@ +import { Organization } from '@/datasources/organizations/entities/organizations.entity.db'; import { ApiProperty } from '@nestjs/swagger'; +import { number } from 'zod'; export class CreateOrganizationResponse { - @ApiProperty() - name!: string; + @ApiProperty({ type: String }) + public readonly name!: Organization['name']; + + @ApiProperty({ type: number }) + public readonly id!: Organization['id']; } diff --git a/src/routes/organizations/entities/update-organization.dto.entity.ts b/src/routes/organizations/entities/update-organization.dto.entity.ts index 7f086b733f0..00239df43c5 100644 --- a/src/routes/organizations/entities/update-organization.dto.entity.ts +++ b/src/routes/organizations/entities/update-organization.dto.entity.ts @@ -1,15 +1,18 @@ +import { Organization } from '@/datasources/organizations/entities/organizations.entity.db'; import { OrganizationStatus } from '@/domain/organizations/entities/organization.entity'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class UpdateOrganizationDto { - @ApiPropertyOptional() - public name?: string; + @ApiPropertyOptional({ type: String }) + public readonly name?: Organization['name']; - @ApiPropertyOptional() - public status?: OrganizationStatus; + @ApiPropertyOptional({ + enum: OrganizationStatus, + }) + public readonly status?: OrganizationStatus; } export class UpdateOrganizationResponse { - @ApiProperty() - public id!: number; + @ApiProperty({ type: Number }) + public readonly id!: Organization['id']; } diff --git a/src/routes/organizations/organizations.controller.ts b/src/routes/organizations/organizations.controller.ts index f74954130c0..1a035c2c110 100644 --- a/src/routes/organizations/organizations.controller.ts +++ b/src/routes/organizations/organizations.controller.ts @@ -1,11 +1,18 @@ -import { ApiTags } from '@nestjs/swagger'; +import { + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { Body, Controller, Delete, Get, + HttpStatus, Param, - ParseIntPipe, Patch, Post, UseGuards, @@ -16,16 +23,19 @@ import { Auth } from '@/routes/auth/decorators/auth.decorator'; import { AuthPayload } from '@/domain/auth/entities/auth-payload.entity'; import { OrganizationStatus } from '@/domain/organizations/entities/organization.entity'; import { CreateOrganizationResponse } from '@/routes/organizations/entities/create-organizations.dto.entity'; -import { CreateOrganizationDto } from '@/routes/organizations/entities/create-organization.dto.entity'; +import { + CreateOrganizationDto, + CreateOrganizationSchema, +} from '@/routes/organizations/entities/create-organization.dto.entity'; import { GetOrganizationResponse } from '@/routes/organizations/entities/get-organization.dto.entity'; import { UpdateOrganizationDto, UpdateOrganizationResponse, } from '@/routes/organizations/entities/update-organization.dto.entity'; import { ValidationPipe } from '@/validation/pipes/validation.pipe'; -import { z } from 'zod'; - +import { RowSchema } from '@/datasources/db/v1/entities/row.entity'; @ApiTags('organizations') +@UseGuards(AuthGuard) @Controller({ path: 'organizations', version: '1' }) export class OrganizationsController { public constructor( @@ -35,9 +45,15 @@ export class OrganizationsController { } @Post() - @UseGuards(AuthGuard) + @ApiOkResponse({ + description: 'Organizations created', + }) + @ApiNotFoundResponse({ description: 'User not found.' }) + @ApiForbiddenResponse({ description: 'Forbidden resource' }) + @ApiUnauthorizedResponse({ description: 'Signer address not provided' }) public async create( - @Body() body: CreateOrganizationDto, + @Body(new ValidationPipe(CreateOrganizationSchema)) + body: CreateOrganizationDto, @Auth() authPayload: AuthPayload, ): Promise { return await this.organizationsService.create({ @@ -48,7 +64,12 @@ export class OrganizationsController { } @Get() - @UseGuards(AuthGuard) + @ApiOkResponse({ + description: 'Organizations found', + }) + @ApiNotFoundResponse({ description: 'User not found.' }) + @ApiForbiddenResponse({ description: 'Forbidden resource' }) + @ApiUnauthorizedResponse({ description: 'Signer address not provided' }) public async get( @Auth() authPayload: AuthPayload, ): Promise> { @@ -56,19 +77,32 @@ export class OrganizationsController { } @Get('/:id') - @UseGuards(AuthGuard) + @ApiOkResponse({ + description: 'Organization found', + }) + @ApiNotFoundResponse({ + description: 'Organization not found. OR User not found.', + }) + @ApiForbiddenResponse({ description: 'Forbidden resource' }) + @ApiUnauthorizedResponse({ description: 'Signer address not provided' }) public async getOne( - @Param('id', new ValidationPipe(z.number())) id: number, + @Param('id', new ValidationPipe(RowSchema.shape.id)) id: number, @Auth() authPayload: AuthPayload, ): Promise { return await this.organizationsService.getOne(id, authPayload); } @Patch('/:id') - @UseGuards(AuthGuard) + @ApiOkResponse({ + description: 'Organization updated', + }) + @ApiForbiddenResponse({ description: 'Forbidden resource' }) + @ApiUnauthorizedResponse({ + description: 'Signer address not provided OR User is unauthorized', + }) public async update( @Body() payload: UpdateOrganizationDto, - @Param('id', new ValidationPipe(z.number())) id: number, + @Param('id', new ValidationPipe(RowSchema.shape.id)) id: number, @Auth() authPayload: AuthPayload, ): Promise { return await this.organizationsService.update({ @@ -79,9 +113,16 @@ export class OrganizationsController { } @Delete('/:id') - @UseGuards(AuthGuard) + @ApiResponse({ + description: 'Organization deleted', + status: HttpStatus.NO_CONTENT, + }) + @ApiForbiddenResponse({ description: 'Forbidden resource' }) + @ApiUnauthorizedResponse({ + description: 'Signer address not provided OR User is unauthorized', + }) public async delete( - @Param('id', ParseIntPipe) id: number, + @Param('id', new ValidationPipe(RowSchema.shape.id)) id: number, @Auth() authPayload: AuthPayload, ): Promise { return await this.organizationsService.delete({ id, authPayload }); diff --git a/src/routes/organizations/organizations.module.ts b/src/routes/organizations/organizations.module.ts index 7c31fdfd47f..9751beae241 100644 --- a/src/routes/organizations/organizations.module.ts +++ b/src/routes/organizations/organizations.module.ts @@ -3,9 +3,14 @@ import { OrganizationsRepositoryModule } from '@/domain/organizations/organizati import { OrganizationsController } from '@/routes/organizations/organizations.controller'; import { OrganizationsService } from '@/routes/organizations/organizations.service'; import { AuthRepositoryModule } from '@/domain/auth/auth.repository.interface'; +import { UserRepositoryModule } from '@/domain/users/users.repository.module'; @Module({ - imports: [OrganizationsRepositoryModule, AuthRepositoryModule], + imports: [ + OrganizationsRepositoryModule, + AuthRepositoryModule, + UserRepositoryModule, + ], controllers: [OrganizationsController], providers: [OrganizationsService], }) diff --git a/src/routes/organizations/organizations.service.ts b/src/routes/organizations/organizations.service.ts index 37e37b632b9..816037bd476 100644 --- a/src/routes/organizations/organizations.service.ts +++ b/src/routes/organizations/organizations.service.ts @@ -1,18 +1,20 @@ import type { Organization } from '@/datasources/organizations/entities/organizations.entity.db'; import type { AuthPayload } from '@/domain/auth/entities/auth-payload.entity'; -import type { IOrganizationsRepository } from '@/domain/organizations/organizations.repository.interface'; +import { IOrganizationsRepository } from '@/domain/organizations/organizations.repository.interface'; import { UserOrganizationRole } from '@/domain/users/entities/user-organization.entity'; -import type { IUsersRepository } from '@/domain/users/users.repository.interface'; +import { IUsersRepository } from '@/domain/users/users.repository.interface'; import type { GetOrganizationResponse } from '@/routes/organizations/entities/get-organization.dto.entity'; import type { UpdateOrganizationDto, UpdateOrganizationResponse, } from '@/routes/organizations/entities/update-organization.dto.entity'; -import { UnauthorizedException } from '@nestjs/common'; +import { Inject, UnauthorizedException } from '@nestjs/common'; export class OrganizationsService { public constructor( + @Inject(IUsersRepository) private readonly userRepository: IUsersRepository, + @Inject(IOrganizationsRepository) private readonly organizationsRepository: IOrganizationsRepository, ) {} @@ -34,10 +36,11 @@ export class OrganizationsService { ): Promise> { this.assertSignerAddress(authPayload); - const { id: userId } = - await this.userRepository.getWithWallets(authPayload); + const { id: userId } = await this.userRepository.findByWalletAddressOrFail( + authPayload.signer_address, + ); - return await this.organizationsRepository.findByUserIdOrFail({ + return await this.organizationsRepository.findByUserId({ userId, select: { // id: true, @@ -69,17 +72,14 @@ export class OrganizationsService { ): Promise { this.assertSignerAddress(authPayload); - const { id: userId } = - await this.userRepository.getWithWallets(authPayload); + const { id: userId } = await this.userRepository.findByWalletAddressOrFail( + authPayload.signer_address, + ); return await this.organizationsRepository.findOneOrFail({ where: { id, - user_organizations: { - user: { - id: userId, - }, - }, + user_organizations: { user: { id: userId } }, }, select: { // id: true,