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 { BusinessServiceClassesModule } from './modules/business-service-classes/business-service-classes.module';
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { UsersModule } from './modules/users/users.module';
|
import { UsersModule } from './modules/users/users.module';
|
||||||
|
import { PlatformSettingsModule } from './modules/platform-settings/platform-settings.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -33,6 +34,7 @@ import { UsersModule } from './modules/users/users.module';
|
|||||||
BusinessServiceClassesModule,
|
BusinessServiceClassesModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
PlatformSettingsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
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