diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 1b94076..0aee80b 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -90,10 +90,8 @@ jobs: echo DISCORD_WEBHOOK_TOKEN=${{ secrets.DISCORD_WEBHOOK_TOKEN }} >> .env echo TRIPO_3D_AI_ENDPOINT=${{ vars.TRIPO_3D_AI_ENDPOINT }} >> .env - echo TRIPO_3D_AI_API_KEY=${{ secrets.TRIPO_3D_AI_API_KEY }} >> .env echo EDEN_AI_ENDPOINT=${{ vars.EDEN_AI_ENDPOINT }} >> .env - echo EDEN_AI_API_KEY=${{ secrets.EDEN_AI_API_KEY }} >> .env - name: Deploy run: pm2 restart furnique-api diff --git a/example.env b/example.env index 7fbc809..a05e06d 100644 --- a/example.env +++ b/example.env @@ -32,11 +32,9 @@ DISCORD_WEBHOOK_TOKEN= #Tripo 3D AI API TRIPO_3D_AI_ENDPOINT=https://api.tripo3d.ai -TRIPO_3D_AI_API_KEY= #Eden AI EDEN_AI_ENDPOINT=https://api.edenai.run -EDEN_AI_API_KEY= ## PAYMENT diff --git a/src/ai-generation/controllers/text-to-image.controller.ts b/src/ai-generation/controllers/text-to-image.controller.ts index cd21442..0dd2fee 100644 --- a/src/ai-generation/controllers/text-to-image.controller.ts +++ b/src/ai-generation/controllers/text-to-image.controller.ts @@ -22,7 +22,7 @@ export class AIGenerationTextToImageController { @ApiOkResponse({ type: TextToImageResponseDto }) @Post() generate(@Req() req, @Body() generateTextToImageDto: GenerateTextToImageDto) { - generateTextToImageDto.providers = ['openai/dall-e-2', 'openai/dall-e-3', 'openai', "amazon", 'amazon/titan-image-generator-v1_premium', 'amazon/titan-image-generator-v1_standard'] + generateTextToImageDto.providers = ['openai/dall-e-2'] generateTextToImageDto.resolution = "512x512" generateTextToImageDto.customerId = _.get(req, 'user._id') return this.aiGenerationTextToImageService.generateTextToImage(generateTextToImageDto) diff --git a/src/ai-generation/services/text-to-image.service.ts b/src/ai-generation/services/text-to-image.service.ts index 5efe239..da09095 100644 --- a/src/ai-generation/services/text-to-image.service.ts +++ b/src/ai-generation/services/text-to-image.service.ts @@ -10,6 +10,8 @@ import { AIGenerationPlatform, AIGenerationPricing, AIGenerationType } from '@ai import { GenerateTextToImageDto } from '@ai-generation/dtos/text-to-image.dto' import { CustomerRepository } from '@customer/repositories/customer.repository' import { Status } from '@common/contracts/constant' +import { SettingService } from '@setting/services/setting.service' +import { SettingKey } from '@setting/contracts/constant' @Injectable() export class AIGenerationTextToImageService { @@ -20,19 +22,19 @@ export class AIGenerationTextToImageService { private readonly aiGenerationRepository: AIGenerationRepository, private readonly httpService: HttpService, private readonly configService: ConfigService, - private readonly customerRepository: CustomerRepository + private readonly customerRepository: CustomerRepository, + private readonly settingService: SettingService ) { this.config = this.configService.get('edenAI') this.headersRequest = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.config.apiKey}` + 'Content-Type': 'application/json' } } async generateTextToImage(generateTextToImageDto: GenerateTextToImageDto) { const { customerId } = generateTextToImageDto - // Check limit AI generation + // 1. Check limit AI generation const { credits } = await this.customerRepository.findOne({ conditions: { _id: customerId, @@ -43,10 +45,14 @@ export class AIGenerationTextToImageService { throw new AppException(Errors.NOT_ENOUGH_CREDITS_ERROR) } + // 2. Get API_KEY from DB + const settingValue = await this.settingService.getValue(SettingKey.EDEN_AI) + + // 3. Run GenAI const { data } = await firstValueFrom( this.httpService .post(`${this.config.endpoint}/v2/image/generation`, generateTextToImageDto, { - headers: this.headersRequest + headers: { ...this.headersRequest, Authorization: `Bearer ${settingValue['apiKey']}` } }) .pipe( catchError((error: AxiosError) => { @@ -58,6 +64,7 @@ export class AIGenerationTextToImageService { const result: any = Object.values(data)[0] if (result?.status !== 'success') throw new AppException({ ...Errors.EDEN_AI_ERROR, data }) + // 4. Save GenAI data, update credits const imageUrl = result?.items[0]?.image_resource_url await Promise.all([ this.aiGenerationRepository.create({ diff --git a/src/ai-generation/services/text-to-model.service.ts b/src/ai-generation/services/text-to-model.service.ts index ece542a..d781515 100644 --- a/src/ai-generation/services/text-to-model.service.ts +++ b/src/ai-generation/services/text-to-model.service.ts @@ -10,6 +10,8 @@ import { Errors } from '@common/contracts/error' import { AIGenerationPlatform, AIGenerationPricing, AIGenerationType } from '@ai-generation/contracts/constant' import { CustomerRepository } from '@customer/repositories/customer.repository' import { Status } from '@common/contracts/constant' +import { SettingService } from '@setting/services/setting.service' +import { SettingKey } from '@setting/contracts/constant' @Injectable() export class AIGenerationTextToModelService { @@ -20,19 +22,19 @@ export class AIGenerationTextToModelService { private readonly aiGenerationRepository: AIGenerationRepository, private readonly httpService: HttpService, private readonly configService: ConfigService, - private readonly customerRepository: CustomerRepository + private readonly customerRepository: CustomerRepository, + private readonly settingService: SettingService ) { this.config = this.configService.get('tripo3dAI') this.headersRequest = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.config.apiKey}` + 'Content-Type': 'application/json' } } async generateTextToDraftModel(generateTextToDraftModelDto: GenerateTextToDraftModelDto) { const { customerId } = generateTextToDraftModelDto - // Check limit AI generation + // 1. Check limit AI generation const { credits } = await this.customerRepository.findOne({ conditions: { _id: customerId, @@ -43,10 +45,14 @@ export class AIGenerationTextToModelService { throw new AppException(Errors.NOT_ENOUGH_CREDITS_ERROR) } + // 2. Get API_KEY from DB + const settingValue = await this.settingService.getValue(SettingKey.TRIPO_3D_AI) + + // 3. Run GenAI const { data } = await firstValueFrom( this.httpService .post(`${this.config.endpoint}/v2/openapi/task`, generateTextToDraftModelDto, { - headers: this.headersRequest + headers: { ...this.headersRequest, Authorization: `Bearer ${settingValue['apiKey']}` } }) .pipe( catchError((error: AxiosError) => { @@ -57,6 +63,7 @@ export class AIGenerationTextToModelService { ) if (data.code !== 0) throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data }) + // 4. Save GenAI data, update credits await Promise.all([ this.aiGenerationRepository.create({ customerId, @@ -77,13 +84,19 @@ export class AIGenerationTextToModelService { } async getTask(taskId: string) { + const settingValue = await this.settingService.getValue(SettingKey.TRIPO_3D_AI) + const { data } = await firstValueFrom( - this.httpService.get(`${this.config.endpoint}/v2/openapi/task/${taskId}`, { headers: this.headersRequest }).pipe( - catchError((error: AxiosError) => { - this.logger.error(error?.response?.data) - throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data: error?.response?.data }) + this.httpService + .get(`${this.config.endpoint}/v2/openapi/task/${taskId}`, { + headers: { ...this.headersRequest, Authorization: `Bearer ${settingValue['apiKey']}` } }) - ) + .pipe( + catchError((error: AxiosError) => { + this.logger.error(error?.response?.data) + throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data: error?.response?.data }) + }) + ) ) if (data.code !== 0) throw new AppException({ ...Errors.TRIPO_3D_AI_ERROR, data }) return data?.data diff --git a/src/app.module.ts b/src/app.module.ts index f75b028..7cbbb09 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -26,6 +26,7 @@ import { AnalyticModule } from '@analytic/analytic.module' import { PaymentModule } from '@payment/payment.module' import { AIGenerationModule } from '@ai-generation/ai-generation.module' import { ReviewModule } from '@review/review.module' +import { SettingModule } from '@setting/setting.module' @Module({ imports: [ @@ -133,7 +134,8 @@ import { ReviewModule } from '@review/review.module' AnalyticModule, PaymentModule, AIGenerationModule, - ReviewModule + ReviewModule, + SettingModule ], controllers: [AppController], providers: [AppService] diff --git a/src/config/index.ts b/src/config/index.ts index b71cda9..0939059 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -36,12 +36,10 @@ export default () => ({ webhookToken: process.env.DISCORD_WEBHOOK_TOKEN, }, tripo3dAI: { - endpoint: process.env.TRIPO_3D_AI_ENDPOINT, - apiKey: process.env.TRIPO_3D_AI_API_KEY + endpoint: process.env.TRIPO_3D_AI_ENDPOINT }, edenAI: { - endpoint: process.env.EDEN_AI_ENDPOINT, - apiKey: process.env.EDEN_AI_API_KEY + endpoint: process.env.EDEN_AI_ENDPOINT }, NODE_ENV: process.env.NODE_ENV, JWT_ACCESS_SECRET: process.env.JWT_ACCESS_SECRET || 'accessSecret', diff --git a/src/setting/contracts/constant.ts b/src/setting/contracts/constant.ts new file mode 100644 index 0000000..35b102b --- /dev/null +++ b/src/setting/contracts/constant.ts @@ -0,0 +1,4 @@ +export enum SettingKey { + EDEN_AI = 'EDEN_AI', + TRIPO_3D_AI= 'TRIPO_3D_AI' +} diff --git a/src/setting/repositories/setting.repository.ts b/src/setting/repositories/setting.repository.ts new file mode 100644 index 0000000..27291c4 --- /dev/null +++ b/src/setting/repositories/setting.repository.ts @@ -0,0 +1,12 @@ +import { PaginateModel } from 'mongoose' +import { Injectable } from '@nestjs/common' +import { InjectModel } from '@nestjs/mongoose' +import { AbstractRepository } from '@common/repositories' +import { Setting, SettingDocument } from '@setting/schemas/setting.schema' + +@Injectable() +export class SettingRepository extends AbstractRepository { + constructor(@InjectModel(Setting.name) model: PaginateModel) { + super(model) + } +} diff --git a/src/setting/schemas/setting.schema.ts b/src/setting/schemas/setting.schema.ts new file mode 100644 index 0000000..c1ea683 --- /dev/null +++ b/src/setting/schemas/setting.schema.ts @@ -0,0 +1,34 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' +import { HydratedDocument } from 'mongoose' +import { Transform } from 'class-transformer' +import { SettingKey } from '@setting/contracts/constant'; + +export type SettingDocument = HydratedDocument; + +@Schema({ + collection: 'settings', + timestamps: true, + toJSON: { + transform(doc, ret) { + delete ret.__v + } + } +}) +export class Setting { + constructor(id?: string) { + this._id = id; + } + @Transform(({ value }) => value?.toString()) + _id?: string; + + @Prop({ enum: SettingKey, required: true }) + key: SettingKey; + + @Prop({ type: Object, required: true }) + value: object; + + @Prop({ type: Boolean, default: true }) + enabled: boolean; +} + +export const SettingSchema = SchemaFactory.createForClass(Setting); \ No newline at end of file diff --git a/src/setting/services/setting.service.ts b/src/setting/services/setting.service.ts new file mode 100644 index 0000000..2236ba9 --- /dev/null +++ b/src/setting/services/setting.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { SettingRepository } from '@setting/repositories/setting.repository'; +import { SettingKey } from '@setting/contracts/constant'; + +@Injectable() +export class SettingService { + constructor(readonly settingRepository: SettingRepository) {} + + async getValue(key: SettingKey) { + return ( + await this.settingRepository.findOne({ + conditions: { + key: key.toString(), + enabled: true, + }, + }) + )?.value; + } + + async updateValue(key: SettingKey, value: any) { + return await this.settingRepository.findOneAndUpdate( + { + key: key.toString(), + enabled: true, + }, + { value }, + ); + } +} diff --git a/src/setting/setting.module.ts b/src/setting/setting.module.ts new file mode 100644 index 0000000..195eaed --- /dev/null +++ b/src/setting/setting.module.ts @@ -0,0 +1,15 @@ +import { Global, Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Setting, SettingSchema } from '@setting/schemas/setting.schema'; +import { SettingService } from '@setting/services/setting.service'; +import { SettingRepository } from '@setting/repositories/setting.repository'; + +@Global() +@Module({ + imports: [ + MongooseModule.forFeature([{ name: Setting.name, schema: SettingSchema }]), + ], + providers: [SettingService, SettingRepository], + exports: [SettingService], +}) +export class SettingModule {} diff --git a/tsconfig.json b/tsconfig.json index 5ae2c1f..d1f2c2b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,6 +40,7 @@ "@payment/*": ["src/payment/*"], "@ai-generation/*": ["src/ai-generation/*"], "@review/*": ["src/review/*"], + "@setting/*": ["src/setting/*"], "@src/*": ["src/*"] },