From d3d61f5e5a29eecb284042d358fdb4de7c8093f0 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Thu, 11 Jun 2026 16:20:46 +0330 Subject: [PATCH] add new lagin desktop --- src/app.module.ts | 2 + .../1789046700000-CreatePlatformSettings.ts | 27 +++++ src/entities/platform-settings.entity.ts | 17 +++ .../platform-settings.controller.ts | 51 ++++++++ .../platform-settings.module.ts | 14 +++ .../platform-settings.service.ts | 112 ++++++++++++++++++ 6 files changed, 223 insertions(+) create mode 100644 src/database/migrations/1789046700000-CreatePlatformSettings.ts create mode 100644 src/entities/platform-settings.entity.ts create mode 100644 src/modules/platform-settings/platform-settings.controller.ts create mode 100644 src/modules/platform-settings/platform-settings.module.ts create mode 100644 src/modules/platform-settings/platform-settings.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 9c55988..3a90e54 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,7 @@ import { BusinessServiceRequestsModule } from './modules/business-service-reques import { BusinessServiceClassesModule } from './modules/business-service-classes/business-service-classes.module'; import { AuthModule } from './modules/auth/auth.module'; import { UsersModule } from './modules/users/users.module'; +import { PlatformSettingsModule } from './modules/platform-settings/platform-settings.module'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { UsersModule } from './modules/users/users.module'; BusinessServiceClassesModule, AuthModule, UsersModule, + PlatformSettingsModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/database/migrations/1789046700000-CreatePlatformSettings.ts b/src/database/migrations/1789046700000-CreatePlatformSettings.ts new file mode 100644 index 0000000..a7a875b --- /dev/null +++ b/src/database/migrations/1789046700000-CreatePlatformSettings.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreatePlatformSettings1789046700000 implements MigrationInterface { + name = 'CreatePlatformSettings1789046700000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "platform_settings" ( + "id" smallint NOT NULL DEFAULT 1, + "login_hero_image_url" character varying(500), + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT "PK_platform_settings" PRIMARY KEY ("id"), + CONSTRAINT "CHK_platform_settings_singleton" CHECK ("id" = 1) + ) + `); + + await queryRunner.query(` + INSERT INTO "platform_settings" ("id") + VALUES (1) + ON CONFLICT ("id") DO NOTHING + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "platform_settings"`); + } +} diff --git a/src/entities/platform-settings.entity.ts b/src/entities/platform-settings.entity.ts new file mode 100644 index 0000000..4c1dc99 --- /dev/null +++ b/src/entities/platform-settings.entity.ts @@ -0,0 +1,17 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('platform_settings') +export class PlatformSettingsEntity { + @PrimaryColumn({ type: 'smallint', default: 1 }) + id!: number; + + @Column({ name: 'login_hero_image_url', type: 'varchar', length: 500, nullable: true }) + loginHeroImageUrl!: string | null; + + @Column({ + name: 'updated_at', + type: 'timestamptz', + default: () => 'CURRENT_TIMESTAMP', + }) + updatedAt!: Date; +} diff --git a/src/modules/platform-settings/platform-settings.controller.ts b/src/modules/platform-settings/platform-settings.controller.ts new file mode 100644 index 0000000..aa1f155 --- /dev/null +++ b/src/modules/platform-settings/platform-settings.controller.ts @@ -0,0 +1,51 @@ +import { + Controller, + Delete, + Get, + Post, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { memoryStorage } from 'multer'; +import { UserRole } from '../../entities/user.entity'; +import { Roles } from '../auth/decorators/roles.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { RolesGuard } from '../auth/guards/roles.guard'; +import { + PlatformSettingsDto, + PlatformSettingsService, +} from './platform-settings.service'; + +@Controller('platform-settings') +export class PlatformSettingsController { + constructor(private readonly service: PlatformSettingsService) {} + + @Get() + getSettings(): Promise { + return this.service.getSettings(); + } + + @Post('login-hero-image') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @UseInterceptors( + FileInterceptor('file', { + storage: memoryStorage(), + limits: { fileSize: 5 * 1024 * 1024 }, + }), + ) + uploadLoginHeroImage( + @UploadedFile() file: Express.Multer.File, + ): Promise { + return this.service.uploadLoginHeroImage(file); + } + + @Delete('login-hero-image') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + removeLoginHeroImage(): Promise { + return this.service.removeLoginHeroImage(); + } +} diff --git a/src/modules/platform-settings/platform-settings.module.ts b/src/modules/platform-settings/platform-settings.module.ts new file mode 100644 index 0000000..1b554db --- /dev/null +++ b/src/modules/platform-settings/platform-settings.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PlatformSettingsEntity } from '../../entities/platform-settings.entity'; +import { AuthModule } from '../auth/auth.module'; +import { PlatformSettingsController } from './platform-settings.controller'; +import { PlatformSettingsService } from './platform-settings.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([PlatformSettingsEntity]), AuthModule], + controllers: [PlatformSettingsController], + providers: [PlatformSettingsService], + exports: [PlatformSettingsService], +}) +export class PlatformSettingsModule {} diff --git a/src/modules/platform-settings/platform-settings.service.ts b/src/modules/platform-settings/platform-settings.service.ts new file mode 100644 index 0000000..9a67b60 --- /dev/null +++ b/src/modules/platform-settings/platform-settings.service.ts @@ -0,0 +1,112 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { randomUUID } from 'crypto'; +import { existsSync, mkdirSync, unlinkSync } from 'fs'; +import { extname, join } from 'path'; +import { Repository } from 'typeorm'; +import { PlatformSettingsEntity } from '../../entities/platform-settings.entity'; + +const ALLOWED_MIME = new Set([ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', +]); +const MAX_FILE_BYTES = 5 * 1024 * 1024; +const SETTINGS_ID = 1; + +export type PlatformSettingsDto = { + loginHeroImageUrl: string | null; +}; + +@Injectable() +export class PlatformSettingsService { + private readonly uploadRoot: string; + + constructor( + @InjectRepository(PlatformSettingsEntity) + private readonly repo: Repository, + ) { + this.uploadRoot = join(process.cwd(), process.env.UPLOAD_DIR ?? 'uploads'); + } + + async getSettings(): Promise { + const row = await this.ensureRow(); + return this.toDto(row); + } + + async uploadLoginHeroImage( + file: Express.Multer.File, + ): Promise { + this.validateFile(file); + + const row = await this.ensureRow(); + const dir = join(this.uploadRoot, 'platform'); + mkdirSync(dir, { recursive: true }); + + if (row.loginHeroImageUrl) { + this.deleteStoredFile(row.loginHeroImageUrl); + } + + const ext = extname(file.originalname).toLowerCase() || '.jpg'; + const filename = `login-hero-${randomUUID()}${ext}`; + const relativeUrl = `platform/${filename}`; + const absolutePath = join(this.uploadRoot, relativeUrl); + + const { writeFileSync } = await import('fs'); + writeFileSync(absolutePath, file.buffer); + + row.loginHeroImageUrl = relativeUrl; + row.updatedAt = new Date(); + await this.repo.save(row); + + return this.toDto(row); + } + + async removeLoginHeroImage(): Promise { + const row = await this.ensureRow(); + + if (row.loginHeroImageUrl) { + this.deleteStoredFile(row.loginHeroImageUrl); + row.loginHeroImageUrl = null; + row.updatedAt = new Date(); + await this.repo.save(row); + } + + return this.toDto(row); + } + + private async ensureRow(): Promise { + let row = await this.repo.findOne({ where: { id: SETTINGS_ID } }); + if (!row) { + row = this.repo.create({ id: SETTINGS_ID, loginHeroImageUrl: null }); + await this.repo.save(row); + } + return row; + } + + private toDto(row: PlatformSettingsEntity): PlatformSettingsDto { + return { + loginHeroImageUrl: row.loginHeroImageUrl, + }; + } + + private validateFile(file: Express.Multer.File | undefined): void { + if (!file) { + throw new BadRequestException('Image file is required'); + } + if (!ALLOWED_MIME.has(file.mimetype)) { + throw new BadRequestException('Only JPEG, PNG, WebP, and GIF images are allowed'); + } + if (file.size > MAX_FILE_BYTES) { + throw new BadRequestException('Image must be at most 5 MB'); + } + } + + private deleteStoredFile(relativeUrl: string): void { + const absolutePath = join(this.uploadRoot, relativeUrl); + if (existsSync(absolutePath)) { + unlinkSync(absolutePath); + } + } +}