chore: add service image and audio
This commit is contained in:
+23
-22
@@ -30,7 +30,8 @@
|
||||
"prepare": "husky || true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.850.0",
|
||||
"@aws-sdk/client-s3": "^3.879.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.879.0",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@keyv/redis": "^4.6.0",
|
||||
"@nest-lab/fastify-multer": "^1.3.0",
|
||||
@@ -39,12 +40,12 @@
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bullmq": "^11.0.3",
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.1.5",
|
||||
"@nestjs/common": "^11.1.6",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.5",
|
||||
"@nestjs/core": "^11.1.6",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-fastify": "^11.1.5",
|
||||
"@nestjs/platform-fastify": "^11.1.6",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
@@ -52,14 +53,14 @@
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"axios": "^1.11.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.56.8",
|
||||
"bullmq": "^5.58.3",
|
||||
"cache-manager": "^6.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"decimal.js": "^10.6.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"fastify": "^5.4.0",
|
||||
"fastify": "^5.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"nestjs-soap": "^3.0.4",
|
||||
"nestjs-telegraf": "^2.9.1",
|
||||
@@ -69,29 +70,29 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"slugify": "^1.6.6",
|
||||
"soap": "^1.2.1",
|
||||
"soap": "^1.3.0",
|
||||
"telegraf": "^4.16.3",
|
||||
"typeorm": "^0.3.25"
|
||||
"typeorm": "^0.3.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"@nestjs/cli": "^11.0.8",
|
||||
"@nestjs/schematics": "^11.0.5",
|
||||
"@nestjs/testing": "^11.1.5",
|
||||
"@nestjs/cli": "^11.0.10",
|
||||
"@nestjs/schematics": "^11.0.7",
|
||||
"@nestjs/testing": "^11.1.6",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.9",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/node": "^20.19.11",
|
||||
"@types/nodemailer": "^6.4.19",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^9.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||
"@typescript-eslint/parser": "^8.41.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^3.10.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
@@ -99,12 +100,12 @@
|
||||
"prettier": "^3.6.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.1.4",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-jest": "^29.4.1",
|
||||
"ts-loader": "^9.5.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0"
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.41.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
Generated
+1500
-1420
File diff suppressed because it is too large
Load Diff
@@ -223,6 +223,11 @@ export const enum ServiceMessage {
|
||||
GUIDE_ORDER_IS_NUMBER = "ترتیب راهنمای سرویس باید یک عدد صحیح باشد",
|
||||
GUIDE_IS_ACTIVE_BOOLEAN = "وضعیت راهنمای سرویس باید یک بولین باشد",
|
||||
GUIDE_TITLE_LENGTH = "عنوان راهنمای سرویس باید بین ۳ تا ۲۰۰ کاراکتر باشد",
|
||||
IMAGES_SHOULD_BE_URL = "عکس های سرویس باید یک آدرس یو آر ال باشد",
|
||||
AUDIOS_REQUIRED = "صوت های سرویس مورد نیاز است",
|
||||
AUDIOS_SHOULD_BE_URL = "صوت های سرویس باید یک آدرس یو آر ال باشد",
|
||||
VIDEOS_REQUIRED = "ویدیو های سرویس مورد نیاز است",
|
||||
VIDEOS_SHOULD_BE_URL = "ویدیو های سرویس باید یک آدرس یو آر ال باشد",
|
||||
}
|
||||
|
||||
export const enum AnnouncementMessage {
|
||||
@@ -863,3 +868,11 @@ export const enum SupportPlanMessage {
|
||||
SUPPORT_PLAN_ALREADY_SUBSCRIBED = "کاربر قبلا اشتراک ثبت کرده است",
|
||||
DOWN_GRADE_NOT_ALLOWED = "امکان تغییر به پلن پایینتر وجود ندارد",
|
||||
}
|
||||
|
||||
export const enum UploaderMessage {
|
||||
FILE_TYPE_NOT_ALLOWED = "نوع فایل مجاز نیست",
|
||||
FILE_SIZE_TOO_SMALL = "فایل کوچکتر از حد مجاز است",
|
||||
UPLOAD_FILE_INVALID = "فایل معتبر نیست",
|
||||
UPLOAD_FILE_TOO_LARGE = "[MAX_FILE_SIZE] فایل بزرگتر از حد مجاز است",
|
||||
UPLOAD_FILE_TYPE_NOT_SUPPORTED = "[MIME_TYPES] نوع فایل مجاز نیست",
|
||||
}
|
||||
|
||||
+20
-17
@@ -1,26 +1,29 @@
|
||||
import { FactoryProvider } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function S3Configs() {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
return {
|
||||
accessKeyId: configService.getOrThrow<string>("BUCKET_ACCESS_KEY"),
|
||||
secretAccessKey: configService.getOrThrow<string>("BUCKET_SECRET_KEY"),
|
||||
endpoint: configService.getOrThrow<string>("BUCKET_URL"),
|
||||
region: configService.getOrThrow<string>("BUCKET_REGION"),
|
||||
bucket: configService.getOrThrow<string>("BUCKET_NAME"),
|
||||
url: configService.getOrThrow<string>("BUCKET_UPLOAD_URL"),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
import { S3_CONFIG } from "../modules/utils/constants";
|
||||
|
||||
export const S3ConfigsProvider: FactoryProvider<IS3Configs> = {
|
||||
provide: S3_CONFIG,
|
||||
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
return {
|
||||
accessKeyId: configService.getOrThrow<string>("BUCKET_ACCESS_KEY"),
|
||||
secretAccessKey: configService.getOrThrow<string>("BUCKET_SECRET_KEY"),
|
||||
endpoint: configService.getOrThrow<string>("BUCKET_URL"),
|
||||
region: configService.getOrThrow<string>("BUCKET_REGION"),
|
||||
bucketName: configService.getOrThrow<string>("BUCKET_NAME"),
|
||||
bucketUploadedUrl: configService.getOrThrow<string>("BUCKET_UPLOAD_URL"),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export interface IS3Configs {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
url: string;
|
||||
bucketName: string;
|
||||
bucketUploadedUrl: string;
|
||||
}
|
||||
|
||||
+39
-37
@@ -1,42 +1,44 @@
|
||||
import { FactoryProvider } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function smsConfigs() {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
return {
|
||||
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
|
||||
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
|
||||
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
|
||||
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
|
||||
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
|
||||
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
|
||||
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
|
||||
SMS_PATTERN_WALLET_CHARGE: configService.getOrThrow<string>("SMS_PATTERN_WALLET_CHARGE"),
|
||||
SMS_PATTERN_WALLET_DEDUCTION: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
||||
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
|
||||
SMS_PATTERN_TICKET_ANSWERED: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
||||
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
|
||||
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
|
||||
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
|
||||
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
|
||||
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
|
||||
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
||||
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
||||
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
|
||||
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_CANCELLATION"),
|
||||
//
|
||||
SMS_PATTERN_BLOG_NEW_COMMENT: configService.getOrThrow<string>("SMS_PATTERN_BLOG_NEW_COMMENT"),
|
||||
SMS_PATTERN_NEW_SERVICE_REVIEW: configService.getOrThrow<string>("SMS_PATTERN_NEW_SERVICE_REVIEW"),
|
||||
SMS_PATTERN_NEW_CUSTOMER: configService.getOrThrow<string>("SMS_PATTERN_NEW_CUSTOMER"),
|
||||
SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow<string>("SMS_PATTERN_NEW_SUBSCRIPTION"),
|
||||
SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow<string>("SMS_PATTERN_NEW_TICKET_GLOBAL"),
|
||||
SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow<string>("SMS_PATTERN_NEW_CRITICISM"),
|
||||
SMS_PATTERN_USER_PASSWORD: configService.getOrThrow<string>("SMS_PATTERN_USER_PASSWORD"),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
import { SMS_CONFIG } from "../modules/utils/constants";
|
||||
|
||||
export const smsConfigsProvider: FactoryProvider<ISmsConfigs> = {
|
||||
provide: SMS_CONFIG,
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
return {
|
||||
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
|
||||
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
|
||||
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
|
||||
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
|
||||
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
|
||||
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
|
||||
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
|
||||
SMS_PATTERN_WALLET_CHARGE: configService.getOrThrow<string>("SMS_PATTERN_WALLET_CHARGE"),
|
||||
SMS_PATTERN_WALLET_DEDUCTION: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
||||
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
|
||||
SMS_PATTERN_TICKET_ANSWERED: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
||||
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
|
||||
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
|
||||
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
|
||||
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
|
||||
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
|
||||
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
||||
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
||||
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
|
||||
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_CANCELLATION"),
|
||||
//
|
||||
SMS_PATTERN_BLOG_NEW_COMMENT: configService.getOrThrow<string>("SMS_PATTERN_BLOG_NEW_COMMENT"),
|
||||
SMS_PATTERN_NEW_SERVICE_REVIEW: configService.getOrThrow<string>("SMS_PATTERN_NEW_SERVICE_REVIEW"),
|
||||
SMS_PATTERN_NEW_CUSTOMER: configService.getOrThrow<string>("SMS_PATTERN_NEW_CUSTOMER"),
|
||||
SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow<string>("SMS_PATTERN_NEW_SUBSCRIPTION"),
|
||||
SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow<string>("SMS_PATTERN_NEW_TICKET_GLOBAL"),
|
||||
SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow<string>("SMS_PATTERN_NEW_CRITICISM"),
|
||||
SMS_PATTERN_USER_PASSWORD: configService.getOrThrow<string>("SMS_PATTERN_USER_PASSWORD"),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export interface ISmsConfigs {
|
||||
API_URL: string;
|
||||
|
||||
@@ -109,9 +109,24 @@ export class CreateServiceDto {
|
||||
|
||||
@IsNotEmpty({ message: ServiceMessage.IMAGES_REQUIRED })
|
||||
@IsString({ each: true })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.IMAGES_SHOULD_BE_URL })
|
||||
@ApiProperty({ description: "The images of the service", example: ["https://example.com/image1.png", "https://example.com/image2.png"] })
|
||||
images: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: ServiceMessage.AUDIOS_REQUIRED })
|
||||
@IsString({ each: true })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.AUDIOS_SHOULD_BE_URL })
|
||||
@ApiProperty({ description: "The audios of the service", example: ["https://example.com/audio1.mp3", "https://example.com/audio2.mp3"] })
|
||||
audios?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: ServiceMessage.VIDEOS_REQUIRED })
|
||||
@IsString({ each: true })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.VIDEOS_SHOULD_BE_URL })
|
||||
@ApiProperty({ description: "The videos of the service", example: ["https://example.com/video1.mp4", "https://example.com/video2.mp4"] })
|
||||
videos?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: ServiceMessage.COVER_URL_REQUIRED })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.COVER_URL_SHOULD_BE_URL })
|
||||
|
||||
@@ -100,8 +100,8 @@ export class DanakServicesController {
|
||||
@PermissionsDec(PermissionEnum.SERVICES)
|
||||
@ApiOperation({ summary: "create new danak services => admin route" })
|
||||
@Post()
|
||||
createService(@Body() createDto: CreateServiceDto) {
|
||||
return this.danakServicesService.createService(createDto);
|
||||
createService(@Body() createDto: CreateServiceDto, @UserDec("id") userId: string) {
|
||||
return this.danakServicesService.createService(createDto, userId);
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
@@ -185,8 +185,8 @@ export class DanakServicesController {
|
||||
@AuthGuards()
|
||||
@PermissionsDec(PermissionEnum.SERVICES)
|
||||
@Patch(":id")
|
||||
updateDanakService(@Param() paramDto: ParamDto, @Body() updateDto: UpdateServiceDto) {
|
||||
return this.danakServicesService.updateDanakService(paramDto.id, updateDto);
|
||||
updateDanakService(@Param() paramDto: ParamDto, @Body() updateDto: UpdateServiceDto, @UserDec("id") userId: string) {
|
||||
return this.danakServicesService.updateDanakService(paramDto.id, updateDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "delete danak service" })
|
||||
|
||||
@@ -17,6 +17,10 @@ import { DanakServiceReviewRepository } from "./repositories/danak-services-revi
|
||||
import { DanakServicesRepository } from "./repositories/danak-services.repository";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { DanakServiceAudio } from "./entities/danak-service-audio.entity";
|
||||
import { DanakServiceVideo } from "./entities/danak-service-video.entity";
|
||||
import { DanakServicesAudioRepository } from "./repositories/danak-services-audio.repository";
|
||||
import { DanakServicesVideoRepository } from "./repositories/danak-services-video.repository";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -27,6 +31,8 @@ import { UsersModule } from "../users/users.module";
|
||||
DanakServiceReview,
|
||||
DanakServiceFeedback,
|
||||
DanakServiceGuide,
|
||||
DanakServiceAudio,
|
||||
DanakServiceVideo,
|
||||
]),
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
@@ -39,6 +45,8 @@ import { UsersModule } from "../users/users.module";
|
||||
DanakServiceReviewRepository,
|
||||
DanakServiceFeedbackRepository,
|
||||
DanakServiceGuideRepository,
|
||||
DanakServicesAudioRepository,
|
||||
DanakServicesVideoRepository,
|
||||
],
|
||||
controllers: [DanakServicesController],
|
||||
exports: [DanakServicesService],
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Column, Entity, ManyToOne } from "typeorm";
|
||||
|
||||
import { DanakService } from "./danak-service.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity()
|
||||
export class DanakServiceAudio extends BaseEntity {
|
||||
@ManyToOne(() => DanakService, (danakService) => danakService.audios, { nullable: false, onDelete: "CASCADE" })
|
||||
danakService: DanakService;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: false })
|
||||
audioUrl: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.audios, { nullable: false, onDelete: "SET NULL" })
|
||||
createBy: User;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Column, Entity, ManyToOne } from "typeorm";
|
||||
|
||||
import { DanakService } from "./danak-service.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity()
|
||||
export class DanakServiceVideo extends BaseEntity {
|
||||
@ManyToOne(() => DanakService, (danakService) => danakService.videos, { nullable: false, onDelete: "CASCADE" })
|
||||
danakService: DanakService;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: false })
|
||||
videoUrl: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.videos, { nullable: false, onDelete: "SET NULL" })
|
||||
createBy: User;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Column, DeleteDateColumn, Entity, Index, ManyToOne, OneToMany } from "typeorm";
|
||||
|
||||
import { DanakServiceAudio } from "./danak-service-audio.entity";
|
||||
import { DanakServiceCategory } from "./danak-service-category.entity";
|
||||
import { DanakServiceFeedback } from "./danak-service-feedback.entity";
|
||||
import { DanakServiceGuide } from "./danak-service-guide.entity";
|
||||
import { DanakServiceImage } from "./danak-service-image.entity";
|
||||
import { DanakServiceReview } from "./danak-service-review.entity";
|
||||
import { DanakServiceVideo } from "./danak-service-video.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Ads } from "../../ads/entities/ads.entity";
|
||||
import { Announcement } from "../../announcements/entities/announcement.entity";
|
||||
@@ -79,6 +81,12 @@ export class DanakService extends BaseEntity {
|
||||
@OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { cascade: true })
|
||||
images: DanakServiceImage[];
|
||||
|
||||
@OneToMany(() => DanakServiceAudio, (danakServiceAudio) => danakServiceAudio.danakService, { cascade: true })
|
||||
audios: DanakServiceAudio[];
|
||||
|
||||
@OneToMany(() => DanakServiceVideo, (danakServiceVideo) => danakServiceVideo.danakService, { cascade: true })
|
||||
videos: DanakServiceVideo[];
|
||||
|
||||
@OneToMany(() => Announcement, (announcement) => announcement.targetService)
|
||||
announcements: Announcement[];
|
||||
|
||||
|
||||
@@ -23,14 +23,18 @@ import { UpdateDanakServiceCategoryDto } from "../DTO/update-category.dto";
|
||||
import { UpdateReviewStatusDto } from "../DTO/update-review-status.dto";
|
||||
import { UpdateServiceGuideDto } from "../DTO/update-service-guide.dto";
|
||||
import { UpdateServiceDto } from "../DTO/update-service.dto";
|
||||
import { DanakServiceAudio } from "../entities/danak-service-audio.entity";
|
||||
import { DanakServiceCategory } from "../entities/danak-service-category.entity";
|
||||
import { DanakServiceImage } from "../entities/danak-service-image.entity";
|
||||
import { DanakServiceVideo } from "../entities/danak-service-video.entity";
|
||||
import { ReviewStatus } from "../enums/review-status.enum";
|
||||
import { DanakServiceFeedbackRepository } from "../repositories/danak-service-feedback.repository";
|
||||
import { DanakServiceGuideRepository } from "../repositories/danak-service-guide.repository";
|
||||
import { DanakServicesAudioRepository } from "../repositories/danak-services-audio.repository";
|
||||
import { DanakServicesCategoryRepository } from "../repositories/danak-services-category.repository";
|
||||
import { DanakServicesImageRepository } from "../repositories/danak-services-image.repository";
|
||||
import { DanakServiceReviewRepository } from "../repositories/danak-services-review.repository";
|
||||
import { DanakServicesVideoRepository } from "../repositories/danak-services-video.repository";
|
||||
import { DanakServicesRepository } from "../repositories/danak-services.repository";
|
||||
@Injectable()
|
||||
export class DanakServicesService {
|
||||
@@ -42,6 +46,8 @@ export class DanakServicesService {
|
||||
private readonly danakServiceFeedbackRepository: DanakServiceFeedbackRepository,
|
||||
private readonly danakServiceGuideRepository: DanakServiceGuideRepository,
|
||||
private readonly danakServicesImageRepository: DanakServicesImageRepository,
|
||||
private readonly danakServicesAudioRepository: DanakServicesAudioRepository,
|
||||
private readonly danakServicesVideoRepository: DanakServicesVideoRepository,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly adminsService: AdminsService,
|
||||
private readonly usersService: UsersService,
|
||||
@@ -279,7 +285,7 @@ export class DanakServicesService {
|
||||
};
|
||||
}
|
||||
/******************************************** */
|
||||
async createService(createDto: CreateServiceDto) {
|
||||
async createService(createDto: CreateServiceDto, userId: string) {
|
||||
const category = await this.danakServicesCategoryRepository.findOneById(createDto.categoryId);
|
||||
if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST);
|
||||
|
||||
@@ -292,7 +298,23 @@ export class DanakServicesService {
|
||||
return { imageUrl: image };
|
||||
});
|
||||
|
||||
const service = this.danakServicesRepository.create({ ...createDto, category, images, slug });
|
||||
let audios: Partial<DanakServiceAudio>[] = [];
|
||||
|
||||
if (createDto.audios) {
|
||||
audios = createDto.audios.map((audio) => {
|
||||
return { audioUrl: audio, createBy: { id: userId } as User };
|
||||
});
|
||||
}
|
||||
|
||||
let videos: Partial<DanakServiceVideo>[] = [];
|
||||
|
||||
if (createDto.videos) {
|
||||
videos = createDto.videos.map((video) => {
|
||||
return { videoUrl: video, createBy: { id: userId } as User };
|
||||
});
|
||||
}
|
||||
|
||||
const service = this.danakServicesRepository.create({ ...createDto, category, images, audios, videos, slug });
|
||||
await this.danakServicesRepository.save(service);
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
@@ -300,10 +322,12 @@ export class DanakServicesService {
|
||||
};
|
||||
}
|
||||
/******************************************** */
|
||||
async updateDanakService(serviceId: string, updateDto: UpdateServiceDto) {
|
||||
async updateDanakService(serviceId: string, updateDto: UpdateServiceDto, userId: string) {
|
||||
const service = await this.findServiceById(serviceId);
|
||||
|
||||
let images: DanakServiceImage[] = [];
|
||||
let audios: DanakServiceAudio[] = [];
|
||||
let videos: DanakServiceVideo[] = [];
|
||||
|
||||
if (updateDto.categoryId) {
|
||||
const category = await this.danakServicesCategoryRepository.findOneById(updateDto.categoryId);
|
||||
@@ -331,6 +355,37 @@ export class DanakServicesService {
|
||||
images = newImages;
|
||||
}
|
||||
|
||||
if (updateDto.audios) {
|
||||
await this.danakServicesAudioRepository.delete({ danakService: { id: serviceId } });
|
||||
|
||||
const newAudios = updateDto.audios.map((audio) => {
|
||||
return this.danakServicesAudioRepository.create({
|
||||
audioUrl: audio,
|
||||
danakService: service,
|
||||
createBy: { id: userId } as User,
|
||||
});
|
||||
});
|
||||
await this.danakServicesAudioRepository.save(newAudios);
|
||||
|
||||
audios = newAudios;
|
||||
}
|
||||
|
||||
if (updateDto.videos) {
|
||||
await this.danakServicesVideoRepository.delete({ danakService: { id: serviceId } });
|
||||
|
||||
const newVideos = updateDto.videos.map((video) => {
|
||||
return this.danakServicesVideoRepository.create({
|
||||
videoUrl: video,
|
||||
danakService: service,
|
||||
createBy: { id: userId } as User,
|
||||
});
|
||||
});
|
||||
|
||||
await this.danakServicesVideoRepository.save(newVideos);
|
||||
|
||||
videos = newVideos;
|
||||
}
|
||||
|
||||
if (updateDto.slug) {
|
||||
const existSlug = await this.danakServicesRepository.findOne({ where: { slug: updateDto.slug, id: Not(serviceId) } });
|
||||
if (existSlug) throw new BadRequestException(ServiceMessage.SERVICE_ALREADY_EXISTS_WITH_THIS_SLUG);
|
||||
@@ -341,6 +396,8 @@ export class DanakServicesService {
|
||||
...updateDto,
|
||||
slug: this.generateSlug(updateDto.slug ?? service.slug ?? service.name),
|
||||
images: images.length ? images : service.images,
|
||||
audios: audios.length ? audios : service.audios,
|
||||
videos: videos.length ? videos : service.videos,
|
||||
});
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { DanakServiceAudio } from "../entities/danak-service-audio.entity";
|
||||
|
||||
@Injectable()
|
||||
export class DanakServicesAudioRepository extends Repository<DanakServiceAudio> {
|
||||
constructor(@InjectRepository(DanakServiceAudio) danakServicesAudioRepository: Repository<DanakServiceAudio>) {
|
||||
super(danakServicesAudioRepository.target, danakServicesAudioRepository.manager, danakServicesAudioRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { DanakServiceVideo } from "../entities/danak-service-video.entity";
|
||||
|
||||
@Injectable()
|
||||
export class DanakServicesVideoRepository extends Repository<DanakServiceVideo> {
|
||||
constructor(@InjectRepository(DanakServiceVideo) danakServicesVideoRepository: Repository<DanakServiceVideo>) {
|
||||
super(danakServicesVideoRepository.target, danakServicesVideoRepository.manager, danakServicesVideoRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,8 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
.leftJoin("service.category", "category")
|
||||
.addSelect(["category.id", "category.title"])
|
||||
.leftJoinAndSelect("service.images", "images")
|
||||
.leftJoinAndSelect("service.audios", "audios")
|
||||
.leftJoinAndSelect("service.videos", "videos")
|
||||
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
|
||||
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
||||
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
||||
@@ -122,7 +124,7 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
// "averageRating",
|
||||
// )
|
||||
.andWhere("service.id = :serviceId", { serviceId })
|
||||
.groupBy("service.id,category.id, images.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||
.groupBy("service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||
|
||||
if (!isAdmin) {
|
||||
serviceQueryBuilder
|
||||
@@ -141,6 +143,8 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
.leftJoin("service.category", "category")
|
||||
.addSelect(["category.id", "category.title"])
|
||||
.leftJoinAndSelect("service.images", "images")
|
||||
.leftJoinAndSelect("service.audios", "audios")
|
||||
.leftJoinAndSelect("service.videos", "videos")
|
||||
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
|
||||
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
||||
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
||||
@@ -168,7 +172,7 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
// "averageRating",
|
||||
// )
|
||||
.andWhere("service.slug = :slug", { slug })
|
||||
.groupBy("service.id,category.id, images.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||
.groupBy("service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||
|
||||
if (!isAdmin) {
|
||||
serviceQueryBuilder
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { File } from "@nest-lab/fastify-multer";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
import { IFile } from "../../utils/interfaces/IFile";
|
||||
|
||||
export class UploadSingleFileDto {
|
||||
@ApiProperty({ type: "string", format: "binary", nullable: false })
|
||||
file: IFile;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export class UploadMultipleFileDto {
|
||||
@@ -16,5 +15,5 @@ export class UploadMultipleFileDto {
|
||||
format: "binary",
|
||||
},
|
||||
})
|
||||
files: IFile[];
|
||||
files: File[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { File } from "@nest-lab/fastify-multer";
|
||||
|
||||
export interface FileUploadResult {
|
||||
file: File;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface FileStreamResult {
|
||||
stream: NodeJS.ReadableStream;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface FileMetadata {
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
bitrate?: number;
|
||||
codec?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export type FileType = "video" | "audio" | "image" | "document" | "text" | "unknown";
|
||||
|
||||
export interface FileValidationOptions {
|
||||
maxFileSize: number;
|
||||
allowedMimeTypes: string[];
|
||||
}
|
||||
|
||||
export interface FileUploadOptions {
|
||||
roomId?: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// File processing status
|
||||
export type FileProcessingStatus = "pending" | "processing" | "completed" | "failed";
|
||||
|
||||
export interface FileProcessingResult {
|
||||
status: FileProcessingStatus;
|
||||
metadata?: FileMetadata;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export interface S3UploadResult {
|
||||
key: string;
|
||||
url: string;
|
||||
bucket: string;
|
||||
}
|
||||
|
||||
export interface S3UploadOptions {
|
||||
contentType: string;
|
||||
metadata?: Record<string, string>;
|
||||
acl?: "private" | "public-read" | "public-read-write";
|
||||
}
|
||||
|
||||
export interface S3DownloadOptions {
|
||||
expiresIn?: number;
|
||||
responseContentType?: string;
|
||||
responseContentDisposition?: string;
|
||||
}
|
||||
|
||||
export interface S3FileInfo {
|
||||
key: string;
|
||||
bucket: string;
|
||||
size: number;
|
||||
lastModified: Date;
|
||||
contentType: string;
|
||||
etag: string;
|
||||
}
|
||||
|
||||
export interface S3DeleteResult {
|
||||
deleted: boolean;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface S3ListResult {
|
||||
files: S3FileInfo[];
|
||||
continuationToken?: string;
|
||||
isTruncated: boolean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface FileSearchOptions {
|
||||
userId?: string;
|
||||
roomId?: string;
|
||||
fileType?: "video" | "audio" | "subtitle";
|
||||
isActive?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: "uploadedAt" | "size" | "originalName";
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Readable } from "stream";
|
||||
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { IS3Configs } from "../../../configs/s3.config";
|
||||
import { S3_CONFIG } from "../../utils/constants";
|
||||
import { FileType } from "../interfaces/file.interface";
|
||||
import { S3UploadResult } from "../interfaces/s3.interface";
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
private readonly logger = new Logger(S3Service.name);
|
||||
private readonly s3Client: S3Client;
|
||||
|
||||
constructor(@Inject(S3_CONFIG) private readonly s3Configs: IS3Configs) {
|
||||
this.s3Client = new S3Client({
|
||||
region: this.s3Configs.region,
|
||||
endpoint: this.s3Configs.endpoint,
|
||||
credentials: {
|
||||
accessKeyId: this.s3Configs.accessKeyId,
|
||||
secretAccessKey: this.s3Configs.secretAccessKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to S3
|
||||
*/
|
||||
async uploadFile(buffer: Buffer, key: string, contentType: string, metadata?: Record<string, string>): Promise<S3UploadResult> {
|
||||
try {
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: this.s3Configs.bucketName,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ACL: "public-read",
|
||||
ContentType: contentType,
|
||||
Metadata: metadata,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
|
||||
const url = `${this.s3Configs.bucketUploadedUrl}/${key}`;
|
||||
|
||||
this.logger.log(`File uploaded to S3: ${key}`);
|
||||
|
||||
return {
|
||||
key,
|
||||
url,
|
||||
bucket: this.s3Configs.bucketName,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file stream from S3
|
||||
*/
|
||||
async getFileStream(key: string): Promise<Readable> {
|
||||
try {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: this.s3Configs.bucketName,
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const response = await this.s3Client.send(command);
|
||||
return response.Body as Readable;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate presigned URL for file download
|
||||
*/
|
||||
async getPresignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
|
||||
try {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: this.s3Configs.bucketName,
|
||||
Key: key,
|
||||
});
|
||||
|
||||
return await getSignedUrl(this.s3Client, command, { expiresIn });
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file from S3
|
||||
*/
|
||||
async deleteFile(key: string): Promise<void> {
|
||||
try {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: this.s3Configs.bucketName,
|
||||
Key: key,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
this.logger.log(`File deleted from S3: ${key}`);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate S3 key for file
|
||||
*/
|
||||
generateFileKey(originalName: string, fileType: FileType): string {
|
||||
const timestamp = Date.now();
|
||||
const randomSuffix = Math.round(Math.random() * 1e9);
|
||||
const extension = originalName.split(".").pop();
|
||||
|
||||
const basePathMap: Record<FileType, string> = {
|
||||
image: "images",
|
||||
video: "videos",
|
||||
audio: "audio",
|
||||
document: "documents",
|
||||
text: "texts",
|
||||
unknown: "unknowns",
|
||||
};
|
||||
|
||||
const basePath = basePathMap[fileType];
|
||||
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { File } from "@nest-lab/fastify-multer";
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { S3Service } from "./s3.service";
|
||||
import { UploaderMessage } from "../../../common/enums/message.enum";
|
||||
import { FileType } from "../interfaces/file.interface";
|
||||
|
||||
@Injectable()
|
||||
export class UploaderService {
|
||||
private readonly logger = new Logger(UploaderService.name);
|
||||
private readonly maxFileSize: number;
|
||||
private readonly allowedMimeTypes: string[];
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly s3Service: S3Service,
|
||||
) {
|
||||
this.maxFileSize = this.configService.get("MAX_FILE_SIZE", 1024 * 1024 * 100); // 100MB
|
||||
this.allowedMimeTypes = [
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/ogg",
|
||||
"video/avi",
|
||||
"video/mov",
|
||||
"video/mkv",
|
||||
"audio/mp3",
|
||||
"audio/wav",
|
||||
"audio/ogg",
|
||||
"audio/webm",
|
||||
"audio/m4a",
|
||||
"audio/aac",
|
||||
"audio/flac",
|
||||
"audio/m4a",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/tiff",
|
||||
"image/bmp",
|
||||
"application/pdf",
|
||||
"text/plain",
|
||||
"text/csv",
|
||||
];
|
||||
}
|
||||
|
||||
async uploadMultiple(files: File[]) {
|
||||
return await Promise.all(files.map((file) => this.uploadFile(file)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file upload
|
||||
*/
|
||||
async uploadFile(file: File) {
|
||||
if (!file || !file.buffer || !file.size) {
|
||||
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
|
||||
}
|
||||
this.logger.log(`Uploading file: ${file.originalname}`);
|
||||
|
||||
this.validateFile(file);
|
||||
|
||||
const fileType = await this.determineFileType(file.mimetype);
|
||||
|
||||
const s3Key = this.s3Service.generateFileKey(file.originalname, fileType);
|
||||
|
||||
// Upload to S3
|
||||
const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, {
|
||||
originalName: file.originalname,
|
||||
uploadedBy: "admin",
|
||||
});
|
||||
|
||||
this.logger.log(`File uploaded successfully: `);
|
||||
|
||||
return {
|
||||
file: s3Result,
|
||||
url: s3Result.url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate uploaded file
|
||||
*/
|
||||
private validateFile(file: File): void {
|
||||
if (!file) throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
|
||||
|
||||
if (file.size && file.size > this.maxFileSize)
|
||||
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TOO_LARGE.replace("[MAX_FILE_SIZE]", this.maxFileSize.toString()));
|
||||
|
||||
if (!this.allowedMimeTypes.includes(file.mimetype))
|
||||
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TYPE_NOT_SUPPORTED.replace("[MIME_TYPES]", file.mimetype));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine file type based on mimetype
|
||||
*/
|
||||
private async determineFileType(mimetype: string): Promise<FileType> {
|
||||
if (mimetype.startsWith("video/")) {
|
||||
return "video";
|
||||
} else if (mimetype.startsWith("audio/")) {
|
||||
return "audio";
|
||||
} else if (mimetype.startsWith("image/")) {
|
||||
return "image";
|
||||
} else if (mimetype.startsWith("text/")) {
|
||||
return "text";
|
||||
} else if (mimetype.startsWith("application/")) {
|
||||
return "document";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
Executable → Regular
+7
-9
@@ -1,25 +1,23 @@
|
||||
import { FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
|
||||
import { File, FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
|
||||
import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common";
|
||||
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { ApiBody, ApiConsumes, ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto";
|
||||
import { UploaderService } from "./uploader.service";
|
||||
import { UploaderService } from "./services/uploader.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { IFile } from "../utils/interfaces/IFile";
|
||||
|
||||
@ApiTags("Uploader")
|
||||
@Controller("uploader")
|
||||
@AuthGuards()
|
||||
export class UploaderController {
|
||||
constructor(private readonly uploaderService: UploaderService) {}
|
||||
|
||||
@ApiOperation({ summary: "Uploads a single file" })
|
||||
@ApiOperation({ summary: "Upload a file (admin)" })
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(FileInterceptor("file"))
|
||||
@ApiBody({ type: UploadSingleFileDto })
|
||||
@Post("single-file")
|
||||
uploadFile(@UploadedFile() file: IFile) {
|
||||
return this.uploaderService.upload(file);
|
||||
uploadFile(@UploadedFile() file: File) {
|
||||
return this.uploaderService.uploadFile(file);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Uploads multiple files" })
|
||||
@@ -27,7 +25,7 @@ export class UploaderController {
|
||||
@UseInterceptors(FilesInterceptor("files"))
|
||||
@ApiBody({ type: UploadMultipleFileDto })
|
||||
@Post("multi-file")
|
||||
uploadFiles(@UploadedFiles() files: IFile[]) {
|
||||
uploadFiles(@UploadedFiles() files: File[]) {
|
||||
return this.uploaderService.uploadMultiple(files);
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
+4
-5
@@ -1,14 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { S3Service } from "./services/s3.service";
|
||||
import { UploaderService } from "./services/uploader.service";
|
||||
import { UploaderController } from "./uploader.controller";
|
||||
import { UploaderService } from "./uploader.service";
|
||||
import { S3Service } from "../utils/providers/s3.service";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { S3ConfigsProvider } from "../../configs/s3.config";
|
||||
|
||||
@Module({
|
||||
imports: [UtilsModule],
|
||||
providers: [UploaderService, S3Service],
|
||||
controllers: [UploaderController],
|
||||
providers: [UploaderService, S3Service, S3ConfigsProvider],
|
||||
exports: [UploaderService],
|
||||
})
|
||||
export class UploaderModule {}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { IFile } from "../utils/interfaces/IFile";
|
||||
import { S3Service } from "../utils/providers/s3.service";
|
||||
|
||||
@Injectable()
|
||||
export class UploaderService {
|
||||
constructor(private s3Service: S3Service) {}
|
||||
|
||||
async upload(file: IFile) {
|
||||
return await this.s3Service.upload(file);
|
||||
}
|
||||
|
||||
async uploadMultiple(files: IFile[]) {
|
||||
// const uploadPromises = files.map((file) => this.s3Service.upload(file));
|
||||
|
||||
// return await Promise.all(uploadPromises);
|
||||
return this.s3Service.uploadMultiple(files);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ import { UserAnnouncement } from "../../announcements/entities/user-announcement
|
||||
import { BlogComment } from "../../blogs/entities/blog-comment.entity";
|
||||
import { Blog } from "../../blogs/entities/blog.entity";
|
||||
import { Criticism } from "../../criticisms/entities/criticism.entity";
|
||||
import { DanakServiceAudio } from "../../danak-services/entities/danak-service-audio.entity";
|
||||
import { DanakServiceFeedback } from "../../danak-services/entities/danak-service-feedback.entity";
|
||||
import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.entity";
|
||||
import { DanakServiceVideo } from "../../danak-services/entities/danak-service-video.entity";
|
||||
import { Discount } from "../../discounts/entities/discount.entity";
|
||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||
import { LearningProgress } from "../../learnings/entities/learning-progress.entity";
|
||||
@@ -79,6 +81,12 @@ export class User extends BaseEntity {
|
||||
|
||||
//---------------------------------------
|
||||
|
||||
@OneToMany(() => DanakServiceAudio, (danakServiceAudio) => danakServiceAudio.createBy)
|
||||
audios: DanakServiceAudio[];
|
||||
|
||||
@OneToMany(() => DanakServiceVideo, (danakServiceVideo) => danakServiceVideo.createBy)
|
||||
videos: DanakServiceVideo[];
|
||||
|
||||
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.user)
|
||||
reviews: DanakServiceReview[];
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export interface IFile {
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
buffer: Buffer;
|
||||
size: number;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { extname } from "node:path";
|
||||
|
||||
import { PutObjectCommand, PutObjectCommandInput, S3Client } from "@aws-sdk/client-s3";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
|
||||
import { IS3Configs } from "../../../configs/s3.config";
|
||||
import { S3_CONFIG } from "../constants";
|
||||
import { IFile } from "../interfaces/IFile";
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
private readonly logger = new Logger(S3Service.name);
|
||||
private client: S3Client;
|
||||
|
||||
//
|
||||
constructor(@Inject(S3_CONFIG) private s3Configs: IS3Configs) {
|
||||
this.client = new S3Client({
|
||||
endpoint: this.s3Configs.endpoint,
|
||||
region: this.s3Configs.region,
|
||||
credentials: {
|
||||
accessKeyId: this.s3Configs.accessKeyId,
|
||||
secretAccessKey: this.s3Configs.secretAccessKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async upload(file: IFile) {
|
||||
const extName = extname(file.originalname);
|
||||
const params: PutObjectCommandInput = {
|
||||
Body: file.buffer,
|
||||
Bucket: this.s3Configs.bucket,
|
||||
Key: `images/${randomUUID()}${Date.now()}${extName}`,
|
||||
ACL: "public-read",
|
||||
};
|
||||
//
|
||||
|
||||
try {
|
||||
await this.client.send(new PutObjectCommand(params));
|
||||
const url = `${this.s3Configs.url}/${params.Key}`;
|
||||
return {
|
||||
url,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new InternalServerErrorException("Failed to upload file");
|
||||
}
|
||||
}
|
||||
|
||||
async uploadMultiple(files: IFile[]) {
|
||||
const uploadPromises = files.map((file) => this.upload(file));
|
||||
const results = await Promise.all(uploadPromises);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,15 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { S3_CONFIG, SMS_CONFIG } from "./constants";
|
||||
import { SMS_CONFIG } from "./constants";
|
||||
import { CacheService } from "./providers/cache.service";
|
||||
import { EmailService } from "./providers/email.service";
|
||||
import { OTPService } from "./providers/otp.service";
|
||||
import { PasswordService } from "./providers/password.service";
|
||||
import { S3Service } from "./providers/s3.service";
|
||||
import { SmsService } from "./providers/sms.service";
|
||||
import { S3Configs } from "../../configs/s3.config";
|
||||
import { smsConfigs } from "../../configs/sms.config";
|
||||
import { smsConfigsProvider } from "../../configs/sms.config";
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
EmailService,
|
||||
OTPService,
|
||||
PasswordService,
|
||||
CacheService,
|
||||
SmsService,
|
||||
S3Service,
|
||||
{
|
||||
provide: SMS_CONFIG,
|
||||
useFactory: smsConfigs().useFactory,
|
||||
inject: smsConfigs().inject,
|
||||
},
|
||||
{
|
||||
provide: S3_CONFIG,
|
||||
useFactory: S3Configs().useFactory,
|
||||
inject: S3Configs().inject,
|
||||
},
|
||||
],
|
||||
exports: [SMS_CONFIG, S3_CONFIG, S3Service, OTPService, PasswordService, CacheService, SmsService, EmailService],
|
||||
providers: [EmailService, OTPService, PasswordService, CacheService, SmsService, smsConfigsProvider],
|
||||
exports: [SMS_CONFIG, OTPService, PasswordService, CacheService, SmsService, EmailService],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
|
||||
Reference in New Issue
Block a user