add new lagin desktop
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreatePlatformSettings1789046700000 implements MigrationInterface {
|
||||
name = 'CreatePlatformSettings1789046700000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "platform_settings"`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<PlatformSettingsDto> {
|
||||
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<PlatformSettingsDto> {
|
||||
return this.service.uploadLoginHeroImage(file);
|
||||
}
|
||||
|
||||
@Delete('login-hero-image')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
removeLoginHeroImage(): Promise<PlatformSettingsDto> {
|
||||
return this.service.removeLoginHeroImage();
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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<PlatformSettingsEntity>,
|
||||
) {
|
||||
this.uploadRoot = join(process.cwd(), process.env.UPLOAD_DIR ?? 'uploads');
|
||||
}
|
||||
|
||||
async getSettings(): Promise<PlatformSettingsDto> {
|
||||
const row = await this.ensureRow();
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async uploadLoginHeroImage(
|
||||
file: Express.Multer.File,
|
||||
): Promise<PlatformSettingsDto> {
|
||||
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<PlatformSettingsDto> {
|
||||
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<PlatformSettingsEntity> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user