-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #287 from internxt/feat/workspaces-main
[_] feat/workspaces (WIP)
- Loading branch information
Showing
67 changed files
with
13,586 additions
and
90 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { ConfigModule, ConfigService } from '@nestjs/config'; | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { AvatarService } from './avatar.service'; | ||
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'; | ||
import { mockClient } from 'aws-sdk-client-mock'; | ||
import configuration from '../../config/configuration'; | ||
import { v4 } from 'uuid'; | ||
import * as s3RequestPresigner from '@aws-sdk/s3-request-presigner'; | ||
|
||
jest.mock('@aws-sdk/s3-request-presigner'); | ||
|
||
describe('Avatar Service', () => { | ||
let service: AvatarService; | ||
let mockS3Client; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
imports: [ | ||
ConfigModule.forRoot({ | ||
envFilePath: [`.env.${process.env.NODE_ENV}`], | ||
load: [configuration], | ||
isGlobal: true, | ||
}), | ||
], | ||
providers: [ | ||
{ | ||
provide: AvatarService, | ||
useFactory: async (configService: ConfigService) => { | ||
return new AvatarService(configService); | ||
}, | ||
inject: [ConfigService], | ||
}, | ||
], | ||
}).compile(); | ||
|
||
service = module.get<AvatarService>(AvatarService); | ||
mockS3Client = mockClient(S3Client); | ||
}); | ||
|
||
describe('Get avatar download url', () => { | ||
it('When avatar key is null then it should throw an error', async () => { | ||
const avatarKey = null; | ||
jest | ||
.spyOn(s3RequestPresigner, 'getSignedUrl') | ||
.mockRejectedValueOnce(new Error()); | ||
await expect(service.getDownloadUrl(avatarKey)).rejects.toThrow(); | ||
}); | ||
it('When avatar key is not null then it should return an url', async () => { | ||
const avatarKey = v4(); | ||
const expectedUrl = `https://avatar.network.com/${avatarKey}`; | ||
jest | ||
.spyOn(s3RequestPresigner, 'getSignedUrl') | ||
.mockResolvedValueOnce(expectedUrl); | ||
const response = await service.getDownloadUrl(avatarKey); | ||
expect(response).toBe(expectedUrl); | ||
}); | ||
}); | ||
|
||
describe('Delete avatar', () => { | ||
it('When deleting the avatar is successful, it should delete the avatar file and return true', async () => { | ||
const avatarKey = v4(); | ||
await expect(service.deleteAvatar(avatarKey)).resolves.toBeTruthy(); | ||
}); | ||
|
||
it('When the avatar deletion fails, then the error is propagated', async () => { | ||
const avatarKey = v4(); | ||
mockS3Client.on(DeleteObjectCommand).rejects(); | ||
await expect(service.deleteAvatar(avatarKey)).rejects.toThrow(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,72 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; | ||
import { GetObjectCommand, S3 } from '@aws-sdk/client-s3'; | ||
|
||
import { | ||
GetObjectCommand, | ||
S3Client, | ||
DeleteObjectCommand, | ||
} from '@aws-sdk/client-s3'; | ||
import { User } from '../../modules/user/user.domain'; | ||
import { Workspace } from '../../modules/workspaces/domains/workspaces.domain'; | ||
import { ConfigService } from '@nestjs/config'; | ||
|
||
@Injectable() | ||
export class AvatarService { | ||
async getDownloadUrl(avatar: User['avatar']): Promise<string> { | ||
const s3 = new S3({ | ||
endpoint: process.env.AVATAR_ENDPOINT, | ||
region: process.env.AVATAR_REGION, | ||
private configService: ConfigService; | ||
|
||
constructor(configService: ConfigService) { | ||
this.configService = configService; | ||
} | ||
|
||
AvatarS3Instance(): S3Client { | ||
return new S3Client({ | ||
endpoint: this.configService.get('avatar.endpoint'), | ||
region: this.configService.get('avatar.region'), | ||
credentials: { | ||
accessKeyId: process.env.AVATAR_ACCESS_KEY, | ||
secretAccessKey: process.env.AVATAR_SECRET_KEY, | ||
accessKeyId: this.configService.get('avatar.accessKey'), | ||
secretAccessKey: this.configService.get('avatar.secretKey'), | ||
}, | ||
forcePathStyle: process.env.AVATAR_FORCE_PATH_STYLE === 'true', | ||
forcePathStyle: | ||
this.configService.get('avatar.forcePathStyle') === 'true', | ||
}); | ||
} | ||
|
||
async getDownloadUrl( | ||
avatarKey: User['avatar'] | Workspace['avatar'], | ||
): Promise<string> { | ||
const s3Client = this.AvatarS3Instance(); | ||
const bucket = this.configService.get('avatar.bucket'); | ||
const url = await getSignedUrl( | ||
s3, | ||
s3Client, | ||
new GetObjectCommand({ | ||
Bucket: process.env.AVATAR_BUCKET, | ||
Key: avatar, | ||
Bucket: bucket, | ||
Key: avatarKey, | ||
}), | ||
{ | ||
expiresIn: 24 * 3600, | ||
}, | ||
); | ||
|
||
const endpointRewrite = process.env.AVATAR_ENDPOINT_REWRITE_FOR_SIGNED_URLS; | ||
const endpointRewrite = this.configService.get( | ||
'avatar.endpointForSignedUrls', | ||
); | ||
if (endpointRewrite) { | ||
return url.replace(process.env.AVATAR_ENDPOINT, endpointRewrite); | ||
const avatarEndpoint = this.configService.get('avatar.endpoint'); | ||
return url.replace(avatarEndpoint, endpointRewrite); | ||
} else { | ||
return url; | ||
} | ||
} | ||
|
||
async deleteAvatar( | ||
avatarKey: User['avatar'] | Workspace['avatar'], | ||
): Promise<boolean> { | ||
const s3Client = this.AvatarS3Instance(); | ||
const bucket = this.configService.get('avatar.bucket'); | ||
const deleteObjectCommand = new DeleteObjectCommand({ | ||
Bucket: bucket, | ||
Key: avatarKey, | ||
}); | ||
await s3Client.send(deleteObjectCommand); | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { S3Client, ObjectCannedACL } from '@aws-sdk/client-s3'; | ||
import { v4 } from 'uuid'; | ||
import multerS3 from 'multer-s3'; | ||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; | ||
import configuration from '../../config/configuration'; | ||
import { BadRequestException } from '@nestjs/common'; | ||
|
||
const { avatar: avatarConfig } = configuration(); | ||
|
||
export const avatarStorageS3Config: MulterOptions = { | ||
storage: multerS3({ | ||
s3: new S3Client({ | ||
endpoint: avatarConfig.endpoint, | ||
region: avatarConfig.region, | ||
credentials: { | ||
accessKeyId: avatarConfig.accessKey, | ||
secretAccessKey: avatarConfig.secretKey, | ||
}, | ||
forcePathStyle: avatarConfig.forcePathStyle === 'true', | ||
}), | ||
bucket: avatarConfig.bucket, | ||
acl: ObjectCannedACL.public_read, | ||
contentType: multerS3.AUTO_CONTENT_TYPE, | ||
key: (req, file, cb) => { | ||
const filenameKeyS3 = v4(); | ||
req.filenameKeyS3 = filenameKeyS3; | ||
cb(null, filenameKeyS3); | ||
}, | ||
}), | ||
fileFilter: (req, file, cb) => { | ||
if (file.mimetype.startsWith('image/')) { | ||
cb(null, true); | ||
} else { | ||
cb(new BadRequestException('Only image file are allowed'), false); | ||
} | ||
}, | ||
limits: { | ||
fileSize: 1024 * 1024 * 1, | ||
files: 1, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.