main
Some checks failed
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-03-17 13:16:12 +03:00
parent 1a6b00478f
commit e0d41d0386
37 changed files with 1326 additions and 750 deletions

View File

@@ -1,19 +1,17 @@
import {
Controller,
Get,
Post,
Put,
Patch,
Delete,
Body,
Param,
Query,
UseInterceptors,
UploadedFile,
ParseFilePipe,
MaxFileSizeValidator,
FileTypeValidator,
OnModuleInit,
Controller,
Get,
Post,
Put,
Patch,
Delete,
Body,
Param,
Query,
UseInterceptors,
UploadedFile,
BadRequestException,
OnModuleInit,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiBearerAuth, ApiConsumes, ApiBody } from '@nestjs/swagger';
@@ -21,212 +19,215 @@ import { diskStorage } from 'multer';
import { extname } from 'path';
import { CmsService } from './cms.service';
import {
CreateProjectDto,
UpdateProjectDto,
ReorderProjectsDto,
UpdateSiteContentDto,
CreateClientDto,
UpdateClientDto,
CreateProjectDto,
UpdateProjectDto,
ReorderProjectsDto,
UpdateSiteContentDto,
CreateClientDto,
UpdateClientDto,
} from './dto';
import { Public, Roles } from '../../common/decorators';
// Generate unique filename
const storage = diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${extname(file.originalname)}`;
cb(null, uniqueName);
},
destination: './uploads',
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${extname(file.originalname)}`;
cb(null, uniqueName);
},
});
@ApiTags('CMS')
@Controller('cms')
export class CmsController implements OnModuleInit {
constructor(private readonly cmsService: CmsService) { }
constructor(private readonly cmsService: CmsService) { }
async onModuleInit() {
await this.cmsService.seedDefaultProjects();
await this.cmsService.seedAdminUser();
async onModuleInit() {
await this.cmsService.seedDefaultProjects();
await this.cmsService.seedAdminUser();
}
// ── Public Endpoints ────────────────────────────
@Public()
@Get('projects')
findAllProjects() {
return this.cmsService.findAllProjects();
}
@Public()
@Get('content')
findAllContent(@Query('locale') locale?: string) {
return this.cmsService.findAllContent(locale);
}
@Public()
@Get('content/:section')
findContentBySection(
@Param('section') section: string,
@Query('locale') locale?: string,
) {
return this.cmsService.findContentBySection(section, locale ?? 'tr');
}
// ── Admin: Projects ─────────────────────────────
@ApiBearerAuth()
@Roles('admin')
@Post('projects')
createProject(@Body() dto: CreateProjectDto) {
return this.cmsService.createProject(dto);
}
@ApiBearerAuth()
@Roles('admin')
@Put('projects/:id')
updateProject(@Param('id') id: string, @Body() dto: UpdateProjectDto) {
return this.cmsService.updateProject(id, dto);
}
@ApiBearerAuth()
@Roles('admin')
@Delete('projects/:id')
deleteProject(@Param('id') id: string) {
return this.cmsService.deleteProject(id);
}
@ApiBearerAuth()
@Roles('admin')
@Post('projects/:id/restore')
restoreProject(@Param('id') id: string) {
return this.cmsService.restoreProject(id);
}
@ApiBearerAuth()
@Roles('admin')
@Patch('projects/reorder')
reorderProjects(@Body() dto: ReorderProjectsDto) {
return this.cmsService.reorderProjects(dto);
}
// ── Admin: Site Content ─────────────────────────
@ApiBearerAuth()
@Roles('admin')
@Put('content/:section')
upsertContent(
@Param('section') section: string,
@Query('locale') locale: string = 'tr',
@Body() dto: UpdateSiteContentDto,
) {
return this.cmsService.upsertContent(section, locale, dto);
}
// ── Admin: Media Upload ─────────────────────────
@ApiBearerAuth()
@Roles('admin')
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage }))
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: { file: { type: 'string', format: 'binary' } },
},
})
async uploadFile(
@UploadedFile() file: Express.Multer.File,
) {
try {
if (!file) {
throw new BadRequestException('Dosya bulunamadı');
}
if (!file.mimetype.startsWith('image/')) {
throw new BadRequestException('Sadece görsel dosyaları yüklenebilir');
}
const url = `/uploads/${file.filename}`;
const media = await this.cmsService.createMediaFile({
filename: file.filename,
originalName: file.originalname,
mimetype: file.mimetype,
path: file.path,
url,
size: file.size,
});
return media;
} catch (err) {
throw err;
}
}
// ── Public Endpoints ────────────────────────────
@Public()
@Get('media')
findAllMedia() {
return this.cmsService.findAllMedia();
}
@Public()
@Get('projects')
findAllProjects() {
return this.cmsService.findAllProjects();
}
@ApiBearerAuth()
@Roles('admin')
@Delete('media/:id')
deleteMedia(@Param('id') id: string) {
return this.cmsService.deleteMedia(id);
}
@Public()
@Get('content')
findAllContent(@Query('locale') locale?: string) {
return this.cmsService.findAllContent(locale);
}
// ── Public: Clients ─────────────────────────
@Public()
@Get('content/:section')
findContentBySection(
@Param('section') section: string,
@Query('locale') locale?: string,
) {
return this.cmsService.findContentBySection(section, locale ?? 'tr');
}
@Public()
@Get('clients')
findAllClients() {
return this.cmsService.findAllClients();
}
// ── Admin: Projects ─────────────────────────────
// ── Admin: Clients ─────────────────────────
@ApiBearerAuth()
@Roles('admin')
@Post('projects')
createProject(@Body() dto: CreateProjectDto) {
return this.cmsService.createProject(dto);
}
@ApiBearerAuth()
@Roles('admin')
@Post('clients')
createClient(@Body() dto: CreateClientDto) {
return this.cmsService.createClient(dto);
}
@ApiBearerAuth()
@Roles('admin')
@Put('projects/:id')
updateProject(@Param('id') id: string, @Body() dto: UpdateProjectDto) {
return this.cmsService.updateProject(id, dto);
}
@ApiBearerAuth()
@Roles('admin')
@Put('clients/:id')
updateClient(@Param('id') id: string, @Body() dto: UpdateClientDto) {
return this.cmsService.updateClient(id, dto);
}
@ApiBearerAuth()
@Roles('admin')
@Delete('projects/:id')
deleteProject(@Param('id') id: string) {
return this.cmsService.deleteProject(id);
}
@ApiBearerAuth()
@Roles('admin')
@Delete('clients/:id')
deleteClient(@Param('id') id: string) {
return this.cmsService.deleteClient(id);
}
@ApiBearerAuth()
@Roles('admin')
@Post('projects/:id/restore')
restoreProject(@Param('id') id: string) {
return this.cmsService.restoreProject(id);
}
// ── Admin: Audit Logs ───────────────────────
@ApiBearerAuth()
@Roles('admin')
@Patch('projects/reorder')
reorderProjects(@Body() dto: ReorderProjectsDto) {
return this.cmsService.reorderProjects(dto);
}
@ApiBearerAuth()
@Roles('admin')
@Get('audit-logs')
findAuditLogs(
@Query('entity') entity?: string,
@Query('action') action?: string,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
) {
return this.cmsService.findAuditLogs({
entity,
action,
limit: limit ? parseInt(limit, 10) : 50,
offset: offset ? parseInt(offset, 10) : 0,
});
}
// ── Admin: Site Content ─────────────────────────
@ApiBearerAuth()
@Roles('admin')
@Put('content/:section')
upsertContent(
@Param('section') section: string,
@Query('locale') locale: string = 'tr',
@Body() dto: UpdateSiteContentDto,
) {
return this.cmsService.upsertContent(section, locale, dto);
}
// ── Admin: Media Upload ─────────────────────────
@ApiBearerAuth()
@Roles('admin')
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage }))
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: { file: { type: 'string', format: 'binary' } },
},
})
async uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
new FileTypeValidator({ fileType: /image\/(jpeg|jpg|png|webp|gif|svg\+xml)/ }),
],
}),
)
file: Express.Multer.File,
) {
const url = `/uploads/${file.filename}`;
const media = await this.cmsService.createMediaFile({
filename: file.filename,
originalName: file.originalname,
mimetype: file.mimetype,
path: file.path,
url,
size: file.size,
});
return media;
}
@Public()
@Get('media')
findAllMedia() {
return this.cmsService.findAllMedia();
}
@ApiBearerAuth()
@Roles('admin')
@Delete('media/:id')
deleteMedia(@Param('id') id: string) {
return this.cmsService.deleteMedia(id);
}
// ── Public: Clients ─────────────────────────
@Public()
@Get('clients')
findAllClients() {
return this.cmsService.findAllClients();
}
// ── Admin: Clients ─────────────────────────
@ApiBearerAuth()
@Roles('admin')
@Post('clients')
createClient(@Body() dto: CreateClientDto) {
return this.cmsService.createClient(dto);
}
@ApiBearerAuth()
@Roles('admin')
@Put('clients/:id')
updateClient(@Param('id') id: string, @Body() dto: UpdateClientDto) {
return this.cmsService.updateClient(id, dto);
}
@ApiBearerAuth()
@Roles('admin')
@Delete('clients/:id')
deleteClient(@Param('id') id: string) {
return this.cmsService.deleteClient(id);
}
// ── Admin: Audit Logs ───────────────────────
@ApiBearerAuth()
@Roles('admin')
@Get('audit-logs')
findAuditLogs(
@Query('entity') entity?: string,
@Query('action') action?: string,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
) {
return this.cmsService.findAuditLogs({
entity,
action,
limit: limit ? parseInt(limit, 10) : 50,
offset: offset ? parseInt(offset, 10) : 0,
});
}
@ApiBearerAuth()
@Roles('admin')
@Get('audit-logs/:entity/:entityId')
findEntityAuditLogs(
@Param('entity') entity: string,
@Param('entityId') entityId: string,
) {
return this.cmsService.findAuditLogsByEntity(entity, entityId);
}
@ApiBearerAuth()
@Roles('admin')
@Get('audit-logs/:entity/:entityId')
findEntityAuditLogs(
@Param('entity') entity: string,
@Param('entityId') entityId: string,
) {
return this.cmsService.findAuditLogsByEntity(entity, entityId);
}
}

