logo set
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddLogoFields1789047500000 implements MigrationInterface {
|
||||
name = 'AddLogoFields1789047500000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "platform_settings"
|
||||
ADD COLUMN IF NOT EXISTS "logo_light_url" character varying(500),
|
||||
ADD COLUMN IF NOT EXISTS "logo_dark_url" character varying(500)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "businesses"
|
||||
ADD COLUMN IF NOT EXISTS "logo_light_url" character varying(500),
|
||||
ADD COLUMN IF NOT EXISTS "logo_dark_url" character varying(500)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "businesses"
|
||||
DROP COLUMN IF EXISTS "logo_dark_url",
|
||||
DROP COLUMN IF EXISTS "logo_light_url"
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "platform_settings"
|
||||
DROP COLUMN IF EXISTS "logo_dark_url",
|
||||
DROP COLUMN IF EXISTS "logo_light_url"
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,12 @@ export class BusinessEntity {
|
||||
@Column({ type: 'varchar', length: 20, default: 'first_booking' })
|
||||
promoDiscountMode!: 'first_booking' | 'always';
|
||||
|
||||
@Column({ name: 'logo_light_url', type: 'varchar', length: 500, nullable: true })
|
||||
logoLightUrl!: string | null;
|
||||
|
||||
@Column({ name: 'logo_dark_url', type: 'varchar', length: 500, nullable: true })
|
||||
logoDarkUrl!: string | null;
|
||||
|
||||
@ManyToOne(() => BusinessCategoryEntity, (category) => category.businesses, {
|
||||
onDelete: 'RESTRICT',
|
||||
})
|
||||
|
||||
@@ -8,6 +8,12 @@ export class PlatformSettingsEntity {
|
||||
@Column({ name: 'login_hero_image_url', type: 'varchar', length: 500, nullable: true })
|
||||
loginHeroImageUrl!: string | null;
|
||||
|
||||
@Column({ name: 'logo_light_url', type: 'varchar', length: 500, nullable: true })
|
||||
logoLightUrl!: string | null;
|
||||
|
||||
@Column({ name: 'logo_dark_url', type: 'varchar', length: 500, nullable: true })
|
||||
logoDarkUrl!: string | null;
|
||||
|
||||
@Column({
|
||||
name: 'updated_at',
|
||||
type: 'timestamptz',
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'fs';
|
||||
import { extname, join } from 'path';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { UserRole } from '../../entities/user.entity';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
|
||||
const ALLOWED_MIME = new Set([
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
]);
|
||||
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
export type LogoVariant = 'light' | 'dark';
|
||||
|
||||
@Injectable()
|
||||
export class BusinessLogosService {
|
||||
private readonly uploadRoot: string;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(BusinessEntity)
|
||||
private readonly businessRepo: Repository<BusinessEntity>,
|
||||
) {
|
||||
this.uploadRoot = join(process.cwd(), process.env.UPLOAD_DIR ?? 'uploads');
|
||||
}
|
||||
|
||||
async upload(
|
||||
businessId: string,
|
||||
variant: LogoVariant,
|
||||
file: Express.Multer.File,
|
||||
actor: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
const business = await this.ensureCanManage(businessId, actor);
|
||||
this.validateFile(file);
|
||||
|
||||
const dir = join(this.uploadRoot, 'businesses', businessId, 'logo');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
const field = variant === 'light' ? 'logoLightUrl' : 'logoDarkUrl';
|
||||
if (business[field]) {
|
||||
this.deleteStoredFile(business[field]!);
|
||||
}
|
||||
|
||||
business[field] = this.saveFile(dir, businessId, variant, file);
|
||||
return this.businessRepo.save(business);
|
||||
}
|
||||
|
||||
async remove(
|
||||
businessId: string,
|
||||
variant: LogoVariant,
|
||||
actor: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
const business = await this.ensureCanManage(businessId, actor);
|
||||
const field = variant === 'light' ? 'logoLightUrl' : 'logoDarkUrl';
|
||||
|
||||
if (business[field]) {
|
||||
this.deleteStoredFile(business[field]!);
|
||||
business[field] = null;
|
||||
return this.businessRepo.save(business);
|
||||
}
|
||||
|
||||
return business;
|
||||
}
|
||||
|
||||
deleteLogoFiles(business: Pick<BusinessEntity, 'logoLightUrl' | 'logoDarkUrl'>): void {
|
||||
for (const url of [business.logoLightUrl, business.logoDarkUrl]) {
|
||||
if (url) {
|
||||
this.deleteStoredFile(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureCanManage(
|
||||
businessId: string,
|
||||
actor: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
const business = await this.businessRepo.findOne({ where: { id: businessId } });
|
||||
if (!business) {
|
||||
throw new NotFoundException('Business not found');
|
||||
}
|
||||
|
||||
if (actor.role === UserRole.ADMIN) {
|
||||
return business;
|
||||
}
|
||||
|
||||
if (actor.role === UserRole.BUSINESS_OWNER && business.ownerId === actor.id) {
|
||||
return business;
|
||||
}
|
||||
|
||||
throw new ForbiddenException('Not allowed to manage this business logo');
|
||||
}
|
||||
|
||||
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 saveFile(
|
||||
dir: string,
|
||||
businessId: string,
|
||||
variant: LogoVariant,
|
||||
file: Express.Multer.File,
|
||||
): string {
|
||||
const ext = extname(file.originalname).toLowerCase() || '.jpg';
|
||||
const filename = `logo-${variant}-${randomUUID()}${ext}`;
|
||||
writeFileSync(join(dir, filename), file.buffer);
|
||||
return `businesses/${businessId}/logo/${filename}`;
|
||||
}
|
||||
|
||||
private deleteStoredFile(relativeUrl: string): void {
|
||||
const absolutePath = join(this.uploadRoot, relativeUrl);
|
||||
if (existsSync(absolutePath)) {
|
||||
unlinkSync(absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UploadedFiles,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { BusinessImageEntity } from '../../entities/business-image.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
@@ -23,6 +24,7 @@ import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { BusinessesService } from './businesses.service';
|
||||
import { BusinessLogosService } from './business-logos.service';
|
||||
import { AssignBusinessOwnerDto } from './dto/assign-business-owner.dto';
|
||||
import { CreateBusinessDto } from './dto/create-business.dto';
|
||||
import { SetAvailabilityDto } from './dto/set-availability.dto';
|
||||
@@ -30,7 +32,10 @@ import { UpdateBusinessDto } from './dto/update-business.dto';
|
||||
|
||||
@Controller('businesses')
|
||||
export class BusinessesController {
|
||||
constructor(private readonly service: BusinessesService) {}
|
||||
constructor(
|
||||
private readonly service: BusinessesService,
|
||||
private readonly logos: BusinessLogosService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
findAll(
|
||||
@@ -90,6 +95,52 @@ export class BusinessesController {
|
||||
return this.service.setMyCoverImage(imageId, user);
|
||||
}
|
||||
|
||||
@Post('mine/logo-light')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.BUSINESS_OWNER)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
uploadMyLogoLight(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
return this.service.uploadMyLogo('light', file, user);
|
||||
}
|
||||
|
||||
@Post('mine/logo-dark')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.BUSINESS_OWNER)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
uploadMyLogoDark(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
return this.service.uploadMyLogo('dark', file, user);
|
||||
}
|
||||
|
||||
@Delete('mine/logo-light')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.BUSINESS_OWNER)
|
||||
removeMyLogoLight(@CurrentUser() user: AuthUser): Promise<BusinessEntity> {
|
||||
return this.service.removeMyLogo('light', user);
|
||||
}
|
||||
|
||||
@Delete('mine/logo-dark')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.BUSINESS_OWNER)
|
||||
removeMyLogoDark(@CurrentUser() user: AuthUser): Promise<BusinessEntity> {
|
||||
return this.service.removeMyLogo('dark', user);
|
||||
}
|
||||
|
||||
@Get(':id/availability')
|
||||
getAvailability(@Param('id') id: string) {
|
||||
return this.service.getAvailability(id);
|
||||
@@ -147,6 +198,60 @@ export class BusinessesController {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Post(':id/logo-light')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
uploadLogoLight(
|
||||
@Param('id') id: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
return this.logos.upload(id, 'light', file, user);
|
||||
}
|
||||
|
||||
@Post(':id/logo-dark')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
uploadLogoDark(
|
||||
@Param('id') id: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
return this.logos.upload(id, 'dark', file, user);
|
||||
}
|
||||
|
||||
@Delete(':id/logo-light')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
|
||||
removeLogoLight(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
return this.logos.remove(id, 'light', user);
|
||||
}
|
||||
|
||||
@Delete(':id/logo-dark')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
|
||||
removeLogoDark(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
return this.logos.remove(id, 'dark', user);
|
||||
}
|
||||
|
||||
@Post('backfill-owners')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { BookingReviewEntity } from '../../entities/booking-review.entity';
|
||||
import { UserEntity } from '../../entities/user.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BusinessImagesService } from './business-images.service';
|
||||
import { BusinessLogosService } from './business-logos.service';
|
||||
import { BusinessesController } from './businesses.controller';
|
||||
import { BusinessesService } from './businesses.service';
|
||||
|
||||
@@ -24,7 +25,7 @@ import { BusinessesService } from './businesses.service';
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [BusinessesController],
|
||||
providers: [BusinessesService, BusinessImagesService],
|
||||
providers: [BusinessesService, BusinessImagesService, BusinessLogosService],
|
||||
exports: [BusinessImagesService],
|
||||
})
|
||||
export class BusinessesModule {}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { BusinessImageEntity } from '../../entities/business-image.entity';
|
||||
import { UserEntity, UserRole } from '../../entities/user.entity';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { BusinessImagesService } from './business-images.service';
|
||||
import { BusinessLogosService } from './business-logos.service';
|
||||
import { AssignBusinessOwnerDto } from './dto/assign-business-owner.dto';
|
||||
import { CreateBusinessDto } from './dto/create-business.dto';
|
||||
import { SetAvailabilityDto } from './dto/set-availability.dto';
|
||||
@@ -49,6 +50,7 @@ export class BusinessesService {
|
||||
private readonly reviewRepo: Repository<BookingReviewEntity>,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly images: BusinessImagesService,
|
||||
private readonly logos: BusinessLogosService,
|
||||
) {}
|
||||
|
||||
async findAll(filters: FindAllFilters): Promise<BusinessEntity[]> {
|
||||
@@ -199,7 +201,7 @@ export class BusinessesService {
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOne(id);
|
||||
const business = await this.findOne(id);
|
||||
|
||||
const imageUrls = await this.images.listUrlsForBusiness(id);
|
||||
|
||||
@@ -208,6 +210,7 @@ export class BusinessesService {
|
||||
});
|
||||
|
||||
this.images.deleteFiles(imageUrls);
|
||||
this.logos.deleteLogoFiles(business);
|
||||
}
|
||||
|
||||
async getAvailability(businessId: string) {
|
||||
@@ -424,6 +427,23 @@ export class BusinessesService {
|
||||
return this.images.upload(business.id, files);
|
||||
}
|
||||
|
||||
async uploadMyLogo(
|
||||
variant: 'light' | 'dark',
|
||||
file: Express.Multer.File,
|
||||
actor: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
const business = await this.requireOwnedBusiness(actor);
|
||||
return this.logos.upload(business.id, variant, file, actor);
|
||||
}
|
||||
|
||||
async removeMyLogo(
|
||||
variant: 'light' | 'dark',
|
||||
actor: AuthUser,
|
||||
): Promise<BusinessEntity> {
|
||||
const business = await this.requireOwnedBusiness(actor);
|
||||
return this.logos.remove(business.id, variant, actor);
|
||||
}
|
||||
|
||||
async removeImage(
|
||||
id: string,
|
||||
imageId: string,
|
||||
|
||||
@@ -13,10 +13,13 @@ 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';
|
||||
import { PlatformSettingsDto } from './platform-settings.dto';
|
||||
import { PlatformSettingsService } from './platform-settings.service';
|
||||
|
||||
const imageUpload = FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
@Controller('platform-settings')
|
||||
export class PlatformSettingsController {
|
||||
@@ -30,12 +33,7 @@ export class PlatformSettingsController {
|
||||
@Post('login-hero-image')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
@UseInterceptors(imageUpload)
|
||||
uploadLoginHeroImage(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
): Promise<PlatformSettingsDto> {
|
||||
@@ -48,4 +46,38 @@ export class PlatformSettingsController {
|
||||
removeLoginHeroImage(): Promise<PlatformSettingsDto> {
|
||||
return this.service.removeLoginHeroImage();
|
||||
}
|
||||
|
||||
@Post('logo-light')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@UseInterceptors(imageUpload)
|
||||
uploadLogoLight(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
): Promise<PlatformSettingsDto> {
|
||||
return this.service.uploadLogo('light', file);
|
||||
}
|
||||
|
||||
@Delete('logo-light')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
removeLogoLight(): Promise<PlatformSettingsDto> {
|
||||
return this.service.removeLogo('light');
|
||||
}
|
||||
|
||||
@Post('logo-dark')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@UseInterceptors(imageUpload)
|
||||
uploadLogoDark(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
): Promise<PlatformSettingsDto> {
|
||||
return this.service.uploadLogo('dark', file);
|
||||
}
|
||||
|
||||
@Delete('logo-dark')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
removeLogoDark(): Promise<PlatformSettingsDto> {
|
||||
return this.service.removeLogo('dark');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PlatformSettingsEntity } from '../../entities/platform-settings.entity';
|
||||
|
||||
export type PlatformSettingsDto = {
|
||||
loginHeroImageUrl: string | null;
|
||||
logoLightUrl: string | null;
|
||||
logoDarkUrl: string | null;
|
||||
};
|
||||
|
||||
export type LogoVariant = 'light' | 'dark';
|
||||
|
||||
export function toPlatformSettingsDto(
|
||||
row: PlatformSettingsEntity,
|
||||
): PlatformSettingsDto {
|
||||
return {
|
||||
loginHeroImageUrl: row.loginHeroImageUrl,
|
||||
logoLightUrl: row.logoLightUrl,
|
||||
logoDarkUrl: row.logoDarkUrl,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { existsSync, mkdirSync, unlinkSync } from 'fs';
|
||||
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'fs';
|
||||
import { extname, join } from 'path';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PlatformSettingsEntity } from '../../entities/platform-settings.entity';
|
||||
import {
|
||||
LogoVariant,
|
||||
PlatformSettingsDto,
|
||||
toPlatformSettingsDto,
|
||||
} from './platform-settings.dto';
|
||||
|
||||
const ALLOWED_MIME = new Set([
|
||||
'image/jpeg',
|
||||
@@ -15,10 +20,6 @@ const ALLOWED_MIME = new Set([
|
||||
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;
|
||||
@@ -32,7 +33,7 @@ export class PlatformSettingsService {
|
||||
|
||||
async getSettings(): Promise<PlatformSettingsDto> {
|
||||
const row = await this.ensureRow();
|
||||
return this.toDto(row);
|
||||
return toPlatformSettingsDto(row);
|
||||
}
|
||||
|
||||
async uploadLoginHeroImage(
|
||||
@@ -48,19 +49,12 @@ export class PlatformSettingsService {
|
||||
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);
|
||||
|
||||
const relativeUrl = this.saveFile(dir, 'login-hero', file);
|
||||
row.loginHeroImageUrl = relativeUrl;
|
||||
row.updatedAt = new Date();
|
||||
await this.repo.save(row);
|
||||
|
||||
return this.toDto(row);
|
||||
return toPlatformSettingsDto(row);
|
||||
}
|
||||
|
||||
async removeLoginHeroImage(): Promise<PlatformSettingsDto> {
|
||||
@@ -73,24 +67,60 @@ export class PlatformSettingsService {
|
||||
await this.repo.save(row);
|
||||
}
|
||||
|
||||
return this.toDto(row);
|
||||
return toPlatformSettingsDto(row);
|
||||
}
|
||||
|
||||
async uploadLogo(
|
||||
variant: LogoVariant,
|
||||
file: Express.Multer.File,
|
||||
): Promise<PlatformSettingsDto> {
|
||||
this.validateFile(file);
|
||||
|
||||
const row = await this.ensureRow();
|
||||
const dir = join(this.uploadRoot, 'platform');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
const field = variant === 'light' ? 'logoLightUrl' : 'logoDarkUrl';
|
||||
if (row[field]) {
|
||||
this.deleteStoredFile(row[field]!);
|
||||
}
|
||||
|
||||
const relativeUrl = this.saveFile(dir, `logo-${variant}`, file);
|
||||
row[field] = relativeUrl;
|
||||
row.updatedAt = new Date();
|
||||
await this.repo.save(row);
|
||||
|
||||
return toPlatformSettingsDto(row);
|
||||
}
|
||||
|
||||
async removeLogo(variant: LogoVariant): Promise<PlatformSettingsDto> {
|
||||
const row = await this.ensureRow();
|
||||
const field = variant === 'light' ? 'logoLightUrl' : 'logoDarkUrl';
|
||||
|
||||
if (row[field]) {
|
||||
this.deleteStoredFile(row[field]!);
|
||||
row[field] = null;
|
||||
row.updatedAt = new Date();
|
||||
await this.repo.save(row);
|
||||
}
|
||||
|
||||
return toPlatformSettingsDto(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 });
|
||||
row = this.repo.create({
|
||||
id: SETTINGS_ID,
|
||||
loginHeroImageUrl: null,
|
||||
logoLightUrl: null,
|
||||
logoDarkUrl: 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');
|
||||
@@ -103,6 +133,18 @@ export class PlatformSettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
private saveFile(
|
||||
dir: string,
|
||||
prefix: string,
|
||||
file: Express.Multer.File,
|
||||
): string {
|
||||
const ext = extname(file.originalname).toLowerCase() || '.jpg';
|
||||
const filename = `${prefix}-${randomUUID()}${ext}`;
|
||||
const relativeUrl = `platform/${filename}`;
|
||||
writeFileSync(join(dir, filename), file.buffer);
|
||||
return relativeUrl;
|
||||
}
|
||||
|
||||
private deleteStoredFile(relativeUrl: string): void {
|
||||
const absolutePath = join(this.uploadRoot, relativeUrl);
|
||||
if (existsSync(absolutePath)) {
|
||||
|
||||
Reference in New Issue
Block a user