From 57e44f324b7a5a787b5f5c1c66a8bf1f553ea181 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 11 Jul 2024 09:04:12 -0400 Subject: [PATCH] chore: added files/folders/gateway/workspaces/dtos, http filter exceptions, bridge service function, mailer functions, workspace uuid pipe --- ...xception-filter-extended.exception.spec.ts | 81 +++++++++++++++++ ...ttp-exception-filter-extended.exception.ts | 30 +++++++ src/externals/bridge/bridge.service.ts | 25 ++++++ src/externals/mailer/mailer.service.ts | 65 ++++++++++++++ src/main.ts | 3 + src/modules/auth/auth.guard.ts | 12 ++- .../disable-global-auth.decorator.ts | 2 + src/modules/auth/gateway-rs256jwt.strategy.ts | 28 ++++++ src/modules/auth/gateway.guard.ts | 17 ++++ src/modules/file/dto/create-file.dto.ts | 90 +++++++++++++++++++ src/modules/file/dto/update-file-meta.dto.ts | 11 +++ src/modules/folder/dto/create-folder.dto.ts | 20 +++++ .../folder/dto/update-folder-meta.dto.ts | 12 +++ .../gateway/dto/delete-workspace.dto.ts | 11 +++ .../gateway/dto/initialize-workspace.dto.ts | 26 ++++++ .../dto/update-workspace-storage.dto.ts | 19 ++++ .../dto/accept-workspace-invite.dto.ts | 13 +++ .../dto/change-user-assigned-space.dto.ts | 13 +++ src/modules/workspaces/dto/create-team.dto.ts | 19 ++++ .../dto/create-workspace-file.dto.ts | 3 + .../dto/create-workspace-folder.dto.ts | 20 +++++ .../dto/create-workspace-invite.dto.ts | 42 +++++++++ .../workspaces/dto/edit-team-data.dto.ts | 13 +++ .../dto/edit-workspace-details-dto.ts | 24 +++++ .../dto/get-items-inside-shared-folder.dto.ts | 42 +++++++++ src/modules/workspaces/dto/pagination.dto.ts | 31 +++++++ .../workspaces/dto/setup-workspace.dto.ts | 34 +++++++ .../dto/share-item-with-team.dto.ts | 34 +++++++ .../workspace-invitations-pagination.dto.ts | 28 ++++++ .../pipes/validate-uuid.pipe.spec.ts | 69 ++++++++++++++ .../workspaces/pipes/validate-uuid.pipe.ts | 18 ++++ 31 files changed, 851 insertions(+), 4 deletions(-) create mode 100644 src/common/http-exception-filter-extended.exception.spec.ts create mode 100644 src/common/http-exception-filter-extended.exception.ts create mode 100644 src/modules/auth/decorators/disable-global-auth.decorator.ts create mode 100644 src/modules/auth/gateway-rs256jwt.strategy.ts create mode 100644 src/modules/auth/gateway.guard.ts create mode 100644 src/modules/file/dto/create-file.dto.ts create mode 100644 src/modules/file/dto/update-file-meta.dto.ts create mode 100644 src/modules/folder/dto/create-folder.dto.ts create mode 100644 src/modules/folder/dto/update-folder-meta.dto.ts create mode 100644 src/modules/gateway/dto/delete-workspace.dto.ts create mode 100644 src/modules/gateway/dto/initialize-workspace.dto.ts create mode 100644 src/modules/gateway/dto/update-workspace-storage.dto.ts create mode 100644 src/modules/workspaces/dto/accept-workspace-invite.dto.ts create mode 100644 src/modules/workspaces/dto/change-user-assigned-space.dto.ts create mode 100644 src/modules/workspaces/dto/create-team.dto.ts create mode 100644 src/modules/workspaces/dto/create-workspace-file.dto.ts create mode 100644 src/modules/workspaces/dto/create-workspace-folder.dto.ts create mode 100644 src/modules/workspaces/dto/create-workspace-invite.dto.ts create mode 100644 src/modules/workspaces/dto/edit-team-data.dto.ts create mode 100644 src/modules/workspaces/dto/edit-workspace-details-dto.ts create mode 100644 src/modules/workspaces/dto/get-items-inside-shared-folder.dto.ts create mode 100644 src/modules/workspaces/dto/pagination.dto.ts create mode 100644 src/modules/workspaces/dto/setup-workspace.dto.ts create mode 100644 src/modules/workspaces/dto/share-item-with-team.dto.ts create mode 100644 src/modules/workspaces/dto/workspace-invitations-pagination.dto.ts create mode 100644 src/modules/workspaces/pipes/validate-uuid.pipe.spec.ts create mode 100644 src/modules/workspaces/pipes/validate-uuid.pipe.ts diff --git a/src/common/http-exception-filter-extended.exception.spec.ts b/src/common/http-exception-filter-extended.exception.spec.ts new file mode 100644 index 000000000..fadd72fbd --- /dev/null +++ b/src/common/http-exception-filter-extended.exception.spec.ts @@ -0,0 +1,81 @@ +import { ArgumentsHost, HttpException, Logger } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BaseExceptionFilter } from '@nestjs/core'; +import { newUser } from '../../test/fixtures'; +import { ExtendedHttpExceptionFilter } from './http-exception-filter-extended.exception'; + +describe('ExtendedHttpExceptionFilter', () => { + let filter: ExtendedHttpExceptionFilter; + let loggerErrorSpy: jest.SpyInstance; + let baseExceptionFilterCatchSpy: jest.SpyInstance; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ExtendedHttpExceptionFilter], + }).compile(); + + filter = module.get( + ExtendedHttpExceptionFilter, + ); + loggerErrorSpy = jest.spyOn(Logger.prototype, 'error').mockImplementation(); + baseExceptionFilterCatchSpy = jest + .spyOn(BaseExceptionFilter.prototype, 'catch') + .mockImplementation(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const createMockArgumentsHost = (url: string, method: string, user: any) => + ({ + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + url, + method, + user, + }), + getResponse: jest.fn().mockReturnValue({ + statusCode: 500, + headers: {}, + getHeader: jest.fn(), + setHeader: jest.fn(), + isHeadersSent: false, + }), + getNext: jest.fn(), + }), + getArgs: jest.fn(), + getArgByIndex: jest.fn(), + switchToRpc: jest.fn(), + switchToWs: jest.fn(), + getType: jest.fn(), + }) as unknown as ArgumentsHost; + + it('When non expected error are sent, then it should log details and call parent catch', () => { + const mockException = new Error('Unexpected error'); + const user = newUser(); + const mockHost = createMockArgumentsHost('/my-endpoint', 'GET', user); + + filter.catch(mockException, mockHost); + + expect(loggerErrorSpy).toHaveBeenCalled(); + expect(baseExceptionFilterCatchSpy).toHaveBeenCalledWith( + mockException, + mockHost, + ); + }); + + it('When expected exception is sent, then it should not log detailss and call parent catch', () => { + const mockException = new HttpException('This is an http error', 400); + const user = newUser(); + const mockHost = createMockArgumentsHost('/my-endpoint', 'GET', user); + + filter.catch(mockException, mockHost); + + expect(loggerErrorSpy).not.toHaveBeenCalled(); + expect(baseExceptionFilterCatchSpy).toHaveBeenCalledWith( + mockException, + mockHost, + ); + }); +}); diff --git a/src/common/http-exception-filter-extended.exception.ts b/src/common/http-exception-filter-extended.exception.ts new file mode 100644 index 000000000..886ccec3f --- /dev/null +++ b/src/common/http-exception-filter-extended.exception.ts @@ -0,0 +1,30 @@ +import { Catch, Logger, HttpException, ArgumentsHost } from '@nestjs/common'; +import { BaseExceptionFilter } from '@nestjs/core'; + +@Catch() +export class ExtendedHttpExceptionFilter extends BaseExceptionFilter { + private readonly logger = new Logger(ExtendedHttpExceptionFilter.name); + + catch(exception: any, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + + const request = ctx.getRequest(); + + if (!(exception instanceof HttpException)) { + const errorResponse = { + timestamp: new Date().toISOString(), + path: request.url, + method: request.method, + message: (exception as Error)?.message, + stack: (exception as Error)?.stack, + user: { email: request?.user?.email, uuid: request?.user?.uuid }, + }; + + this.logger.error( + `[UNEXPECTED_ERROR] - Details: ${JSON.stringify(errorResponse)}`, + ); + } + + super.catch(exception, host); + } +} diff --git a/src/externals/bridge/bridge.service.ts b/src/externals/bridge/bridge.service.ts index 11ed4d4f4..d7017b42c 100644 --- a/src/externals/bridge/bridge.service.ts +++ b/src/externals/bridge/bridge.service.ts @@ -176,6 +176,31 @@ export class BridgeService { } } + async setStorage(email: UserAttributes['email'], bytes: number) { + try { + const url = this.configService.get('apis.storage.url'); + const username = this.configService.get('apis.storage.auth.username'); + const password = this.configService.get('apis.storage.auth.password'); + + const params = { + headers: { 'Content-Type': 'application/json' }, + auth: { username, password }, + }; + + await this.httpClient.post( + `${url}/gateway/upgrade`, + { email, bytes }, + params, + ); + } catch (error) { + Logger.error(` + [BRIDGESERVICE/SETSTORAGE]: There was an error while trying to set user storage space Error: ${JSON.stringify( + error, + )} + `); + } + } + async getLimit( networkUser: UserAttributes['bridgeUser'], networkPass: UserAttributes['userId'], diff --git a/src/externals/mailer/mailer.service.ts b/src/externals/mailer/mailer.service.ts index 38d672b7f..f5f590ee2 100644 --- a/src/externals/mailer/mailer.service.ts +++ b/src/externals/mailer/mailer.service.ts @@ -4,6 +4,7 @@ import sendgrid from '@sendgrid/mail'; import { User } from '../../modules/user/user.domain'; import { Folder } from '../../modules/folder/folder.domain'; import { File } from '../../modules/file/file.domain'; +import { Workspace } from '../../modules/workspaces/domains/workspaces.domain'; type SendInvitationToSharingContext = { notification_message: string; @@ -116,6 +117,70 @@ export class MailerService { ); } + async sendWorkspaceUserInvitation( + senderName: User['name'], + invitedUserEmail: User['email'], + workspaceName: Workspace['name'], + mailInfo: { + acceptUrl: string; + declineUrl: string; + }, + optionals: { + avatar?: { + pictureUrl: string; + initials: string; + }; + message?: string; + }, + ): Promise { + const context = { + sender_name: senderName, + workspace_name: workspaceName, + avatar: { + picture_url: optionals?.avatar?.pictureUrl, + initials: optionals?.avatar?.initials, + }, + accept_url: mailInfo.acceptUrl, + decline_url: mailInfo.declineUrl, + message: optionals?.message, + }; + await this.send( + invitedUserEmail, + this.configService.get('mailer.templates.invitationToWorkspaceUser'), + context, + ); + } + + async sendWorkspaceUserExternalInvitation( + senderName: User['name'], + invitedUserEmail: User['email'], + workspaceName: Workspace['name'], + signUpUrl: string, + optionals: { + avatar?: { + pictureUrl: string; + initials: string; + }; + message?: string; + }, + ): Promise { + const context = { + sender_name: senderName, + workspace_name: workspaceName, + avatar: { + picture_url: optionals?.avatar?.pictureUrl, + initials: optionals?.avatar?.initials, + }, + signup_url: signUpUrl, + message: optionals?.message, + }; + await this.send( + invitedUserEmail, + this.configService.get('mailer.templates.invitationToWorkspaceGuestUser'), + context, + ); + } + async sendRemovedFromSharingEmail( userRemovedFromSharingEmail: User['email'], itemName: File['plainName'] | Folder['plainName'], diff --git a/src/main.ts b/src/main.ts index 390f0956f..3270ac98b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,6 +35,8 @@ async function bootstrap() { 'internxt-mnemonic', 'x-share-password', 'X-Internxt-Captcha', + 'x-internxt-workspace', + 'internxt-resources-token', ], exposedHeaders: ['sessionId'], origin: '*', @@ -68,6 +70,7 @@ async function bootstrap() { .setDescription('Drive API') .setVersion('1.0') .addBearerAuth() + .addBearerAuth(undefined, 'gateway') .build(); const document = SwaggerModule.createDocument(app, swaggerConfig); diff --git a/src/modules/auth/auth.guard.ts b/src/modules/auth/auth.guard.ts index 45d787542..6e74d8648 100644 --- a/src/modules/auth/auth.guard.ts +++ b/src/modules/auth/auth.guard.ts @@ -10,12 +10,16 @@ export class AuthGuard extends PassportAuthGuard([JwtStrategy.id]) { } canActivate(context: ExecutionContext) { - const isPublic = this.reflector.get( - 'isPublic', - context.getHandler(), + const handlerContext = context.getHandler(); + const classContext = context.getClass(); + + const isPublic = this.reflector.get('isPublic', handlerContext); + const disableGlobalAuth = this.reflector.getAllAndOverride( + 'disableGlobalAuth', + [handlerContext, classContext], ); - if (isPublic) { + if (isPublic || disableGlobalAuth) { return true; } diff --git a/src/modules/auth/decorators/disable-global-auth.decorator.ts b/src/modules/auth/decorators/disable-global-auth.decorator.ts new file mode 100644 index 000000000..a282d939f --- /dev/null +++ b/src/modules/auth/decorators/disable-global-auth.decorator.ts @@ -0,0 +1,2 @@ +import { SetMetadata } from '@nestjs/common'; +export const DisableGlobalAuth = () => SetMetadata('disableGlobalAuth', true); diff --git a/src/modules/auth/gateway-rs256jwt.strategy.ts b/src/modules/auth/gateway-rs256jwt.strategy.ts new file mode 100644 index 000000000..419678768 --- /dev/null +++ b/src/modules/auth/gateway-rs256jwt.strategy.ts @@ -0,0 +1,28 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { ConfigService } from '@nestjs/config'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Injectable } from '@nestjs/common'; + +const strategyId = 'gateway.jwt.rs256'; +@Injectable() +export class GatewayRS256JwtStrategy extends PassportStrategy( + Strategy, + strategyId, +) { + static id = strategyId; + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: Buffer.from( + configService.get('secrets.driveGateway') as string, + 'base64', + ).toString('utf8'), + algorithms: ['RS256'], + }); + } + + async validate(): Promise { + return true; + } +} diff --git a/src/modules/auth/gateway.guard.ts b/src/modules/auth/gateway.guard.ts new file mode 100644 index 000000000..d31dd639a --- /dev/null +++ b/src/modules/auth/gateway.guard.ts @@ -0,0 +1,17 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; +import { GatewayRS256JwtStrategy } from './gateway-rs256jwt.strategy'; + +@Injectable() +export class GatewayGuard extends PassportAuthGuard( + GatewayRS256JwtStrategy.id, +) { + constructor(private readonly reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + return super.canActivate(context); + } +} diff --git a/src/modules/file/dto/create-file.dto.ts b/src/modules/file/dto/create-file.dto.ts new file mode 100644 index 000000000..3c692d0ad --- /dev/null +++ b/src/modules/file/dto/create-file.dto.ts @@ -0,0 +1,90 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsDateString, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +export class CreateFileDto { + @ApiProperty({ + description: 'The name of the file', + example: 'example', + }) + @IsString() + name: string; + + @ApiProperty({ + description: 'The bucket where the file is stored', + example: 'my-bucket', + }) + @IsString() + bucket: string; + + @ApiProperty({ + description: 'The ID of the file', + example: 'file12345', + }) + @IsString() + fileId: string; + + @ApiProperty({ + description: 'The encryption version used for the file', + example: '03-aes', + }) + @IsString() + encryptVersion: string; + + @ApiProperty({ + description: 'The UUID of the folder containing the file', + type: 'string', + format: 'uuid', + example: '550e8400-e29b-41d4-a716-446655440000', + }) + @IsUUID('4') + folderUuid: string; + + @ApiProperty({ + description: 'The size of the file in bytes', + type: 'number', + format: 'bigint', + example: 123456789, + }) + @IsNumber() + size: bigint; + + @ApiProperty({ + description: 'The plain text name of the file', + example: 'example', + }) + @IsString() + plainName: string; + + @ApiProperty({ + description: 'The type of the file (optional)', + required: false, + example: 'text', + }) + @IsString() + @IsOptional() + type: string; + + @ApiProperty({ + description: 'The last modification time of the file (optional)', + required: false, + example: '2023-05-30T12:34:56.789Z', + }) + @IsDateString() + @IsOptional() + modificationTime: Date; + + @ApiProperty({ + description: 'The date associated with the file (optional)', + required: false, + example: '2023-05-30T12:34:56.789Z', + }) + @IsDateString() + @IsOptional() + date: Date; +} diff --git a/src/modules/file/dto/update-file-meta.dto.ts b/src/modules/file/dto/update-file-meta.dto.ts new file mode 100644 index 000000000..c10de7d08 --- /dev/null +++ b/src/modules/file/dto/update-file-meta.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class UpdateFileMetaDto { + @IsString() + @ApiProperty({ + example: 'New name', + description: 'The name the file is going to be updated to', + }) + plainName: string; +} diff --git a/src/modules/folder/dto/create-folder.dto.ts b/src/modules/folder/dto/create-folder.dto.ts new file mode 100644 index 000000000..0836f803b --- /dev/null +++ b/src/modules/folder/dto/create-folder.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { FolderAttributes } from '../../folder/folder.attributes'; + +export class CreateFolderDto { + @ApiProperty({ + example: 'Untitled Folder', + description: 'Folder name', + }) + @IsNotEmpty() + plainName: FolderAttributes['plainName']; + + @ApiProperty({ + example: '79a88429-b45a-4ae7-90f1-c351b6882670', + description: 'Uuid of the parent folder', + }) + @IsNotEmpty() + @IsUUID('4') + parentFolderUuid: FolderAttributes['uuid']; +} diff --git a/src/modules/folder/dto/update-folder-meta.dto.ts b/src/modules/folder/dto/update-folder-meta.dto.ts new file mode 100644 index 000000000..f4b88d5b4 --- /dev/null +++ b/src/modules/folder/dto/update-folder-meta.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateFolderMetaDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + example: 'Untitled Folder', + description: 'New name', + }) + plainName: string; +} diff --git a/src/modules/gateway/dto/delete-workspace.dto.ts b/src/modules/gateway/dto/delete-workspace.dto.ts new file mode 100644 index 000000000..d8aeec174 --- /dev/null +++ b/src/modules/gateway/dto/delete-workspace.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + +export class DeleteWorkspaceDto { + @ApiProperty({ + example: 'Id of the owner', + description: 'Uuid of the owner of the space', + }) + @IsNotEmpty() + ownerId: string; +} diff --git a/src/modules/gateway/dto/initialize-workspace.dto.ts b/src/modules/gateway/dto/initialize-workspace.dto.ts new file mode 100644 index 000000000..2cf7daf73 --- /dev/null +++ b/src/modules/gateway/dto/initialize-workspace.dto.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional } from 'class-validator'; + +export class InitializeWorkspaceDto { + @ApiProperty({ + example: 'Id of the owner', + description: 'Uuid of the owner of the space', + }) + @IsNotEmpty() + ownerId: string; + + @ApiProperty({ + example: 'Address from billing', + description: 'Address of the workspace', + }) + @IsOptional() + address?: string; + + @ApiProperty({ + example: '312321312', + description: 'Workspace max space in bytes', + }) + @IsNotEmpty() + @IsNumber() + maxSpaceBytes: number; +} diff --git a/src/modules/gateway/dto/update-workspace-storage.dto.ts b/src/modules/gateway/dto/update-workspace-storage.dto.ts new file mode 100644 index 000000000..0da5ea8fd --- /dev/null +++ b/src/modules/gateway/dto/update-workspace-storage.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber } from 'class-validator'; + +export class UpdateWorkspaceStorageDto { + @ApiProperty({ + example: 'Id of the owner', + description: 'Uuid of the owner of the space', + }) + @IsNotEmpty() + ownerId: string; + + @ApiProperty({ + example: '312321312', + description: 'Workspace max space in bytes', + }) + @IsNotEmpty() + @IsNumber() + maxSpaceBytes: number; +} diff --git a/src/modules/workspaces/dto/accept-workspace-invite.dto.ts b/src/modules/workspaces/dto/accept-workspace-invite.dto.ts new file mode 100644 index 000000000..1d3c824a7 --- /dev/null +++ b/src/modules/workspaces/dto/accept-workspace-invite.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { WorkspaceInviteAttributes } from '../attributes/workspace-invite.attribute'; + +export class AcceptWorkspaceInviteDto { + @ApiProperty({ + example: '0f8fad5b-d9cb-469f-a165-70867728950e', + description: 'id of the invitation', + }) + @IsUUID() + @IsNotEmpty() + inviteId: WorkspaceInviteAttributes['id']; +} diff --git a/src/modules/workspaces/dto/change-user-assigned-space.dto.ts b/src/modules/workspaces/dto/change-user-assigned-space.dto.ts new file mode 100644 index 000000000..3fce9d459 --- /dev/null +++ b/src/modules/workspaces/dto/change-user-assigned-space.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsPositive } from 'class-validator'; +import { WorkspaceInvite } from '../domains/workspace-invite.domain'; + +export class ChangeUserAssignedSpaceDto { + @ApiProperty({ + example: '1073741824', + description: 'New Space assigned to user in bytes', + }) + @IsNotEmpty() + @IsPositive() + spaceLimit: WorkspaceInvite['spaceLimit']; +} diff --git a/src/modules/workspaces/dto/create-team.dto.ts b/src/modules/workspaces/dto/create-team.dto.ts new file mode 100644 index 000000000..81992a275 --- /dev/null +++ b/src/modules/workspaces/dto/create-team.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional } from 'class-validator'; +import { WorkspaceTeam } from '../domains/workspace-team.domain'; + +export class CreateTeamDto { + @ApiProperty({ + example: 'Designers team', + description: 'Name of the team to be created', + }) + @IsNotEmpty() + name: WorkspaceTeam['name']; + + @ApiProperty({ + example: 'e54c5cc0-3a12-4537-9646-251ec0f0dbe4', + description: 'Uuid of the user to assign as manager', + }) + @IsOptional() + managerId?: WorkspaceTeam['name']; +} diff --git a/src/modules/workspaces/dto/create-workspace-file.dto.ts b/src/modules/workspaces/dto/create-workspace-file.dto.ts new file mode 100644 index 000000000..d0987f276 --- /dev/null +++ b/src/modules/workspaces/dto/create-workspace-file.dto.ts @@ -0,0 +1,3 @@ +import { CreateFileDto } from '../../file/dto/create-file.dto'; + +export class CreateWorkspaceFileDto extends CreateFileDto {} diff --git a/src/modules/workspaces/dto/create-workspace-folder.dto.ts b/src/modules/workspaces/dto/create-workspace-folder.dto.ts new file mode 100644 index 000000000..947f4e21e --- /dev/null +++ b/src/modules/workspaces/dto/create-workspace-folder.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { FolderAttributes } from '../../folder/folder.attributes'; + +export class CreateWorkspaceFolderDto { + @ApiProperty({ + example: 'Untitled Folder', + description: 'Folder name', + }) + @IsNotEmpty() + name: FolderAttributes['name']; + + @ApiProperty({ + example: '79a88429-b45a-4ae7-90f1-c351b6882670', + description: 'Uuid of the parent folder', + }) + @IsNotEmpty() + @IsUUID('4') + parentFolderUuid: FolderAttributes['uuid']; +} diff --git a/src/modules/workspaces/dto/create-workspace-invite.dto.ts b/src/modules/workspaces/dto/create-workspace-invite.dto.ts new file mode 100644 index 000000000..b7f400833 --- /dev/null +++ b/src/modules/workspaces/dto/create-workspace-invite.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsPositive } from 'class-validator'; +import { User } from '../../user/user.domain'; +import { WorkspaceInvite } from '../domains/workspace-invite.domain'; + +export class CreateWorkspaceInviteDto { + @ApiProperty({ + example: 'invited_user@internxt.com', + description: 'The email of the user you want to invite', + }) + @IsNotEmpty() + invitedUser: User['email']; + + @ApiProperty({ + example: '1073741824', + description: 'Space assigned to user in bytes', + }) + @IsNotEmpty() + @IsPositive() + spaceLimit: WorkspaceInvite['spaceLimit']; + + @ApiProperty({ + example: 'encrypted encryption key', + description: + "Owner's encryption key encrypted with the invited user's public key.", + }) + @IsNotEmpty() + encryptionKey: WorkspaceInvite['encryptionKey']; + + @ApiProperty({ + example: 'Hello, join to my workspace', + description: 'Message to include in the invitation.', + }) + message?: string; + + @ApiProperty({ + example: 'aes-256-gcm', + description: 'Encryption algorithm used to encrypt the encryption key.', + }) + @IsNotEmpty() + encryptionAlgorithm: WorkspaceInvite['encryptionAlgorithm']; +} diff --git a/src/modules/workspaces/dto/edit-team-data.dto.ts b/src/modules/workspaces/dto/edit-team-data.dto.ts new file mode 100644 index 000000000..b87fe95df --- /dev/null +++ b/src/modules/workspaces/dto/edit-team-data.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; +import { WorkspaceTeam } from '../domains/workspace-team.domain'; + +export class EditTeamDto { + @ApiProperty({ + example: 'Designers team', + description: 'New name of the team', + }) + @IsNotEmpty() + @IsString() + name: WorkspaceTeam['name']; +} diff --git a/src/modules/workspaces/dto/edit-workspace-details-dto.ts b/src/modules/workspaces/dto/edit-workspace-details-dto.ts new file mode 100644 index 000000000..d967fe4e7 --- /dev/null +++ b/src/modules/workspaces/dto/edit-workspace-details-dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString, Length } from 'class-validator'; +import { Workspace } from '../domains/workspaces.domain'; + +export class EditWorkspaceDetailsDto { + @ApiProperty({ + example: 'Internxt', + description: 'Name of the workspace', + }) + @IsOptional() + @IsString() + @Length(3, 50) + name?: Workspace['name']; + + @ApiProperty({ + example: + 'Our goal is to create a cloud storage ecosystem that gives users total control, security, and privacy of the files and information online.', + description: 'Description of the workspace', + }) + @IsOptional() + @IsString() + @Length(0, 150) + description?: Workspace['description']; +} diff --git a/src/modules/workspaces/dto/get-items-inside-shared-folder.dto.ts b/src/modules/workspaces/dto/get-items-inside-shared-folder.dto.ts new file mode 100644 index 000000000..4d811246a --- /dev/null +++ b/src/modules/workspaces/dto/get-items-inside-shared-folder.dto.ts @@ -0,0 +1,42 @@ +import { IsOptional, IsString, IsInt, Min, Max } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { OrderBy } from '../../../common/order.type'; + +export class GetItemsInsideSharedFolderDtoQuery { + @ApiPropertyOptional({ + description: 'Order by', + example: 'name:asc', + }) + @IsOptional() + @IsString() + orderBy?: OrderBy; + + @ApiPropertyOptional({ + description: 'Token', + }) + @IsOptional() + @IsString() + token?: string; + + @ApiPropertyOptional({ + description: 'Page number', + default: 0, + }) + @IsOptional() + @IsInt() + @Min(0) + @Type(() => Number) + page?: number = 0; + + @ApiPropertyOptional({ + description: 'Items per page', + default: 50, + }) + @IsOptional() + @IsInt() + @Min(1) + @Max(50) + @Type(() => Number) + perPage?: number = 50; +} diff --git a/src/modules/workspaces/dto/pagination.dto.ts b/src/modules/workspaces/dto/pagination.dto.ts new file mode 100644 index 000000000..273b0e8ee --- /dev/null +++ b/src/modules/workspaces/dto/pagination.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNumber, IsOptional, Max, Min } from 'class-validator'; + +export class PaginationQueryDto { + @ApiProperty({ + description: 'Items per page', + example: 3, + required: false, + minimum: 0, + maximum: 50, + }) + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(0) + @Max(50) + limit?: number; + + @ApiProperty({ + description: 'Offset for pagination', + example: 0, + required: false, + minimum: 0, + }) + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(0) + offset?: number; +} diff --git a/src/modules/workspaces/dto/setup-workspace.dto.ts b/src/modules/workspaces/dto/setup-workspace.dto.ts new file mode 100644 index 000000000..fac303b40 --- /dev/null +++ b/src/modules/workspaces/dto/setup-workspace.dto.ts @@ -0,0 +1,34 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional } from 'class-validator'; +import { Workspace } from '../domains/workspaces.domain'; +import { WorkspaceUserAttributes } from '../attributes/workspace-users.attributes'; + +export class SetupWorkspaceDto { + @ApiProperty({ + example: 'My workspace', + description: 'Name of the workspace to be created', + }) + @IsOptional() + name?: Workspace['name']; + + @ApiProperty({ + example: 'Address', + description: 'Address of the workspace', + }) + @IsOptional() + address?: Workspace['address']; + + @ApiProperty({ + example: 'My workspae', + description: 'Workspace description', + }) + @IsOptional() + description?: Workspace['description']; + + @ApiProperty({ + example: 'Encrypted key in base64', + description: 'Owner mnemnonic encrypted with their public key in base64', + }) + @IsNotEmpty() + encryptedMnemonic: WorkspaceUserAttributes['key']; +} diff --git a/src/modules/workspaces/dto/share-item-with-team.dto.ts b/src/modules/workspaces/dto/share-item-with-team.dto.ts new file mode 100644 index 000000000..9297921df --- /dev/null +++ b/src/modules/workspaces/dto/share-item-with-team.dto.ts @@ -0,0 +1,34 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; +import { WorkspaceItemUser } from '../domains/workspace-item-user.domain'; +import { WorkspaceTeam } from '../domains/workspace-team.domain'; + +export class ShareItemWithTeamDto { + @ApiProperty({ + example: 'uuid', + description: 'The uuid of the item to share', + }) + @IsNotEmpty() + itemId: WorkspaceItemUser['itemId']; + + @ApiProperty({ + example: 'file | folder', + description: 'The type of the resource to share', + }) + @IsNotEmpty() + itemType: WorkspaceItemUser['itemType']; + + @ApiProperty({ + example: '84f47d08-dc7c-43dc-b27c-bec4edaa9598', + description: "Workspace's team id you want to share this file with", + }) + @IsNotEmpty() + sharedWith: WorkspaceTeam['id']; + + @ApiProperty({ + example: '84f47d08-dc7c-43dc-b27c-bec4edaa9598', + description: 'Role of the team regarding the item.', + }) + @IsNotEmpty() + roleId: string; +} diff --git a/src/modules/workspaces/dto/workspace-invitations-pagination.dto.ts b/src/modules/workspaces/dto/workspace-invitations-pagination.dto.ts new file mode 100644 index 000000000..abd137ac8 --- /dev/null +++ b/src/modules/workspaces/dto/workspace-invitations-pagination.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt, Min, Max } from 'class-validator'; + +export class WorkspaceInvitationsPagination { + @Type(() => Number) + @IsInt() + @Min(1) + @Max(25) + @ApiProperty({ + example: 1, + description: 'Number of items to request', + required: true, + type: Number, + }) + limit: number; + + @Type(() => Number) + @IsInt() + @Min(0) + @ApiProperty({ + example: 0, + description: 'Number of items to skip', + required: true, + type: Number, + }) + offset: number; +} diff --git a/src/modules/workspaces/pipes/validate-uuid.pipe.spec.ts b/src/modules/workspaces/pipes/validate-uuid.pipe.spec.ts new file mode 100644 index 000000000..ca4e21d97 --- /dev/null +++ b/src/modules/workspaces/pipes/validate-uuid.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ValidateUUIDPipe } from './validate-uuid.pipe'; +import { BadRequestException, ArgumentMetadata } from '@nestjs/common'; + +describe('ValidateUUIDPipe', () => { + let pipe: ValidateUUIDPipe; + + beforeEach(() => { + pipe = new ValidateUUIDPipe(); + }); + + it('should be defined', () => { + expect(pipe).toBeDefined(); + }); + + it('When uuid given is valid, then it should return the same value', () => { + const validUUID = '123e4567-e89b-12d3-a456-426614174000'; + const metadata: ArgumentMetadata = { + type: 'body', + metatype: null, + data: 'testUUID', + }; + expect(pipe.transform(validUUID, metadata)).toEqual(validUUID); + }); + + it('When UUID is invalid, then it should throw', () => { + const invalidUUID = 'invalid-uuid'; + const metadata: ArgumentMetadata = { + type: 'body', + metatype: null, + data: 'testUUID', + }; + expect(() => pipe.transform(invalidUUID, metadata)).toThrow( + BadRequestException, + ); + expect(() => pipe.transform(invalidUUID, metadata)).toThrow( + `Value of 'testUUID' is not a valid UUID.`, + ); + }); + + it('When UUID is null, then it should throw', () => { + const nullUUID = null; + const metadata: ArgumentMetadata = { + type: 'body', + metatype: null, + data: 'testUUID', + }; + expect(() => pipe.transform(nullUUID, metadata)).toThrow( + BadRequestException, + ); + expect(() => pipe.transform(nullUUID, metadata)).toThrow( + `Value of 'testUUID' is not a valid UUID.`, + ); + }); + + it('When UUID is undefined, then it should throw', () => { + const undefinedUUID = undefined; + const metadata: ArgumentMetadata = { + type: 'body', + metatype: null, + data: 'testUUID', + }; + expect(() => pipe.transform(undefinedUUID, metadata)).toThrow( + BadRequestException, + ); + expect(() => pipe.transform(undefinedUUID, metadata)).toThrow( + `Value of 'testUUID' is not a valid UUID.`, + ); + }); +}); diff --git a/src/modules/workspaces/pipes/validate-uuid.pipe.ts b/src/modules/workspaces/pipes/validate-uuid.pipe.ts new file mode 100644 index 000000000..76e9ef7b3 --- /dev/null +++ b/src/modules/workspaces/pipes/validate-uuid.pipe.ts @@ -0,0 +1,18 @@ +import { + PipeTransform, + Injectable, + ArgumentMetadata, + BadRequestException, +} from '@nestjs/common'; +import { isUUID } from 'class-validator'; + +@Injectable() +export class ValidateUUIDPipe implements PipeTransform { + transform(value: any, metadata: ArgumentMetadata) { + if (!value || !isUUID(value)) + throw new BadRequestException( + `Value of '${metadata.data}' is not a valid UUID.`, + ); + return value; + } +}