View File

@@ -4,9 +4,9 @@ import { CmsService } from './cms.service';
import { DatabaseModule } from '../../database/database.module';
@Module({
imports: [DatabaseModule],
controllers: [CmsController],
providers: [CmsService],
exports: [CmsService],
imports: [DatabaseModule],
controllers: [CmsController],
providers: [CmsService],
exports: [CmsService],
})
export class CmsModule { }
export class CmsModule {}

View File

@@ -1,456 +1,493 @@
import { Injectable, Logger, NotFoundException, InternalServerErrorException } from '@nestjs/common';
import {
Injectable,
Logger,
NotFoundException,
InternalServerErrorException,
} from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../database/prisma.service';
import {
CreateProjectDto,
UpdateProjectDto,
ReorderProjectsDto,
UpdateSiteContentDto,
CreateClientDto,
UpdateClientDto,
CreateProjectDto,
UpdateProjectDto,
ReorderProjectsDto,
UpdateSiteContentDto,
CreateClientDto,
UpdateClientDto,
} from './dto';
@Injectable()
export class CmsService {
private readonly logger = new Logger(CmsService.name);
private readonly logger = new Logger(CmsService.name);
constructor(private readonly prisma: PrismaService) { }
constructor(private readonly prisma: PrismaService) {}
// ── Audit Logging ──────────────────────────────
// ── Audit Logging ──────────────────────────────
private async audit(
entity: string,
entityId: string,
action: string,
before?: any,
after?: any,
) {
try {
await this.prisma.auditLog.create({
data: {
entity,
entityId,
action,
before: before ? JSON.stringify(before) : null,
after: after ? JSON.stringify(after) : null,
},
});
this.logger.log(`[AUDIT] ${action} ${entity}:${entityId}`);
} catch (err) {
// Audit failure should never break the main operation
this.logger.error(`[AUDIT] Failed to log ${action} ${entity}:${entityId}`, err.stack);
}
private async audit(
entity: string,
entityId: string,
action: string,
before?: any,
after?: any,
) {
try {
await this.prisma.auditLog.create({
data: {
entity,
entityId,
action,
before: before ? JSON.stringify(before) : null,
after: after ? JSON.stringify(after) : null,
},
});
this.logger.log(`[AUDIT] ${action} ${entity}:${entityId}`);
} catch (err) {
// Audit failure should never break the main operation
this.logger.error(
`[AUDIT] Failed to log ${action} ${entity}:${entityId}`,
err.stack,
);
}
}
// ── Write Verification ─────────────────────────
private async verifyWrite<T extends { id: string }>(
model: string,
id: string,
operation: string,
): Promise<T> {
const record = await (this.prisma as any)[model].findUnique({
where: { id },
});
if (!record) {
this.logger.error(
`[VERIFY] ${operation} failed — record ${model}:${id} not found after write`,
);
throw new InternalServerErrorException(
`Veritabanı yazma doğrulaması başarısız: ${model}:${id} yazıldıktan sonra bulunamadı`,
);
}
this.logger.debug(`[VERIFY] ${operation} ${model}:${id} — OK`);
return record as T;
}
// ── Projects ──────────────────────────────────
async findAllProjects() {
return this.prisma.project.findMany({
where: { isActive: true, deletedAt: null },
orderBy: { sortOrder: 'asc' },
});
}
async findProjectById(id: string) {
const project = await this.prisma.project.findUnique({ where: { id } });
if (!project || project.deletedAt)
throw new NotFoundException('Project not found');
return project;
}
async createProject(dto: CreateProjectDto) {
// Auto-set sortOrder to last position
const lastProject = await this.prisma.project.findFirst({
where: { deletedAt: null },
orderBy: { sortOrder: 'desc' },
});
const nextOrder = (lastProject?.sortOrder ?? -1) + 1;
const created = await this.prisma.project.create({
data: {
title: dto.title,
image: dto.image,
roles: JSON.stringify(dto.roles),
color: dto.color ?? '#FF5733',
sortOrder: dto.sortOrder ?? nextOrder,
},
});
// Write verification
const verified = await this.verifyWrite(
'project',
created.id,
'CREATE Project',
);
// Audit log
await this.audit('project', created.id, 'CREATE', null, verified);
return verified;
}
async updateProject(id: string, dto: UpdateProjectDto) {
const before = await this.findProjectById(id); // throws if not found
await this.prisma.project.update({
where: { id },
data: {
...dto,
roles: dto.roles ? JSON.stringify(dto.roles) : undefined,
},
});
// Write verification
const verified = await this.verifyWrite('project', id, 'UPDATE Project');
// Audit log
await this.audit('project', id, 'UPDATE', before, verified);
return verified;
}
async deleteProject(id: string) {
const before = await this.findProjectById(id);
// Soft delete — projeyi silmek yerine deletedAt işaretle
const deleted = await this.prisma.project.update({
where: { id },
data: { deletedAt: new Date() },
});
// Write verification — deletedAt'ın set edildiğini doğrula
const verified = await (this.prisma as any).project.findUnique({
where: { id },
});
if (!verified || !verified.deletedAt) {
this.logger.error(
`[VERIFY] Soft DELETE failed — project:${id} deletedAt is still null`,
);
throw new InternalServerErrorException('Silme doğrulaması başarısız');
}
// ── Write Verification ─────────────────────────
// Audit log
await this.audit('project', id, 'DELETE', before, null);
private async verifyWrite<T extends { id: string }>(
model: string,
id: string,
operation: string,
): Promise<T> {
const record = await (this.prisma as any)[model].findUnique({ where: { id } });
if (!record) {
this.logger.error(`[VERIFY] ${operation} failed — record ${model}:${id} not found after write`);
throw new InternalServerErrorException(
`Veritabanı yazma doğrulaması başarısız: ${model}:${id} yazıldıktan sonra bulunamadı`,
);
}
this.logger.debug(`[VERIFY] ${operation} ${model}:${id} — OK`);
return record as T;
return deleted;
}
async restoreProject(id: string) {
const project = await this.prisma.project.findUnique({ where: { id } });
if (!project) throw new NotFoundException('Project not found');
if (!project.deletedAt)
throw new NotFoundException('Project is not deleted');
await this.prisma.project.update({
where: { id },
data: { deletedAt: null },
});
// Write verification
const verified = await this.verifyWrite('project', id, 'RESTORE Project');
// Audit log
await this.audit('project', id, 'RESTORE', null, verified);
return verified;
}
async reorderProjects(dto: ReorderProjectsDto) {
const updates = dto.items.map((item) =>
this.prisma.project.update({
where: { id: item.id },
data: { sortOrder: item.sortOrder },
}),
);
await this.prisma.$transaction(updates);
return this.findAllProjects();
}
// ── Site Content ──────────────────────────────
async findAllContent(locale?: string) {
const where = locale ? { locale } : {};
const contents = await this.prisma.siteContent.findMany({ where });
// Transform into a section-keyed object
const result: Record<string, any> = {};
for (const c of contents) {
if (!result[c.locale]) result[c.locale] = {};
try {
result[c.locale][c.section] = JSON.parse(c.content);
} catch {
result[c.locale][c.section] = c.content;
}
}
return result;
}
async findContentBySection(section: string, locale: string = 'tr') {
const content = await this.prisma.siteContent.findUnique({
where: { section_locale: { section, locale } },
});
if (!content) return null;
try {
return JSON.parse(content.content);
} catch {
return content.content;
}
}
async upsertContent(
section: string,
locale: string,
dto: UpdateSiteContentDto,
) {
const contentStr = JSON.stringify(dto.content);
// Get before state for audit
const before = await this.prisma.siteContent.findUnique({
where: { section_locale: { section, locale } },
});
const result = await this.prisma.siteContent.upsert({
where: { section_locale: { section, locale } },
create: {
section,
locale,
content: contentStr,
},
update: {
content: contentStr,
},
});
// Audit log
await this.audit(
'content',
result.id,
before ? 'UPDATE' : 'CREATE',
before,
result,
);
return result;
}
// ── Media Files ──────────────────────────────
async createMediaFile(file: {
filename: string;
originalName: string;
mimetype: string;
path: string;
url: string;
size: number;
}) {
const created = await this.prisma.mediaFile.create({ data: file });
// Audit log
await this.audit('media', created.id, 'CREATE', null, created);
return created;
}
async findAllMedia() {
return this.prisma.mediaFile.findMany({
orderBy: { createdAt: 'desc' },
});
}
async deleteMedia(id: string) {
const media = await this.prisma.mediaFile.findUnique({ where: { id } });
if (!media) throw new NotFoundException('Media not found');
const result = await this.prisma.mediaFile.delete({ where: { id } });
// Audit log
await this.audit('media', id, 'DELETE', media, null);
return result;
}
// ── Seed ──────────────────────────────────────
async seedDefaultProjects() {
const count = await this.prisma.project.count({
where: { deletedAt: null },
});
if (count > 0) {
this.logger.log('Projects already seeded, skipping');
return;
}
// ── Projects ──────────────────────────────────
const defaults = [
{
title: 'Deadpool',
image: 'https://picsum.photos/seed/deadpool/1920/1080?blur=2',
roles: JSON.stringify([
'Official Turkish Voice',
'Character Voice Acting',
]),
color: '#FF5733',
sortOrder: 0,
},
{
title: 'Spider-Man',
image: 'https://picsum.photos/seed/spiderman/1920/1080?blur=2',
roles: JSON.stringify([
'Official Turkish Voice',
'Character Voice Acting',
]),
color: '#C70039',
sortOrder: 1,
},
{
title: 'The Lion King',
image: 'https://picsum.photos/seed/lionking/1920/1080?blur=2',
roles: JSON.stringify(['Simba', 'Musical Performance', 'Vocals']),
color: '#900C3F',
sortOrder: 2,
},
{
title: 'Monster Notebook',
image: 'https://picsum.photos/seed/monster/1920/1080?blur=2',
roles: JSON.stringify(['Brand Face', 'Corporate Voice']),
color: '#511845',
sortOrder: 3,
},
];
async findAllProjects() {
return this.prisma.project.findMany({
where: { isActive: true, deletedAt: null },
orderBy: { sortOrder: 'asc' },
});
for (const p of defaults) {
const created = await this.prisma.project.create({ data: p });
await this.audit('project', created.id, 'CREATE', null, created);
}
this.logger.log(`Seeded ${defaults.length} default projects`);
}
async seedAdminUser() {
const adminExists = await this.prisma.user.findUnique({
where: { email: 'admin@haruncan.com' },
});
if (adminExists) {
this.logger.log('Admin user already exists, skipping');
return;
}
async findProjectById(id: string) {
const project = await this.prisma.project.findUnique({ where: { id } });
if (!project || project.deletedAt) throw new NotFoundException('Project not found');
return project;
}
const hashedPassword = await bcrypt.hash('admin123', 10);
async createProject(dto: CreateProjectDto) {
// Auto-set sortOrder to last position
const lastProject = await this.prisma.project.findFirst({
where: { deletedAt: null },
orderBy: { sortOrder: 'desc' },
});
const nextOrder = (lastProject?.sortOrder ?? -1) + 1;
const created = await this.prisma.project.create({
data: {
title: dto.title,
image: dto.image,
roles: JSON.stringify(dto.roles),
color: dto.color ?? '#FF5733',
sortOrder: dto.sortOrder ?? nextOrder,
await this.prisma.user.create({
data: {
email: 'admin@haruncan.com',
password: hashedPassword,
firstName: 'Harun',
lastName: 'CAN',
roles: {
create: {
role: {
connectOrCreate: {
where: { name: 'admin' },
create: { name: 'admin', description: 'Administrator' },
},
},
});
// Write verification
const verified = await this.verifyWrite('project', created.id, 'CREATE Project');
// Audit log
await this.audit('project', created.id, 'CREATE', null, verified);
return verified;
}
async updateProject(id: string, dto: UpdateProjectDto) {
const before = await this.findProjectById(id); // throws if not found
const updated = await this.prisma.project.update({
where: { id },
data: {
...dto,
roles: dto.roles ? JSON.stringify(dto.roles) : undefined,
},
});
// Write verification
const verified = await this.verifyWrite('project', id, 'UPDATE Project');
// Audit log
await this.audit('project', id, 'UPDATE', before, verified);
return verified;
}
async deleteProject(id: string) {
const before = await this.findProjectById(id);
// Soft delete — projeyi silmek yerine deletedAt işaretle
const deleted = await this.prisma.project.update({
where: { id },
data: { deletedAt: new Date() },
});
// Write verification — deletedAt'ın set edildiğini doğrula
const verified = await (this.prisma as any).project.findUnique({ where: { id } });
if (!verified || !verified.deletedAt) {
this.logger.error(`[VERIFY] Soft DELETE failed — project:${id} deletedAt is still null`);
throw new InternalServerErrorException('Silme doğrulaması başarısız');
}
// Audit log
await this.audit('project', id, 'DELETE', before, null);
return deleted;
}
async restoreProject(id: string) {
const project = await this.prisma.project.findUnique({ where: { id } });
if (!project) throw new NotFoundException('Project not found');
if (!project.deletedAt) throw new NotFoundException('Project is not deleted');
const restored = await this.prisma.project.update({
where: { id },
data: { deletedAt: null },
});
// Write verification
const verified = await this.verifyWrite('project', id, 'RESTORE Project');
// Audit log
await this.audit('project', id, 'RESTORE', null, verified);
return verified;
}
async reorderProjects(dto: ReorderProjectsDto) {
const updates = dto.items.map((item) =>
this.prisma.project.update({
where: { id: item.id },
data: { sortOrder: item.sortOrder },
}),
);
await this.prisma.$transaction(updates);
return this.findAllProjects();
}
// ── Site Content ──────────────────────────────
async findAllContent(locale?: string) {
const where = locale ? { locale } : {};
const contents = await this.prisma.siteContent.findMany({ where });
// Transform into a section-keyed object
const result: Record<string, any> = {};
for (const c of contents) {
if (!result[c.locale]) result[c.locale] = {};
try {
result[c.locale][c.section] = JSON.parse(c.content);
} catch {
result[c.locale][c.section] = c.content;
}
}
return result;
}
async findContentBySection(section: string, locale: string = 'tr') {
const content = await this.prisma.siteContent.findUnique({
where: { section_locale: { section, locale } },
});
if (!content) return null;
try {
return JSON.parse(content.content);
} catch {
return content.content;
}
}
async upsertContent(
section: string,
locale: string,
dto: UpdateSiteContentDto,
) {
const contentStr = JSON.stringify(dto.content);
// Get before state for audit
const before = await this.prisma.siteContent.findUnique({
where: { section_locale: { section, locale } },
});
const result = await this.prisma.siteContent.upsert({
where: { section_locale: { section, locale } },
create: {
section,
locale,
content: contentStr,
},
update: {
content: contentStr,
},
});
// Audit log
await this.audit(
'content',
result.id,
before ? 'UPDATE' : 'CREATE',
before,
result,
);
return result;
}
// ── Media Files ──────────────────────────────
async createMediaFile(file: {
filename: string;
originalName: string;
mimetype: string;
path: string;
url: string;
size: number;
}) {
const created = await this.prisma.mediaFile.create({ data: file });
// Audit log
await this.audit('media', created.id, 'CREATE', null, created);
return created;
}
async findAllMedia() {
return this.prisma.mediaFile.findMany({
orderBy: { createdAt: 'desc' },
});
}
async deleteMedia(id: string) {
const media = await this.prisma.mediaFile.findUnique({ where: { id } });
if (!media) throw new NotFoundException('Media not found');
const result = await this.prisma.mediaFile.delete({ where: { id } });
// Audit log
await this.audit('media', id, 'DELETE', media, null);
return result;
}
// ── Seed ──────────────────────────────────────
async seedDefaultProjects() {
const count = await this.prisma.project.count({
where: { deletedAt: null },
});
if (count > 0) {
this.logger.log('Projects already seeded, skipping');
return;
}
const defaults = [
{
title: 'Deadpool',
image: 'https://picsum.photos/seed/deadpool/1920/1080?blur=2',
roles: JSON.stringify(['Official Turkish Voice', 'Character Voice Acting']),
color: '#FF5733',
sortOrder: 0,
},
{
title: 'Spider-Man',
image: 'https://picsum.photos/seed/spiderman/1920/1080?blur=2',
roles: JSON.stringify(['Official Turkish Voice', 'Character Voice Acting']),
color: '#C70039',
sortOrder: 1,
},
{
title: 'The Lion King',
image: 'https://picsum.photos/seed/lionking/1920/1080?blur=2',
roles: JSON.stringify(['Simba', 'Musical Performance', 'Vocals']),
color: '#900C3F',
sortOrder: 2,
},
{
title: 'Monster Notebook',
image: 'https://picsum.photos/seed/monster/1920/1080?blur=2',
roles: JSON.stringify(['Brand Face', 'Corporate Voice']),
color: '#511845',
sortOrder: 3,
},
];
for (const p of defaults) {
const created = await this.prisma.project.create({ data: p });
await this.audit('project', created.id, 'CREATE', null, created);
}
this.logger.log(`Seeded ${defaults.length} default projects`);
}
async seedAdminUser() {
const adminExists = await this.prisma.user.findUnique({
where: { email: 'admin@haruncan.com' },
});
if (adminExists) {
this.logger.log('Admin user already exists, skipping');
return;
}
const hashedPassword = await bcrypt.hash('admin123', 10);
await this.prisma.user.create({
data: {
email: 'admin@haruncan.com',
password: hashedPassword,
firstName: 'Harun',
lastName: 'CAN',
roles: {
create: {
role: {
connectOrCreate: {
where: { name: 'admin' },
create: { name: 'admin', description: 'Administrator' },
},
},
},
},
},
});
this.logger.log('✅ Admin user created: admin@haruncan.com / admin123');
}
// ── Clients ──────────────────────────────────
async findAllClients() {
return this.prisma.client.findMany({
where: { isActive: true },
orderBy: { sortOrder: 'asc' },
});
}
async createClient(dto: CreateClientDto) {
const lastClient = await this.prisma.client.findFirst({
orderBy: { sortOrder: 'desc' },
});
const nextOrder = (lastClient?.sortOrder ?? -1) + 1;
const created = await this.prisma.client.create({
data: {
name: dto.name,
logo: dto.logo,
website: dto.website,
sortOrder: dto.sortOrder ?? nextOrder,
},
});
// Write verification
const verified = await this.verifyWrite('client', created.id, 'CREATE Client');
// Audit log
await this.audit('client', created.id, 'CREATE', null, verified);
return verified;
}
async updateClient(id: string, dto: UpdateClientDto) {
const before = await this.prisma.client.findUniqueOrThrow({ where: { id } }).catch(() => {
throw new NotFoundException('Client not found');
});
const updated = await this.prisma.client.update({ where: { id }, data: dto });
// Write verification
const verified = await this.verifyWrite('client', id, 'UPDATE Client');
// Audit log
await this.audit('client', id, 'UPDATE', before, verified);
return verified;
}
async deleteClient(id: string) {
const client = await this.prisma.client.findUnique({ where: { id } });
if (!client) throw new NotFoundException('Client not found');
const result = await this.prisma.client.delete({ where: { id } });
// Audit log
await this.audit('client', id, 'DELETE', client, null);
return result;
}
// ── Audit Logs (Admin Queries) ───────────────
async findAuditLogs(options?: {
entity?: string;
entityId?: string;
action?: string;
limit?: number;
offset?: number;
}) {
const where: any = {};
if (options?.entity) where.entity = options.entity;
if (options?.entityId) where.entityId = options.entityId;
if (options?.action) where.action = options.action;
const [logs, total] = await Promise.all([
this.prisma.auditLog.findMany({
where,
orderBy: { createdAt: 'desc' },
take: options?.limit ?? 50,
skip: options?.offset ?? 0,
}),
this.prisma.auditLog.count({ where }),
]);
return { logs, total };
}
async findAuditLogsByEntity(entity: string, entityId: string) {
return this.prisma.auditLog.findMany({
where: { entity, entityId },
orderBy: { createdAt: 'desc' },
});
}
},
},
},
});
this.logger.log('✅ Admin user created: admin@haruncan.com / admin123');
}
// ── Clients ──────────────────────────────────
async findAllClients() {
return this.prisma.client.findMany({
where: { isActive: true },
orderBy: { sortOrder: 'asc' },
});
}
async createClient(dto: CreateClientDto) {
const lastClient = await this.prisma.client.findFirst({
orderBy: { sortOrder: 'desc' },
});
const nextOrder = (lastClient?.sortOrder ?? -1) + 1;
const created = await this.prisma.client.create({
data: {
name: dto.name,
logo: dto.logo,
website: dto.website,
sortOrder: dto.sortOrder ?? nextOrder,
},
});
// Write verification
const verified = await this.verifyWrite(
'client',
created.id,
'CREATE Client',
);
// Audit log
await this.audit('client', created.id, 'CREATE', null, verified);
return verified;
}
async updateClient(id: string, dto: UpdateClientDto) {
const before = await this.prisma.client
.findUniqueOrThrow({ where: { id } })
.catch(() => {
throw new NotFoundException('Client not found');
});
await this.prisma.client.update({
where: { id },
data: dto,
});
// Write verification
const verified = await this.verifyWrite('client', id, 'UPDATE Client');
// Audit log
await this.audit('client', id, 'UPDATE', before, verified);
return verified;
}
async deleteClient(id: string) {
const client = await this.prisma.client.findUnique({ where: { id } });
if (!client) throw new NotFoundException('Client not found');
const result = await this.prisma.client.delete({ where: { id } });
// Audit log
await this.audit('client', id, 'DELETE', client, null);
return result;
}
// ── Audit Logs (Admin Queries) ───────────────
async findAuditLogs(options?: {
entity?: string;
entityId?: string;
action?: string;
limit?: number;
offset?: number;
}) {
const where: any = {};
if (options?.entity) where.entity = options.entity;
if (options?.entityId) where.entityId = options.entityId;
if (options?.action) where.action = options.action;
const [logs, total] = await Promise.all([
this.prisma.auditLog.findMany({
where,
orderBy: { createdAt: 'desc' },
take: options?.limit ?? 50,
skip: options?.offset ?? 0,
}),
this.prisma.auditLog.count({ where }),
]);
return { logs, total };
}
async findAuditLogsByEntity(entity: string, entityId: string) {
return this.prisma.auditLog.findMany({
where: { entity, entityId },
orderBy: { createdAt: 'desc' },
});
}
}

View File

@@ -1,142 +1,144 @@
import {
IsString,
IsOptional,
IsArray,
IsInt,
IsBoolean,
IsObject,
Min,
IsString,
IsOptional,
IsArray,
IsInt,
IsBoolean,
IsObject,
Min,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
// ── Projects ──────────────────────────────────────
export class CreateProjectDto {
@ApiProperty({ example: 'Deadpool' })
@IsString()
title: string;
@ApiProperty({ example: 'Deadpool' })
@IsString()
title: string;
@ApiProperty({ example: 'https://example.com/deadpool.jpg' })
@IsString()
image: string;
@ApiProperty({ example: 'https://example.com/deadpool.jpg' })
@IsString()
image: string;
@ApiProperty({ example: ['Official Turkish Voice', 'Character Voice Acting'] })
@IsArray()
@IsString({ each: true })
roles: string[];
@ApiProperty({
example: ['Official Turkish Voice', 'Character Voice Acting'],
})
@IsArray()
@IsString({ each: true })
roles: string[];
@ApiPropertyOptional({ example: '#FF5733' })
@IsOptional()
@IsString()
color?: string;
@ApiPropertyOptional({ example: '#FF5733' })
@IsOptional()
@IsString()
color?: string;
@ApiPropertyOptional({ example: 0 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ example: 0 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
}
export class UpdateProjectDto {
@ApiPropertyOptional({ example: 'Deadpool 3' })
@IsOptional()
@IsString()
title?: string;
@ApiPropertyOptional({ example: 'Deadpool 3' })
@IsOptional()
@IsString()
title?: string;
@ApiPropertyOptional({ example: 'https://example.com/deadpool3.jpg' })
@IsOptional()
@IsString()
image?: string;
@ApiPropertyOptional({ example: 'https://example.com/deadpool3.jpg' })
@IsOptional()
@IsString()
image?: string;
@ApiPropertyOptional({ example: ['Official Turkish Voice'] })
@IsOptional()
@IsArray()
@IsString({ each: true })
roles?: string[];
@ApiPropertyOptional({ example: ['Official Turkish Voice'] })
@IsOptional()
@IsArray()
@IsString({ each: true })
roles?: string[];
@ApiPropertyOptional({ example: '#C70039' })
@IsOptional()
@IsString()
color?: string;
@ApiPropertyOptional({ example: '#C70039' })
@IsOptional()
@IsString()
color?: string;
@ApiPropertyOptional({ example: 1 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ example: 1 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ example: true })
@IsOptional()
@IsBoolean()
isActive?: boolean;
@ApiPropertyOptional({ example: true })
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export class ReorderProjectsDto {
@ApiProperty({
example: [
{ id: 'uuid-1', sortOrder: 0 },
{ id: 'uuid-2', sortOrder: 1 },
],
})
@IsArray()
items: { id: string; sortOrder: number }[];
@ApiProperty({
example: [
{ id: 'uuid-1', sortOrder: 0 },
{ id: 'uuid-2', sortOrder: 1 },
],
})
@IsArray()
items: { id: string; sortOrder: number }[];
}
// ── Site Content ──────────────────────────────────
export class UpdateSiteContentDto {
@ApiProperty({ example: { title: 'Hello', desc: 'World' } })
@IsObject()
content: Record<string, any>;
@ApiProperty({ example: { title: 'Hello', desc: 'World' } })
@IsObject()
content: Record<string, any>;
}
// ── Clients ──────────────────────────────────────
export class CreateClientDto {
@ApiProperty({ example: 'Netflix' })
@IsString()
name: string;
@ApiProperty({ example: 'Netflix' })
@IsString()
name: string;
@ApiProperty({ example: 'https://example.com/netflix-logo.png' })
@IsString()
logo: string;
@ApiProperty({ example: 'https://example.com/netflix-logo.png' })
@IsString()
logo: string;
@ApiPropertyOptional({ example: 'https://netflix.com' })
@IsOptional()
@IsString()
website?: string;
@ApiPropertyOptional({ example: 'https://netflix.com' })
@IsOptional()
@IsString()
website?: string;
@ApiPropertyOptional({ example: 0 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ example: 0 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
}
export class UpdateClientDto {
@ApiPropertyOptional({ example: 'Netflix' })
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({ example: 'Netflix' })
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({ example: 'https://example.com/netflix-logo.png' })
@IsOptional()
@IsString()
logo?: string;
@ApiPropertyOptional({ example: 'https://example.com/netflix-logo.png' })
@IsOptional()
@IsString()
logo?: string;
@ApiPropertyOptional({ example: 'https://netflix.com' })
@IsOptional()
@IsString()
website?: string;
@ApiPropertyOptional({ example: 'https://netflix.com' })
@IsOptional()
@IsString()
website?: string;
@ApiPropertyOptional({ example: 0 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ example: 0 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ example: true })
@IsOptional()
@IsBoolean()
isActive?: boolean;
@ApiPropertyOptional({ example: true })
@IsOptional()
@IsBoolean()
isActive?: boolean;
}