feat: add media service
This commit is contained in:
parent
36c058f885
commit
b2e02e0ae2
@ -41,6 +41,7 @@
|
||||
"class-validator": "^0.14.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cross-env": "^10.1.0",
|
||||
"multer": "^2.1.1",
|
||||
"prom-client": "^15.1.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
@ -55,6 +56,7 @@
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/multer": "^2.1.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
|
||||
@ -4,6 +4,7 @@ import { PassportModule } from '@teacinema/passport'
|
||||
|
||||
import { AccountModule } from '../modules/account/account.module'
|
||||
import { AuthModule } from '../modules/auth/auth.module'
|
||||
import { MediaModule } from '../modules/media/media.module'
|
||||
import { UsersModule } from '../modules/users/users.module'
|
||||
import { ObservabilityModule } from '../observability/observability.module'
|
||||
|
||||
@ -24,6 +25,7 @@ import { getPassportConfig } from './config'
|
||||
ObservabilityModule,
|
||||
AuthModule,
|
||||
AccountModule,
|
||||
MediaModule,
|
||||
UsersModule,
|
||||
PassportModule.registerAsync({
|
||||
useFactory: getPassportConfig,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import type { ClientGrpc } from '@nestjs/microservices'
|
||||
import { InjectGrpc } from '@teacinema/common'
|
||||
import { AccountServiceClient } from '@teacinema/contracts/gen/account'
|
||||
import { AccountServiceClient } from '@teacinema/contracts/gen/ts/account'
|
||||
|
||||
import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client'
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import type { ClientGrpc } from '@nestjs/microservices'
|
||||
import { InjectGrpc } from '@teacinema/common'
|
||||
import { AuthServiceClient } from '@teacinema/contracts/gen/auth'
|
||||
import { AuthServiceClient } from '@teacinema/contracts/gen/ts/auth'
|
||||
|
||||
import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client'
|
||||
|
||||
|
||||
13
src/modules/media/media.grpc.ts
Normal file
13
src/modules/media/media.grpc.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import type { ClientGrpc } from '@nestjs/microservices'
|
||||
import { InjectGrpc } from '@teacinema/common'
|
||||
import { MediaServiceClient } from '@teacinema/contracts/gen/ts/media'
|
||||
|
||||
import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client'
|
||||
|
||||
@Injectable()
|
||||
export class MediaClientGrpc extends AbstractGrpcClient<MediaServiceClient> {
|
||||
constructor(@InjectGrpc('MEDIA_PACKAGE') client: ClientGrpc) {
|
||||
super(client, 'MediaService')
|
||||
}
|
||||
}
|
||||
11
src/modules/media/media.module.ts
Normal file
11
src/modules/media/media.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { GrpcModule } from '@teacinema/common'
|
||||
|
||||
import { MediaClientGrpc } from './media.grpc'
|
||||
|
||||
@Module({
|
||||
imports: [GrpcModule.register(['MEDIA_PACKAGE'])],
|
||||
providers: [MediaClientGrpc],
|
||||
exports: [MediaClientGrpc]
|
||||
})
|
||||
export class MediaModule {}
|
||||
@ -1,14 +1,28 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
FileTypeValidator,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Patch
|
||||
MaxFileSizeValidator,
|
||||
ParseFilePipe,
|
||||
Patch,
|
||||
UploadedFile,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common'
|
||||
import { ApiBearerAuth, ApiOkResponse, ApiOperation } from '@nestjs/swagger'
|
||||
import { FileInterceptor } from '@nestjs/platform-express'
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
ApiOkResponse,
|
||||
ApiOperation
|
||||
} from '@nestjs/swagger'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
|
||||
import { CurrentUser, Protected } from '../../shared/decorators'
|
||||
import { MediaClientGrpc } from '../media/media.grpc'
|
||||
|
||||
import {
|
||||
GetMeResponseDto,
|
||||
@ -17,9 +31,15 @@ import {
|
||||
} from './dto'
|
||||
import { UsersClientGrpc } from './users.grpc'
|
||||
|
||||
const AVATAR_MAX_SIZE = 10_485_760 // 10 MB
|
||||
const AVATAR_ALLOWED_MIME_TYPES = /image\/(jpg|jpeg|gif|png)/
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly client: UsersClientGrpc) {}
|
||||
constructor(
|
||||
private readonly users: UsersClientGrpc,
|
||||
private readonly media: MediaClientGrpc
|
||||
) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Get Me',
|
||||
@ -31,7 +51,7 @@ export class UsersController {
|
||||
@Get('me')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
public async getMe(@CurrentUser() userId: string) {
|
||||
const { user } = await this.client.call('getMe', { id: userId })
|
||||
const { user } = await this.users.call('getMe', { id: userId })
|
||||
return user
|
||||
}
|
||||
|
||||
@ -48,6 +68,62 @@ export class UsersController {
|
||||
@CurrentUser() userId: string,
|
||||
@Body() dto: PatchUserRequestDto
|
||||
) {
|
||||
return this.client.call('patchUser', { userId, ...dto })
|
||||
return this.users.call('patchUser', { userId, ...dto })
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Update user avatar',
|
||||
description: 'Update user avatar'
|
||||
})
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({
|
||||
description: 'Avatar file',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { file: { type: 'string', format: 'binary' } },
|
||||
required: ['file']
|
||||
}
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ type: PatchUserResponseDto })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { file: { type: 'string', format: 'binary' } },
|
||||
required: ['file']
|
||||
}
|
||||
})
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', { limits: { fileSize: AVATAR_MAX_SIZE } })
|
||||
)
|
||||
@Protected()
|
||||
@Patch('me/avatar')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
public async changeAvatar(
|
||||
@CurrentUser() userId: string,
|
||||
@UploadedFile(
|
||||
new ParseFilePipe({
|
||||
validators: [
|
||||
new MaxFileSizeValidator({ maxSize: AVATAR_MAX_SIZE }),
|
||||
new FileTypeValidator({ fileType: AVATAR_ALLOWED_MIME_TYPES })
|
||||
]
|
||||
})
|
||||
)
|
||||
file: Express.Multer.File
|
||||
) {
|
||||
const response = await this.media.call('upload', {
|
||||
fileName: randomBytes(16).toString('hex'),
|
||||
folder: 'users',
|
||||
contentType: file.mimetype,
|
||||
data: new Uint8Array(file.buffer),
|
||||
resizeWidth: 512,
|
||||
resizeHeight: 512
|
||||
})
|
||||
|
||||
return this.users.call('patchUser', {
|
||||
userId,
|
||||
avatar: response.key
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import type { ClientGrpc } from '@nestjs/microservices'
|
||||
import { InjectGrpc } from '@teacinema/common'
|
||||
import { UsersServiceClient } from '@teacinema/contracts/gen/users'
|
||||
import { UsersServiceClient } from '@teacinema/contracts/gen/ts/users'
|
||||
|
||||
import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client'
|
||||
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { GrpcModule } from '@teacinema/common'
|
||||
|
||||
import { MediaModule } from '../media/media.module'
|
||||
|
||||
import { UsersController } from './users.controller'
|
||||
import { UsersClientGrpc } from './users.grpc'
|
||||
|
||||
@Module({
|
||||
imports: [GrpcModule.register(['USERS_PACKAGE'])],
|
||||
imports: [GrpcModule.register(['USERS_PACKAGE']), MediaModule],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersClientGrpc],
|
||||
exports: [UsersClientGrpc]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { applyDecorators, UseGuards } from '@nestjs/common'
|
||||
import { Role } from '@teacinema/contracts/gen/account'
|
||||
import { Role } from '@teacinema/contracts/gen/ts/account'
|
||||
|
||||
import { AuthGuard, RolesGuard } from '../guards'
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common'
|
||||
import { Role } from '@teacinema/contracts/gen/account'
|
||||
import { Role } from '@teacinema/contracts/gen/ts/account'
|
||||
|
||||
export const ROLES_KEY = 'required_roles'
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
NotFoundException
|
||||
} from '@nestjs/common'
|
||||
import { Reflector } from '@nestjs/core'
|
||||
import { Role } from '@teacinema/contracts/gen/account'
|
||||
import { Role } from '@teacinema/contracts/gen/ts/account'
|
||||
import { Request } from 'express'
|
||||
|
||||
import { AccountClientGrpc } from '../../modules/account/account.grpc'
|
||||
|
||||
67
yarn.lock
67
yarn.lock
@ -329,9 +329,9 @@
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@borewit/text-codec@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@borewit/text-codec/-/text-codec-0.2.1.tgz#5d171538907a8cb395fdc2eb5e8f7947d96c7f2f"
|
||||
integrity sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@borewit/text-codec/-/text-codec-0.2.2.tgz#75025f735c0983b3a871668804a57387e3649375"
|
||||
integrity sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==
|
||||
|
||||
"@bufbuild/protobuf@^2.0.0", "@bufbuild/protobuf@^2.10.2":
|
||||
version "2.11.0"
|
||||
@ -1028,12 +1028,12 @@
|
||||
tslib "2.8.1"
|
||||
|
||||
"@nestjs/common@^11.1.16":
|
||||
version "11.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.16.tgz#1f4061f66d7ae63e407f8d2be2aaeaf83097332b"
|
||||
integrity sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ==
|
||||
version "11.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.17.tgz#58a330f3f6748cf92b0a244a2a232cfddba1bfd2"
|
||||
integrity sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==
|
||||
dependencies:
|
||||
uid "2.0.2"
|
||||
file-type "21.3.0"
|
||||
file-type "21.3.2"
|
||||
iterare "1.2.1"
|
||||
load-esm "1.0.3"
|
||||
tslib "2.8.1"
|
||||
@ -1094,9 +1094,9 @@
|
||||
tslib "2.8.1"
|
||||
|
||||
"@nestjs/microservices@^11.1.16":
|
||||
version "11.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-11.1.16.tgz#6f445c44adcee88af983830e0667c93ccefe2869"
|
||||
integrity sha512-eG/ArIq0UJyR3i/GTYuApA4OZylhuLGacVaVT9mMxQgT7ZTpp5CZgOwLNdcUUdOS6qypK3waG1m2AC54xzdf0Q==
|
||||
version "11.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-11.1.17.tgz#f2b1aa25e24532377477a437583f1af002118602"
|
||||
integrity sha512-rubBPEEXS9PwA3LwdNN/ytjEUmON+d8+4fbiUCIFGW8i+rR/g36vrpwuvoAzcGpxlu45/TpZPfHTb0nJciG9Yw==
|
||||
dependencies:
|
||||
iterare "1.2.1"
|
||||
tslib "2.8.1"
|
||||
@ -1997,9 +1997,9 @@
|
||||
"@sinonjs/commons" "^3.0.1"
|
||||
|
||||
"@teacinema/common@^1.0.0":
|
||||
version "1.1.3"
|
||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcommon/-/1.1.3/common-1.1.3.tgz#73c8e3642f37d9c6574f6a1e5b542dd6cbfdbf2f"
|
||||
integrity sha512-Bk2JzG3P1K7YFoFywtSP3zHxNfUpy+NyboqChj+4GZ6JuoWg2V1wkrjMljeE90cPOaqCiNHxk08zX037waiKJg==
|
||||
version "1.1.6"
|
||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcommon/-/1.1.6/common-1.1.6.tgz#4b43fc7c1f19cef0ba138a3a7988918f54f52ebb"
|
||||
integrity sha512-a0wU0EaJ92PdMlwFQ5pA0EZ9G1/f9Gca8CCN2qUy75q0HldNTGKKYUEyDHx4Y0z16N1e3/RO12gagebOrV6Nbg==
|
||||
dependencies:
|
||||
"@nestjs/common" "^11.1.16"
|
||||
"@nestjs/config" "^4.0.3"
|
||||
@ -2007,9 +2007,9 @@
|
||||
"@teacinema/contracts" "^1.1.3"
|
||||
|
||||
"@teacinema/contracts@^1.0.0", "@teacinema/contracts@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.1.3/contracts-1.1.3.tgz#e08600a8dc09977dae35654d372e29e411029e33"
|
||||
integrity sha512-kwcSh2YIxmoBJQ56L+tQpGj12ySwNwf9KRNcUqhPffLPO60PR5aiKA2eN2SfKc23TKp0YoSkdC9LWeor0a9Ysg==
|
||||
version "1.1.8"
|
||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.1.8/contracts-1.1.8.tgz#e96c23a957d2682503c157250672eb4c46bf4ef9"
|
||||
integrity sha512-wSFTntA0IoQrC/iYz3rylJW6qK6KGWa25oQrl/n55aSOcMk1ACmSBt0aqAPML86SQWAsn7yikpr/foaTgJH3zg==
|
||||
dependencies:
|
||||
"@nestjs/microservices" "^11.1.12"
|
||||
protoc "33.4.0"
|
||||
@ -2186,7 +2186,7 @@
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express@^5.0.0":
|
||||
"@types/express@*", "@types/express@^5.0.0":
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.6.tgz#2d724b2c990dcb8c8444063f3580a903f6d500cc"
|
||||
integrity sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==
|
||||
@ -2244,6 +2244,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547"
|
||||
integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==
|
||||
|
||||
"@types/multer@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/multer/-/multer-2.1.0.tgz#c6ae866b9a3d9310b477d021a78b33feda092358"
|
||||
integrity sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/mysql@2.15.27":
|
||||
version "2.15.27"
|
||||
resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.27.tgz#fb13b0e8614d39d42f40f381217ec3215915f1e9"
|
||||
@ -3846,6 +3853,16 @@ file-type@21.3.0:
|
||||
token-types "^6.1.1"
|
||||
uint8array-extras "^1.4.0"
|
||||
|
||||
file-type@21.3.2:
|
||||
version "21.3.2"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-21.3.2.tgz#b28bb3a19ea4b03f59af31546b9d95ee8a3623eb"
|
||||
integrity sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==
|
||||
dependencies:
|
||||
"@tokenizer/inflate" "^0.4.1"
|
||||
strtok3 "^10.3.4"
|
||||
token-types "^6.1.1"
|
||||
uint8array-extras "^1.4.0"
|
||||
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
@ -5093,6 +5110,16 @@ multer@2.0.2:
|
||||
type-is "^1.6.18"
|
||||
xtend "^4.0.2"
|
||||
|
||||
multer@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/multer/-/multer-2.1.1.tgz#122d819244fbdfee1efddd9147426691014385b7"
|
||||
integrity sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==
|
||||
dependencies:
|
||||
append-field "^1.0.0"
|
||||
busboy "^1.6.0"
|
||||
concat-stream "^2.0.0"
|
||||
type-is "^1.6.18"
|
||||
|
||||
mute-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b"
|
||||
@ -5883,9 +5910,9 @@ strip-json-comments@^3.1.1:
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strtok3@^10.3.4:
|
||||
version "10.3.4"
|
||||
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-10.3.4.tgz#793ebd0d59df276a085586134b73a406e60be9c1"
|
||||
integrity sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==
|
||||
version "10.3.5"
|
||||
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-10.3.5.tgz#7213285da0dc3dec0fc8ce5df4b8b7a733f14360"
|
||||
integrity sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==
|
||||
dependencies:
|
||||
"@tokenizer/token" "^0.3.0"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user