add s3, add change user avatar, profile
This commit is contained in:
parent
5bf182e627
commit
c63592448a
@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.12.2",
|
"@apollo/server": "^4.12.2",
|
||||||
|
"@aws-sdk/client-s3": "^3.842.0",
|
||||||
"@nestjs-modules/mailer": "^2.0.2",
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
"@nestjs/apollo": "^13.1.0",
|
"@nestjs/apollo": "^13.1.0",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
@ -46,6 +47,7 @@
|
|||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"geoip-lite": "^1.4.10",
|
"geoip-lite": "^1.4.10",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
|
"graphql-upload": "14",
|
||||||
"hi-base32": "^0.5.1",
|
"hi-base32": "^0.5.1",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
@ -55,7 +57,8 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1",
|
||||||
|
"sharp": "^0.34.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import { getGraphQLConfig } from '@/src/core/config/graphql.config'
|
|||||||
import { AccountModule } from '@/src/module/auth/account/account.module'
|
import { AccountModule } from '@/src/module/auth/account/account.module'
|
||||||
import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module'
|
import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module'
|
||||||
import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module'
|
import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module'
|
||||||
|
import { ProfileModule } from '@/src/module/auth/profile/profile.module'
|
||||||
import { SessionModule } from '@/src/module/auth/session/session.module'
|
import { SessionModule } from '@/src/module/auth/session/session.module'
|
||||||
import { TotpModule } from '@/src/module/auth/totp/totp.module'
|
import { TotpModule } from '@/src/module/auth/totp/totp.module'
|
||||||
import { VerificationModule } from '@/src/module/auth/verification/verification.module'
|
import { VerificationModule } from '@/src/module/auth/verification/verification.module'
|
||||||
import { CronModule } from '@/src/module/cron/cron.module'
|
import { CronModule } from '@/src/module/cron/cron.module'
|
||||||
import { MailModule } from '@/src/module/libs/mail/mail.module'
|
import { MailModule } from '@/src/module/libs/mail/mail.module'
|
||||||
|
import { StorageModule } from '@/src/module/libs/storage/storage.module'
|
||||||
import { IS_DEV } from '@/src/shared/util/is-dev.util'
|
import { IS_DEV } from '@/src/shared/util/is-dev.util'
|
||||||
import { ApolloDriver } from '@nestjs/apollo'
|
import { ApolloDriver } from '@nestjs/apollo'
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
@ -31,12 +33,14 @@ import { RedisModule } from './redis/redis.module'
|
|||||||
RedisModule,
|
RedisModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
CronModule,
|
CronModule,
|
||||||
|
StorageModule,
|
||||||
AccountModule,
|
AccountModule,
|
||||||
SessionModule,
|
SessionModule,
|
||||||
VerificationModule,
|
VerificationModule,
|
||||||
PasswordRecoveryModule,
|
PasswordRecoveryModule,
|
||||||
TotpModule,
|
TotpModule,
|
||||||
DeactivateModule,
|
DeactivateModule,
|
||||||
|
ProfileModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {}
|
export class CoreModule {}
|
||||||
|
|||||||
@ -16,6 +16,12 @@ input ChangePasswordInput {
|
|||||||
oldPassword: String!
|
oldPassword: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ChangeProfileInfoInput {
|
||||||
|
bio: String!
|
||||||
|
displayName: String!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
input CreateUserInput {
|
input CreateUserInput {
|
||||||
email: String!
|
email: String!
|
||||||
name: String!
|
name: String!
|
||||||
@ -60,6 +66,8 @@ input LoginInput {
|
|||||||
type Mutation {
|
type Mutation {
|
||||||
changeEmail(data: ChangeEmailInput!): UserModel!
|
changeEmail(data: ChangeEmailInput!): UserModel!
|
||||||
changePassword(data: ChangePasswordInput!): UserModel!
|
changePassword(data: ChangePasswordInput!): UserModel!
|
||||||
|
changeProfileAvatar(avatar: Upload!): Boolean!
|
||||||
|
changeProfileInfo(data: ChangeProfileInfoInput!): UserModel!
|
||||||
clearSessionCookie: Boolean!
|
clearSessionCookie: Boolean!
|
||||||
createUser(data: CreateUserInput!): UserModel!
|
createUser(data: CreateUserInput!): UserModel!
|
||||||
deactivateAccount(data: DeactivateAccountInput!): AuthModel!
|
deactivateAccount(data: DeactivateAccountInput!): AuthModel!
|
||||||
@ -67,6 +75,7 @@ type Mutation {
|
|||||||
enableTotp(data: EnableTotpInput!): Boolean!
|
enableTotp(data: EnableTotpInput!): Boolean!
|
||||||
loginUser(data: LoginInput!): AuthModel!
|
loginUser(data: LoginInput!): AuthModel!
|
||||||
logoutUser: Boolean!
|
logoutUser: Boolean!
|
||||||
|
removeProfileAvatar: Boolean!
|
||||||
removeSession(id: String!): Boolean!
|
removeSession(id: String!): Boolean!
|
||||||
resetPassword(data: ResetPasswordInput!): Boolean!
|
resetPassword(data: ResetPasswordInput!): Boolean!
|
||||||
setNewPassword(data: NewPasswordInput!): Boolean!
|
setNewPassword(data: NewPasswordInput!): Boolean!
|
||||||
@ -108,6 +117,9 @@ type TotpModel {
|
|||||||
secret: String!
|
secret: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""The `Upload` scalar type represents a file upload."""
|
||||||
|
scalar Upload
|
||||||
|
|
||||||
type UserModel {
|
type UserModel {
|
||||||
avatar: String
|
avatar: String
|
||||||
bio: String
|
bio: String
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import RedisStore from 'connect-redis'
|
|||||||
import * as cookieParser from 'cookie-parser'
|
import * as cookieParser from 'cookie-parser'
|
||||||
import { CoreModule } from '@/src/core/core.module'
|
import { CoreModule } from '@/src/core/core.module'
|
||||||
import * as session from 'express-session'
|
import * as session from 'express-session'
|
||||||
|
import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(CoreModule)
|
const app = await NestFactory.create(CoreModule)
|
||||||
@ -16,6 +17,7 @@ async function bootstrap() {
|
|||||||
const redis = app.get(RedisService)
|
const redis = app.get(RedisService)
|
||||||
|
|
||||||
app.use(cookieParser(config.getOrThrow<string>('COOKIE_SECRET')))
|
app.use(cookieParser(config.getOrThrow<string>('COOKIE_SECRET')))
|
||||||
|
app.use(config.getOrThrow<string>('GRAPHQL_PREFIX'), graphqlUploadExpress())
|
||||||
|
|
||||||
app.useGlobalPipes(new ValidationPipe({
|
app.useGlobalPipes(new ValidationPipe({
|
||||||
transform: true,
|
transform: true,
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql'
|
||||||
|
import { IsNotEmpty, IsString, Matches, MaxLength } from 'class-validator'
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class ChangeProfileInfoInput {
|
||||||
|
@Field(() => String)
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Matches(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/)
|
||||||
|
name: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
displayName: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(300)
|
||||||
|
bio: string
|
||||||
|
}
|
||||||
8
backend/src/module/auth/profile/profile.module.ts
Normal file
8
backend/src/module/auth/profile/profile.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ProfileService } from './profile.service'
|
||||||
|
import { ProfileResolver } from './profile.resolver'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [ProfileResolver, ProfileService],
|
||||||
|
})
|
||||||
|
export class ProfileModule {}
|
||||||
39
backend/src/module/auth/profile/profile.resolver.ts
Normal file
39
backend/src/module/auth/profile/profile.resolver.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
||||||
|
import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'
|
||||||
|
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
||||||
|
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
||||||
|
import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe'
|
||||||
|
import { Args, Mutation, Resolver } from '@nestjs/graphql'
|
||||||
|
import { ProfileService } from './profile.service'
|
||||||
|
import { User } from '@/prisma/generated'
|
||||||
|
import * as Upload from 'graphql-upload/Upload.js'
|
||||||
|
import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
||||||
|
|
||||||
|
@Resolver('Profile')
|
||||||
|
export class ProfileResolver {
|
||||||
|
constructor(private readonly profileService: ProfileService) {}
|
||||||
|
|
||||||
|
@Authorization()
|
||||||
|
@Mutation(() => Boolean, { name: 'changeProfileAvatar' })
|
||||||
|
public async changeAvatar(
|
||||||
|
@Authorized() user: User,
|
||||||
|
@Args('avatar', { type: () => GraphQLUpload }, FileValidationPipe) file: Upload,
|
||||||
|
) {
|
||||||
|
return this.profileService.changeAvatar(user, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorization()
|
||||||
|
@Mutation(() => Boolean, { name: 'removeProfileAvatar' })
|
||||||
|
public async removeAvatar(@Authorized() user: User) {
|
||||||
|
return this.profileService.removeAvatar(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorization()
|
||||||
|
@Mutation(() => UserModel, { name: 'changeProfileInfo' })
|
||||||
|
public async changeInfo(
|
||||||
|
@Authorized() user: User,
|
||||||
|
@Args('data') input: ChangeProfileInfoInput,
|
||||||
|
) {
|
||||||
|
return this.profileService.changeInfo(user, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
backend/src/module/auth/profile/profile.service.ts
Normal file
75
backend/src/module/auth/profile/profile.service.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||||
|
import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'
|
||||||
|
import { ConflictException, Injectable } from '@nestjs/common'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
import { StorageService } from '../../libs/storage/storage.service'
|
||||||
|
import { User } from '@/prisma/generated'
|
||||||
|
import * as Upload from 'graphql-upload/Upload.js'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProfileService {
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly storageService: StorageService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async changeAvatar(user: User, file: Upload) {
|
||||||
|
if (user.avatar) {
|
||||||
|
await this.storageService.remove(user.avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
|
||||||
|
for await (const chunk of file.createReadStream()) {
|
||||||
|
chunks.push(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.concat(chunks)
|
||||||
|
const fileName = `/channels/${user.name}.webp`
|
||||||
|
|
||||||
|
const processedBuffer = await sharp(buffer, { animated: file.filename.endsWith('.gif') })
|
||||||
|
.resize(512, 512)
|
||||||
|
.webp()
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
await this.storageService.upload(processedBuffer, fileName, 'image/webp')
|
||||||
|
|
||||||
|
await this.prismaService.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { avatar: fileName },
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeAvatar(user: User) {
|
||||||
|
if (!user.avatar) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.storageService.remove(user.avatar)
|
||||||
|
|
||||||
|
await this.prismaService.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { avatar: null },
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public async changeInfo(user: User, input: ChangeProfileInfoInput) {
|
||||||
|
const { bio = user.bio, displayName = user.displayName, name = user.name } = input
|
||||||
|
|
||||||
|
const existingUser = await this.prismaService.user.findUnique({
|
||||||
|
where: { name },
|
||||||
|
})
|
||||||
|
if (existingUser && user.name !== name) {
|
||||||
|
throw new ConflictException('Пользователь с таким именем уже существует')
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prismaService.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { bio, displayName, name },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||||
import { MailService } from '@/src/module/libs/mail/mail.service'
|
import { MailService } from '@/src/module/libs/mail/mail.service'
|
||||||
|
import { StorageService } from '@/src/module/libs/storage/storage.service'
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule'
|
import { Cron, CronExpression } from '@nestjs/schedule'
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ export class CronService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
|
private readonly storageService: StorageService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +30,9 @@ export class CronService {
|
|||||||
for (const user of deactivatedAccounts) {
|
for (const user of deactivatedAccounts) {
|
||||||
console.log('Deactivate user', user.name, user.email)
|
console.log('Deactivate user', user.name, user.email)
|
||||||
await this.mailService.sendAccountDeletion(user.email)
|
await this.mailService.sendAccountDeletion(user.email)
|
||||||
|
if (user.avatar) {
|
||||||
|
await this.storageService.remove(user.avatar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaService.user.deleteMany({
|
await this.prismaService.user.deleteMany({
|
||||||
|
|||||||
9
backend/src/module/libs/storage/storage.module.ts
Normal file
9
backend/src/module/libs/storage/storage.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common'
|
||||||
|
import { StorageService } from './storage.service'
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [StorageService],
|
||||||
|
exports: [StorageService],
|
||||||
|
})
|
||||||
|
export class StorageModule {}
|
||||||
61
backend/src/module/libs/storage/storage.service.ts
Normal file
61
backend/src/module/libs/storage/storage.service.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
DeleteObjectCommand,
|
||||||
|
DeleteObjectCommandInput,
|
||||||
|
PutObjectCommand,
|
||||||
|
PutObjectCommandInput,
|
||||||
|
S3Client,
|
||||||
|
} from '@aws-sdk/client-s3'
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StorageService {
|
||||||
|
private readonly client: S3Client
|
||||||
|
private readonly bucket: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.client = new S3Client({
|
||||||
|
endpoint: this.configService.getOrThrow('S3_ENDPOINT'),
|
||||||
|
region: this.configService.getOrThrow('S3_REGION'),
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: this.configService.getOrThrow('S3_ACCESS_KEY_ID'),
|
||||||
|
secretAccessKey: this.configService.getOrThrow('S3_SECRET_KEY_ID'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bucket = this.configService.getOrThrow('S3_BUCKET_NAME')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async upload(buffer: Buffer, key: string, mimetype: string) {
|
||||||
|
const command: PutObjectCommandInput = {
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: String(key),
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: mimetype,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.send(new PutObjectCommand(command))
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
throw new BadRequestException('Ошибка при загрузке файла')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(key: string) {
|
||||||
|
const command: DeleteObjectCommandInput = {
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: String(key),
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.send(new DeleteObjectCommand(command))
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log('e', e);
|
||||||
|
throw new BadRequestException('Ошибка при удалении файла')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
backend/src/shared/pipes/file-validation.pipe.ts
Normal file
29
backend/src/shared/pipes/file-validation.pipe.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { validateFileFormat, validateFileSize } from '@/src/shared/util/file.utils'
|
||||||
|
import { type ReadStream } from 'node:fs'
|
||||||
|
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'
|
||||||
|
import * as FileUpload from 'graphql-upload/Upload.js'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FileValidationPipe implements PipeTransform {
|
||||||
|
async transform(value: FileUpload, metadata: ArgumentMetadata): FileUpload {
|
||||||
|
if (!value.filename) {
|
||||||
|
throw new BadRequestException('Файл не загружен')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { filename, createReadStream } = value
|
||||||
|
const fileStream = createReadStream() as ReadStream
|
||||||
|
const allowedFormats = ['jpg', 'jpeg', 'png', 'webp', 'gif']
|
||||||
|
const allowedFileSizeInByte = 5 * 1024 * 1024
|
||||||
|
const isFileFormatValidate = validateFileFormat(filename, allowedFormats)
|
||||||
|
if (!isFileFormatValidate) {
|
||||||
|
throw new BadRequestException('Неподдерживаемый формат файла')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFileSizeValid = await validateFileSize(fileStream, allowedFileSizeInByte)
|
||||||
|
if (!isFileSizeValid) {
|
||||||
|
throw new BadRequestException('Превышен максимальный размер файла')
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
25
backend/src/shared/util/file.utils.ts
Normal file
25
backend/src/shared/util/file.utils.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ReadStream } from 'fs'
|
||||||
|
|
||||||
|
export function validateFileFormat(
|
||||||
|
fileName: string,
|
||||||
|
allowedFormats: string[],
|
||||||
|
) {
|
||||||
|
const fileParts = fileName.split('.')
|
||||||
|
const extension = fileParts[fileParts.length - 1]
|
||||||
|
|
||||||
|
return allowedFormats.includes(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateFileSize(
|
||||||
|
fileStream: ReadStream,
|
||||||
|
allowedFileSizeInBytes: number,
|
||||||
|
) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
let fileSizeInBytes = 0
|
||||||
|
|
||||||
|
fileStream
|
||||||
|
.on('data', (data: Buffer) => { fileSizeInBytes = data.byteLength })
|
||||||
|
.on('end', () => { res(fileSizeInBytes <= allowedFileSizeInBytes) })
|
||||||
|
.on('error', (error) => { rej(error) })
|
||||||
|
})
|
||||||
|
}
|
||||||
1329
backend/yarn.lock
1329
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user