add new lagin desktop

This commit is contained in:
2026-06-11 16:20:46 +03:30
parent ea0860d176
commit d3d61f5e5a
6 changed files with 223 additions and 0 deletions
+2
View File
@@ -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"`);
}
}
+17
View File
@@ -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);
}
}
}