diff --git a/apps/api/src/app/organization/dtos/member-response.dto.ts b/apps/api/src/app/organization/dtos/member-response.dto.ts new file mode 100644 index 00000000000..1aebd1ec48e --- /dev/null +++ b/apps/api/src/app/organization/dtos/member-response.dto.ts @@ -0,0 +1,78 @@ +import { MemberRoleEnum, MemberStatusEnum } from '@novu/shared'; +import { IsArray, IsDate, IsObject, IsString, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class MemberUserDto { + @ApiProperty() + @IsString() + _id: string; + + @ApiProperty() + @IsString() + firstName: string; + + @ApiProperty() + @IsString() + lastName: string; + + @ApiProperty() + @IsString() + email: string; +} + +export class MemberInviteDTO { + @ApiProperty() + @IsString() + email: string; + + @ApiProperty() + @IsString() + token: string; + + @ApiProperty() + @IsDate() + invitationDate: Date; + + @ApiPropertyOptional() + @IsDate() + answerDate?: Date; + + @ApiProperty() + @IsString() + _inviterId: string; +} + +export class MemberResponseDto { + @ApiProperty() + @IsString() + _id: string; + + @ApiProperty() + @IsString() + _userId: string; + + @ApiPropertyOptional() + @IsObject() + user?: MemberUserDto; + + @ApiPropertyOptional({ + enum: MemberRoleEnum, + isArray: true, + }) + @IsEnum(MemberRoleEnum) + roles?: MemberRoleEnum; + + @ApiPropertyOptional() + @IsObject() + invite?: MemberInviteDTO; + + @ApiPropertyOptional({ + enum: { ...MemberStatusEnum }, + }) + @IsEnum(MemberStatusEnum) + memberStatus?: MemberStatusEnum; + + @ApiProperty() + @IsString() + _organizationId: string; +} diff --git a/apps/api/src/app/organization/dtos/organization-response.dto.ts b/apps/api/src/app/organization/dtos/organization-response.dto.ts new file mode 100644 index 00000000000..46f9c81becc --- /dev/null +++ b/apps/api/src/app/organization/dtos/organization-response.dto.ts @@ -0,0 +1,56 @@ +import { PartnerTypeEnum, DirectionEnum } from '@novu/dal'; +import { IsObject, IsArray, IsString, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { UpdateBrandingDetailsDto } from './update-branding-details.dto'; + +export class IPartnerConfigurationResponseDto { + @ApiPropertyOptional() + @IsArray() + @IsString({ each: true }) + projectIds?: string[]; + + @ApiProperty() + @IsString() + accessToken: string; + + @ApiProperty() + @IsString() + configurationId: string; + + @ApiPropertyOptional() + @IsString() + teamId: string; + + @ApiProperty({ + enum: { ...PartnerTypeEnum }, + description: 'Partner Type Enum', + }) + @IsEnum(PartnerTypeEnum) + partnerType: PartnerTypeEnum; +} + +export class OrganizationBrandingResponseDto extends UpdateBrandingDetailsDto { + @ApiPropertyOptional({ + enum: { ...DirectionEnum }, + }) + @IsString() + direction?: DirectionEnum; +} + +export class OrganizationResponseDto { + @ApiProperty() + @IsString() + name: string; + + @ApiPropertyOptional() + @IsString() + logo?: string; + + @ApiProperty() + @IsObject() + branding: OrganizationBrandingResponseDto; + + @ApiPropertyOptional() + @IsObject() + partnerConfigurations: IPartnerConfigurationResponseDto[]; +} diff --git a/apps/api/src/app/organization/organization.controller.ts b/apps/api/src/app/organization/organization.controller.ts index aa739fe9468..9a92742186f 100644 --- a/apps/api/src/app/organization/organization.controller.ts +++ b/apps/api/src/app/organization/organization.controller.ts @@ -13,7 +13,7 @@ import { } from '@nestjs/common'; import { OrganizationEntity } from '@novu/dal'; import { IJwtPayload, MemberRoleEnum } from '@novu/shared'; -import { ApiExcludeController, ApiTags } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger'; import { Roles } from '../auth/framework/roles.decorator'; import { UserSession } from '../shared/framework/user.decorator'; import { CreateOrganizationDto } from './dtos/create-organization.dto'; @@ -39,12 +39,14 @@ import { RenameOrganization } from './usecases/rename-organization/rename-organi import { RenameOrganizationDto } from './dtos/rename-organization.dto'; import { UpdateBrandingDetailsDto } from './dtos/update-branding-details.dto'; import { UpdateMemberRolesDto } from './dtos/update-member-roles.dto'; - +import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; +import { ApiResponse } from '../shared/framework/response.decorator'; +import { OrganizationBrandingResponseDto, OrganizationResponseDto } from './dtos/organization-response.dto'; +import { MemberResponseDto } from './dtos/member-response.dto'; @Controller('/organizations') @UseInterceptors(ClassSerializerInterceptor) @UseGuards(JwtAuthGuard) @ApiTags('Organizations') -@ApiExcludeController() export class OrganizationController { constructor( private createOrganizationUsecase: CreateOrganization, @@ -58,6 +60,11 @@ export class OrganizationController { ) {} @Post('/') + @ExternalApiAccessible() + @ApiResponse(OrganizationResponseDto, 201) + @ApiOperation({ + summary: 'Create an organization', + }) async createOrganization( @UserSession() user: IJwtPayload, @Body() body: CreateOrganizationDto @@ -72,6 +79,11 @@ export class OrganizationController { } @Get('/') + @ExternalApiAccessible() + @ApiResponse(OrganizationResponseDto, 200, true) + @ApiOperation({ + summary: 'Fetch all organizations', + }) async getOrganizations(@UserSession() user: IJwtPayload): Promise { const command = GetOrganizationsCommand.create({ userId: user._id, @@ -81,6 +93,11 @@ export class OrganizationController { } @Get('/me') + @ExternalApiAccessible() + @ApiResponse(OrganizationResponseDto) + @ApiOperation({ + summary: 'Fetch current organization details', + }) async getMyOrganization(@UserSession() user: IJwtPayload): Promise { const command = GetMyOrganizationCommand.create({ userId: user._id, @@ -91,7 +108,13 @@ export class OrganizationController { } @Delete('/members/:memberId') + @ExternalApiAccessible() @Roles(MemberRoleEnum.ADMIN) + @ApiResponse(MemberResponseDto) + @ApiOperation({ + summary: 'Remove a member from organization using memberId', + }) + @ApiParam({ name: 'memberId', type: String, required: true }) async removeMember(@UserSession() user: IJwtPayload, @Param('memberId') memberId: string) { return await this.removeMemberUsecase.execute( RemoveMemberCommand.create({ @@ -103,7 +126,13 @@ export class OrganizationController { } @Put('/members/:memberId/roles') + @ExternalApiAccessible() @Roles(MemberRoleEnum.ADMIN) + @ApiResponse(MemberResponseDto) + @ApiOperation({ + summary: 'Update a member role to admin', + }) + @ApiParam({ name: 'memberId', type: String, required: true }) async updateMemberRoles( @UserSession() user: IJwtPayload, @Param('memberId') memberId: string, @@ -120,6 +149,11 @@ export class OrganizationController { } @Get('/members') + @ExternalApiAccessible() + @ApiResponse(MemberResponseDto, 200, true) + @ApiOperation({ + summary: 'Fetch all members of current organizations', + }) async getMember(@UserSession() user: IJwtPayload) { return await this.getMembers.execute( GetMembersCommand.create({ @@ -130,19 +164,12 @@ export class OrganizationController { ); } - @Post('/members/invite') - @Roles(MemberRoleEnum.ADMIN) - async inviteMember(@UserSession() user: IJwtPayload) { - return await this.getMembers.execute( - GetMembersCommand.create({ - user, - userId: user._id, - organizationId: user.organizationId, - }) - ); - } - @Put('/branding') + @ExternalApiAccessible() + @ApiResponse(OrganizationBrandingResponseDto) + @ApiOperation({ + summary: 'Update organization branding details', + }) async updateBrandingDetails(@UserSession() user: IJwtPayload, @Body() body: UpdateBrandingDetailsDto) { return await this.updateBrandingDetailsUsecase.execute( UpdateBrandingDetailsCommand.create({ @@ -158,7 +185,12 @@ export class OrganizationController { } @Patch('/') + @ExternalApiAccessible() @Roles(MemberRoleEnum.ADMIN) + @ApiResponse(RenameOrganizationDto) + @ApiOperation({ + summary: 'Rename organization name', + }) async renameOrganization(@UserSession() user: IJwtPayload, @Body() body: RenameOrganizationDto) { return await this.renameOrganizationUsecase.execute( RenameOrganizationCommand.create({ diff --git a/apps/api/src/app/organization/organization.module.ts b/apps/api/src/app/organization/organization.module.ts index 5cc77ed1573..9e058638c81 100644 --- a/apps/api/src/app/organization/organization.module.ts +++ b/apps/api/src/app/organization/organization.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule, RequestMethod, forwardRef } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { EnvironmentsModule } from '../environments/environments.module'; import { IntegrationModule } from '../integrations/integrations.module'; @@ -6,9 +6,10 @@ import { SharedModule } from '../shared/shared.module'; import { UserModule } from '../user/user.module'; import { OrganizationController } from './organization.controller'; import { USE_CASES } from './usecases'; +import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [SharedModule, UserModule, EnvironmentsModule, IntegrationModule], + imports: [SharedModule, UserModule, EnvironmentsModule, IntegrationModule, forwardRef(() => AuthModule)], controllers: [OrganizationController], providers: [...USE_CASES], exports: [...USE_CASES], diff --git a/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts b/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts index f60a885af8e..b51ffc67604 100644 --- a/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts +++ b/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts @@ -1,5 +1,5 @@ import { Injectable, Scope } from '@nestjs/common'; -import { OrganizationRepository, MemberRepository } from '@novu/dal'; +import { MemberRepository } from '@novu/dal'; import { MemberRoleEnum, MemberStatusEnum } from '@novu/shared'; import { GetMembersCommand } from './get-members.command'; @@ -7,7 +7,7 @@ import { GetMembersCommand } from './get-members.command'; scope: Scope.REQUEST, }) export class GetMembers { - constructor(private organizationRepository: OrganizationRepository, private membersRepository: MemberRepository) {} + constructor(private membersRepository: MemberRepository) {} async execute(command: GetMembersCommand) { return (await this.membersRepository.getOrganizationMembers(command.organizationId)) diff --git a/apps/api/src/bootstrap.ts b/apps/api/src/bootstrap.ts index 2aa6d7fcaf4..7cf551fb7b0 100644 --- a/apps/api/src/bootstrap.ts +++ b/apps/api/src/bootstrap.ts @@ -94,7 +94,7 @@ export async function bootstrap(expressApp?): Promise { const options = new DocumentBuilder() .setTitle('Novu API') - .setDescription('The Novu API description') + .setDescription('Open API Specification for Novu API') .setVersion('1.0') .addTag('Events') .addTag('Subscribers') @@ -111,6 +111,7 @@ export async function bootstrap(expressApp?): Promise { .addTag('Feeds') .addTag('Tenants') .addTag('Messages') + .addTag('Organizations') .addTag('Execution Details') .build(); const document = SwaggerModule.createDocument(app, options); diff --git a/libs/dal/src/repositories/organization/organization.entity.ts b/libs/dal/src/repositories/organization/organization.entity.ts index 7de9c528b3c..8ea0a1ee541 100644 --- a/libs/dal/src/repositories/organization/organization.entity.ts +++ b/libs/dal/src/repositories/organization/organization.entity.ts @@ -30,3 +30,8 @@ export interface IPartnerConfiguration { export enum PartnerTypeEnum { VERCEL = 'vercel', } + +export enum DirectionEnum { + LTR = 'ltr', + RTL = 'trl', +} diff --git a/packages/node/README.md b/packages/node/README.md index d8631982118..1cf9b61e242 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -712,7 +712,7 @@ const novu = new Novu(''); const payload = { content: "

Layout Start

{{{body}}}

Layout End

", - description: "Organisation's first layout", + description: "Organization's first layout", name: "First Layout", identifier: "firstlayout", variables: [ @@ -737,7 +737,7 @@ const novu = new Novu(''); const payloadToUpdate = { content: "

Layout Start

{{{body}}}

Layout End

", - description: "Organisation's first layout", + description: "Organization's first layout", name: "First Layout", identifier: "firstlayout", variables: [