21
package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.66.4",
|
"bullmq": "^5.66.4",
|
||||||
"cache-manager": "^7.2.7",
|
"cache-manager": "^7.2.7",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4942,6 +4944,12 @@
|
|||||||
"@types/superagent": "^8.1.0"
|
"@types/superagent": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/validator": {
|
"node_modules/@types/validator": {
|
||||||
"version": "13.15.10",
|
"version": "13.15.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
||||||
@@ -12514,6 +12522,19 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist-node/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.66.4",
|
"bullmq": "^5.66.4",
|
||||||
"cache-manager": "^7.2.7",
|
"cache-manager": "^7.2.7",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, Logger as NestLogger } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
@@ -41,6 +41,7 @@ import { AdminModule } from './modules/admin/admin.module';
|
|||||||
import { HealthModule } from './modules/health/health.module';
|
import { HealthModule } from './modules/health/health.module';
|
||||||
import { GeminiModule } from './modules/gemini/gemini.module';
|
import { GeminiModule } from './modules/gemini/gemini.module';
|
||||||
import { CmsModule } from './modules/cms/cms.module';
|
import { CmsModule } from './modules/cms/cms.module';
|
||||||
|
import { MailModule } from './modules/mail/mail.module';
|
||||||
|
|
||||||
// Guards
|
// Guards
|
||||||
import {
|
import {
|
||||||
@@ -71,7 +72,7 @@ import {
|
|||||||
LoggerModule.forRootAsync({
|
LoggerModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: (configService: ConfigService) => {
|
||||||
return {
|
return {
|
||||||
pinoHttp: {
|
pinoHttp: {
|
||||||
level: configService.get('app.isDevelopment') ? 'debug' : 'info',
|
level: configService.get('app.isDevelopment') ? 'debug' : 'info',
|
||||||
@@ -121,6 +122,7 @@ import {
|
|||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
|
const logger = new NestLogger('CacheModule');
|
||||||
const useRedis = configService.get('REDIS_ENABLED', 'false') === 'true';
|
const useRedis = configService.get('REDIS_ENABLED', 'false') === 'true';
|
||||||
|
|
||||||
if (useRedis) {
|
if (useRedis) {
|
||||||
@@ -132,18 +134,18 @@ import {
|
|||||||
},
|
},
|
||||||
ttl: 60 * 1000, // 1 minute default
|
ttl: 60 * 1000, // 1 minute default
|
||||||
});
|
});
|
||||||
console.log('✅ Redis cache connected');
|
logger.log('✅ Redis cache connected');
|
||||||
return {
|
return {
|
||||||
store: store as unknown as any,
|
store: store as unknown as any,
|
||||||
ttl: 60 * 1000,
|
ttl: 60 * 1000,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
console.warn('⚠️ Redis connection failed, using in-memory cache');
|
logger.warn('⚠️ Redis connection failed, using in-memory cache');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to in-memory cache
|
// Fallback to in-memory cache
|
||||||
console.log('📦 Using in-memory cache');
|
logger.log('📦 Using in-memory cache');
|
||||||
return {
|
return {
|
||||||
ttl: 60 * 1000,
|
ttl: 60 * 1000,
|
||||||
};
|
};
|
||||||
@@ -166,6 +168,9 @@ import {
|
|||||||
// CMS Module
|
// CMS Module
|
||||||
CmsModule,
|
CmsModule,
|
||||||
|
|
||||||
|
// Mail Module
|
||||||
|
MailModule,
|
||||||
|
|
||||||
// Serve uploaded files
|
// Serve uploaded files
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
rootPath: path.join(__dirname, '..', 'uploads'),
|
rootPath: path.join(__dirname, '..', 'uploads'),
|
||||||
@@ -210,4 +215,10 @@ import {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {
|
||||||
|
private readonly logger = new NestLogger(AppModule.name);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger.log('AppModule initialized');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
|||||||
});
|
});
|
||||||
// Only update if translation exists (key is different from result)
|
// Only update if translation exists (key is different from result)
|
||||||
if (translatedMessage !== `errors.${message}`) {
|
if (translatedMessage !== `errors.${message}`) {
|
||||||
message = translatedMessage as string;
|
message = translatedMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
10
src/main.ts
@@ -18,10 +18,12 @@ async function bootstrap() {
|
|||||||
app.useGlobalInterceptors(new LoggerErrorInterceptor());
|
app.useGlobalInterceptors(new LoggerErrorInterceptor());
|
||||||
|
|
||||||
// Security Headers
|
// Security Headers
|
||||||
app.use(helmet({
|
app.use(
|
||||||
contentSecurityPolicy: false,
|
helmet({
|
||||||
crossOriginEmbedderPolicy: false,
|
contentSecurityPolicy: false,
|
||||||
}));
|
crossOriginEmbedderPolicy: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Graceful Shutdown (Prisma & Docker)
|
// Graceful Shutdown (Prisma & Docker)
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Patch,
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
ParseFilePipe,
|
BadRequestException,
|
||||||
MaxFileSizeValidator,
|
OnModuleInit,
|
||||||
FileTypeValidator,
|
|
||||||
OnModuleInit,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { ApiTags, ApiBearerAuth, ApiConsumes, ApiBody } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth, ApiConsumes, ApiBody } from '@nestjs/swagger';
|
||||||
@@ -21,212 +19,215 @@ import { diskStorage } from 'multer';
|
|||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { CmsService } from './cms.service';
|
import { CmsService } from './cms.service';
|
||||||
import {
|
import {
|
||||||
CreateProjectDto,
|
CreateProjectDto,
|
||||||
UpdateProjectDto,
|
UpdateProjectDto,
|
||||||
ReorderProjectsDto,
|
ReorderProjectsDto,
|
||||||
UpdateSiteContentDto,
|
UpdateSiteContentDto,
|
||||||
CreateClientDto,
|
CreateClientDto,
|
||||||
UpdateClientDto,
|
UpdateClientDto,
|
||||||
} from './dto';
|
} from './dto';
|
||||||
import { Public, Roles } from '../../common/decorators';
|
import { Public, Roles } from '../../common/decorators';
|
||||||
|
|
||||||
// Generate unique filename
|
// Generate unique filename
|
||||||
const storage = diskStorage({
|
const storage = diskStorage({
|
||||||
destination: './uploads',
|
destination: './uploads',
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${extname(file.originalname)}`;
|
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${extname(file.originalname)}`;
|
||||||
cb(null, uniqueName);
|
cb(null, uniqueName);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ApiTags('CMS')
|
@ApiTags('CMS')
|
||||||
@Controller('cms')
|
@Controller('cms')
|
||||||
export class CmsController implements OnModuleInit {
|
export class CmsController implements OnModuleInit {
|
||||||
constructor(private readonly cmsService: CmsService) { }
|
constructor(private readonly cmsService: CmsService) { }
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.cmsService.seedDefaultProjects();
|
await this.cmsService.seedDefaultProjects();
|
||||||
await this.cmsService.seedAdminUser();
|
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()
|
@ApiBearerAuth()
|
||||||
@Get('projects')
|
@Roles('admin')
|
||||||
findAllProjects() {
|
@Delete('media/:id')
|
||||||
return this.cmsService.findAllProjects();
|
deleteMedia(@Param('id') id: string) {
|
||||||
}
|
return this.cmsService.deleteMedia(id);
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
// ── Public: Clients ─────────────────────────
|
||||||
@Get('content')
|
|
||||||
findAllContent(@Query('locale') locale?: string) {
|
|
||||||
return this.cmsService.findAllContent(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Get('content/:section')
|
@Get('clients')
|
||||||
findContentBySection(
|
findAllClients() {
|
||||||
@Param('section') section: string,
|
return this.cmsService.findAllClients();
|
||||||
@Query('locale') locale?: string,
|
}
|
||||||
) {
|
|
||||||
return this.cmsService.findContentBySection(section, locale ?? 'tr');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Admin: Projects ─────────────────────────────
|
// ── Admin: Clients ─────────────────────────
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
@Post('projects')
|
@Post('clients')
|
||||||
createProject(@Body() dto: CreateProjectDto) {
|
createClient(@Body() dto: CreateClientDto) {
|
||||||
return this.cmsService.createProject(dto);
|
return this.cmsService.createClient(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
@Put('projects/:id')
|
@Put('clients/:id')
|
||||||
updateProject(@Param('id') id: string, @Body() dto: UpdateProjectDto) {
|
updateClient(@Param('id') id: string, @Body() dto: UpdateClientDto) {
|
||||||
return this.cmsService.updateProject(id, dto);
|
return this.cmsService.updateClient(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
@Delete('projects/:id')
|
@Delete('clients/:id')
|
||||||
deleteProject(@Param('id') id: string) {
|
deleteClient(@Param('id') id: string) {
|
||||||
return this.cmsService.deleteProject(id);
|
return this.cmsService.deleteClient(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBearerAuth()
|
// ── Admin: Audit Logs ───────────────────────
|
||||||
@Roles('admin')
|
|
||||||
@Post('projects/:id/restore')
|
|
||||||
restoreProject(@Param('id') id: string) {
|
|
||||||
return this.cmsService.restoreProject(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
@Patch('projects/reorder')
|
@Get('audit-logs')
|
||||||
reorderProjects(@Body() dto: ReorderProjectsDto) {
|
findAuditLogs(
|
||||||
return this.cmsService.reorderProjects(dto);
|
@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')
|
||||||
@ApiBearerAuth()
|
@Get('audit-logs/:entity/:entityId')
|
||||||
@Roles('admin')
|
findEntityAuditLogs(
|
||||||
@Put('content/:section')
|
@Param('entity') entity: string,
|
||||||
upsertContent(
|
@Param('entityId') entityId: string,
|
||||||
@Param('section') section: string,
|
) {
|
||||||
@Query('locale') locale: string = 'tr',
|
return this.cmsService.findAuditLogsByEntity(entity, entityId);
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { CmsService } from './cms.service';
|
|||||||
import { DatabaseModule } from '../../database/database.module';
|
import { DatabaseModule } from '../../database/database.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule],
|
imports: [DatabaseModule],
|
||||||
controllers: [CmsController],
|
controllers: [CmsController],
|
||||||
providers: [CmsService],
|
providers: [CmsService],
|
||||||
exports: [CmsService],
|
exports: [CmsService],
|
||||||
})
|
})
|
||||||
export class CmsModule { }
|
export class CmsModule {}
|
||||||
|
|||||||
@@ -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 * as bcrypt from 'bcrypt';
|
||||||
import { PrismaService } from '../../database/prisma.service';
|
import { PrismaService } from '../../database/prisma.service';
|
||||||
import {
|
import {
|
||||||
CreateProjectDto,
|
CreateProjectDto,
|
||||||
UpdateProjectDto,
|
UpdateProjectDto,
|
||||||
ReorderProjectsDto,
|
ReorderProjectsDto,
|
||||||
UpdateSiteContentDto,
|
UpdateSiteContentDto,
|
||||||
CreateClientDto,
|
CreateClientDto,
|
||||||
UpdateClientDto,
|
UpdateClientDto,
|
||||||
} from './dto';
|
} from './dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CmsService {
|
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(
|
private async audit(
|
||||||
entity: string,
|
entity: string,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
action: string,
|
action: string,
|
||||||
before?: any,
|
before?: any,
|
||||||
after?: any,
|
after?: any,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await this.prisma.auditLog.create({
|
await this.prisma.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
entity,
|
entity,
|
||||||
entityId,
|
entityId,
|
||||||
action,
|
action,
|
||||||
before: before ? JSON.stringify(before) : null,
|
before: before ? JSON.stringify(before) : null,
|
||||||
after: after ? JSON.stringify(after) : null,
|
after: after ? JSON.stringify(after) : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.logger.log(`[AUDIT] ${action} ${entity}:${entityId}`);
|
this.logger.log(`[AUDIT] ${action} ${entity}:${entityId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Audit failure should never break the main operation
|
// Audit failure should never break the main operation
|
||||||
this.logger.error(`[AUDIT] Failed to log ${action} ${entity}:${entityId}`, err.stack);
|
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 }>(
|
return deleted;
|
||||||
model: string,
|
}
|
||||||
id: string,
|
|
||||||
operation: string,
|
async restoreProject(id: string) {
|
||||||
): Promise<T> {
|
const project = await this.prisma.project.findUnique({ where: { id } });
|
||||||
const record = await (this.prisma as any)[model].findUnique({ where: { id } });
|
if (!project) throw new NotFoundException('Project not found');
|
||||||
if (!record) {
|
if (!project.deletedAt)
|
||||||
this.logger.error(`[VERIFY] ${operation} failed — record ${model}:${id} not found after write`);
|
throw new NotFoundException('Project is not deleted');
|
||||||
throw new InternalServerErrorException(
|
|
||||||
`Veritabanı yazma doğrulaması başarısız: ${model}:${id} yazıldıktan sonra bulunamadı`,
|
await this.prisma.project.update({
|
||||||
);
|
where: { id },
|
||||||
}
|
data: { deletedAt: null },
|
||||||
this.logger.debug(`[VERIFY] ${operation} ${model}:${id} — OK`);
|
});
|
||||||
return record as T;
|
|
||||||
|
// 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() {
|
for (const p of defaults) {
|
||||||
return this.prisma.project.findMany({
|
const created = await this.prisma.project.create({ data: p });
|
||||||
where: { isActive: true, deletedAt: null },
|
await this.audit('project', created.id, 'CREATE', null, created);
|
||||||
orderBy: { sortOrder: 'asc' },
|
}
|
||||||
});
|
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 hashedPassword = await bcrypt.hash('admin123', 10);
|
||||||
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) {
|
await this.prisma.user.create({
|
||||||
// Auto-set sortOrder to last position
|
data: {
|
||||||
const lastProject = await this.prisma.project.findFirst({
|
email: 'admin@haruncan.com',
|
||||||
where: { deletedAt: null },
|
password: hashedPassword,
|
||||||
orderBy: { sortOrder: 'desc' },
|
firstName: 'Harun',
|
||||||
});
|
lastName: 'CAN',
|
||||||
const nextOrder = (lastProject?.sortOrder ?? -1) + 1;
|
roles: {
|
||||||
|
create: {
|
||||||
const created = await this.prisma.project.create({
|
role: {
|
||||||
data: {
|
connectOrCreate: {
|
||||||
title: dto.title,
|
where: { name: 'admin' },
|
||||||
image: dto.image,
|
create: { name: 'admin', description: 'Administrator' },
|
||||||
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
|
this.logger.log('✅ Admin user created: admin@haruncan.com / admin123');
|
||||||
await this.audit('project', created.id, 'CREATE', null, verified);
|
}
|
||||||
|
|
||||||
return verified;
|
// ── Clients ──────────────────────────────────
|
||||||
}
|
|
||||||
|
async findAllClients() {
|
||||||
async updateProject(id: string, dto: UpdateProjectDto) {
|
return this.prisma.client.findMany({
|
||||||
const before = await this.findProjectById(id); // throws if not found
|
where: { isActive: true },
|
||||||
|
orderBy: { sortOrder: 'asc' },
|
||||||
const updated = await this.prisma.project.update({
|
});
|
||||||
where: { id },
|
}
|
||||||
data: {
|
|
||||||
...dto,
|
async createClient(dto: CreateClientDto) {
|
||||||
roles: dto.roles ? JSON.stringify(dto.roles) : undefined,
|
const lastClient = await this.prisma.client.findFirst({
|
||||||
},
|
orderBy: { sortOrder: 'desc' },
|
||||||
});
|
});
|
||||||
|
const nextOrder = (lastClient?.sortOrder ?? -1) + 1;
|
||||||
// Write verification
|
|
||||||
const verified = await this.verifyWrite('project', id, 'UPDATE Project');
|
const created = await this.prisma.client.create({
|
||||||
|
data: {
|
||||||
// Audit log
|
name: dto.name,
|
||||||
await this.audit('project', id, 'UPDATE', before, verified);
|
logo: dto.logo,
|
||||||
|
website: dto.website,
|
||||||
return verified;
|
sortOrder: dto.sortOrder ?? nextOrder,
|
||||||
}
|
},
|
||||||
|
});
|
||||||
async deleteProject(id: string) {
|
|
||||||
const before = await this.findProjectById(id);
|
// Write verification
|
||||||
|
const verified = await this.verifyWrite(
|
||||||
// Soft delete — projeyi silmek yerine deletedAt işaretle
|
'client',
|
||||||
const deleted = await this.prisma.project.update({
|
created.id,
|
||||||
where: { id },
|
'CREATE Client',
|
||||||
data: { deletedAt: new Date() },
|
);
|
||||||
});
|
|
||||||
|
// Audit log
|
||||||
// Write verification — deletedAt'ın set edildiğini doğrula
|
await this.audit('client', created.id, 'CREATE', null, verified);
|
||||||
const verified = await (this.prisma as any).project.findUnique({ where: { id } });
|
|
||||||
if (!verified || !verified.deletedAt) {
|
return verified;
|
||||||
this.logger.error(`[VERIFY] Soft DELETE failed — project:${id} deletedAt is still null`);
|
}
|
||||||
throw new InternalServerErrorException('Silme doğrulaması başarısız');
|
|
||||||
}
|
async updateClient(id: string, dto: UpdateClientDto) {
|
||||||
|
const before = await this.prisma.client
|
||||||
// Audit log
|
.findUniqueOrThrow({ where: { id } })
|
||||||
await this.audit('project', id, 'DELETE', before, null);
|
.catch(() => {
|
||||||
|
throw new NotFoundException('Client not found');
|
||||||
return deleted;
|
});
|
||||||
}
|
|
||||||
|
await this.prisma.client.update({
|
||||||
async restoreProject(id: string) {
|
where: { id },
|
||||||
const project = await this.prisma.project.findUnique({ where: { id } });
|
data: dto,
|
||||||
if (!project) throw new NotFoundException('Project not found');
|
});
|
||||||
if (!project.deletedAt) throw new NotFoundException('Project is not deleted');
|
|
||||||
|
// Write verification
|
||||||
const restored = await this.prisma.project.update({
|
const verified = await this.verifyWrite('client', id, 'UPDATE Client');
|
||||||
where: { id },
|
|
||||||
data: { deletedAt: null },
|
// Audit log
|
||||||
});
|
await this.audit('client', id, 'UPDATE', before, verified);
|
||||||
|
|
||||||
// Write verification
|
return verified;
|
||||||
const verified = await this.verifyWrite('project', id, 'RESTORE Project');
|
}
|
||||||
|
|
||||||
// Audit log
|
async deleteClient(id: string) {
|
||||||
await this.audit('project', id, 'RESTORE', null, verified);
|
const client = await this.prisma.client.findUnique({ where: { id } });
|
||||||
|
if (!client) throw new NotFoundException('Client not found');
|
||||||
return verified;
|
|
||||||
}
|
const result = await this.prisma.client.delete({ where: { id } });
|
||||||
|
|
||||||
async reorderProjects(dto: ReorderProjectsDto) {
|
// Audit log
|
||||||
const updates = dto.items.map((item) =>
|
await this.audit('client', id, 'DELETE', client, null);
|
||||||
this.prisma.project.update({
|
|
||||||
where: { id: item.id },
|
return result;
|
||||||
data: { sortOrder: item.sortOrder },
|
}
|
||||||
}),
|
|
||||||
);
|
// ── Audit Logs (Admin Queries) ───────────────
|
||||||
await this.prisma.$transaction(updates);
|
|
||||||
return this.findAllProjects();
|
async findAuditLogs(options?: {
|
||||||
}
|
entity?: string;
|
||||||
|
entityId?: string;
|
||||||
// ── Site Content ──────────────────────────────
|
action?: string;
|
||||||
|
limit?: number;
|
||||||
async findAllContent(locale?: string) {
|
offset?: number;
|
||||||
const where = locale ? { locale } : {};
|
}) {
|
||||||
const contents = await this.prisma.siteContent.findMany({ where });
|
const where: any = {};
|
||||||
|
if (options?.entity) where.entity = options.entity;
|
||||||
// Transform into a section-keyed object
|
if (options?.entityId) where.entityId = options.entityId;
|
||||||
const result: Record<string, any> = {};
|
if (options?.action) where.action = options.action;
|
||||||
for (const c of contents) {
|
|
||||||
if (!result[c.locale]) result[c.locale] = {};
|
const [logs, total] = await Promise.all([
|
||||||
try {
|
this.prisma.auditLog.findMany({
|
||||||
result[c.locale][c.section] = JSON.parse(c.content);
|
where,
|
||||||
} catch {
|
orderBy: { createdAt: 'desc' },
|
||||||
result[c.locale][c.section] = c.content;
|
take: options?.limit ?? 50,
|
||||||
}
|
skip: options?.offset ?? 0,
|
||||||
}
|
}),
|
||||||
return result;
|
this.prisma.auditLog.count({ where }),
|
||||||
}
|
]);
|
||||||
|
|
||||||
async findContentBySection(section: string, locale: string = 'tr') {
|
return { logs, total };
|
||||||
const content = await this.prisma.siteContent.findUnique({
|
}
|
||||||
where: { section_locale: { section, locale } },
|
|
||||||
});
|
async findAuditLogsByEntity(entity: string, entityId: string) {
|
||||||
if (!content) return null;
|
return this.prisma.auditLog.findMany({
|
||||||
try {
|
where: { entity, entityId },
|
||||||
return JSON.parse(content.content);
|
orderBy: { createdAt: 'desc' },
|
||||||
} 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' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,142 +1,144 @@
|
|||||||
import {
|
import {
|
||||||
IsString,
|
IsString,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsArray,
|
IsArray,
|
||||||
IsInt,
|
IsInt,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsObject,
|
IsObject,
|
||||||
Min,
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
// ── Projects ──────────────────────────────────────
|
// ── Projects ──────────────────────────────────────
|
||||||
|
|
||||||
export class CreateProjectDto {
|
export class CreateProjectDto {
|
||||||
@ApiProperty({ example: 'Deadpool' })
|
@ApiProperty({ example: 'Deadpool' })
|
||||||
@IsString()
|
@IsString()
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'https://example.com/deadpool.jpg' })
|
@ApiProperty({ example: 'https://example.com/deadpool.jpg' })
|
||||||
@IsString()
|
@IsString()
|
||||||
image: string;
|
image: string;
|
||||||
|
|
||||||
@ApiProperty({ example: ['Official Turkish Voice', 'Character Voice Acting'] })
|
@ApiProperty({
|
||||||
@IsArray()
|
example: ['Official Turkish Voice', 'Character Voice Acting'],
|
||||||
@IsString({ each: true })
|
})
|
||||||
roles: string[];
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
roles: string[];
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: '#FF5733' })
|
@ApiPropertyOptional({ example: '#FF5733' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 0 })
|
@ApiPropertyOptional({ example: 0 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateProjectDto {
|
export class UpdateProjectDto {
|
||||||
@ApiPropertyOptional({ example: 'Deadpool 3' })
|
@ApiPropertyOptional({ example: 'Deadpool 3' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 'https://example.com/deadpool3.jpg' })
|
@ApiPropertyOptional({ example: 'https://example.com/deadpool3.jpg' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
image?: string;
|
image?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: ['Official Turkish Voice'] })
|
@ApiPropertyOptional({ example: ['Official Turkish Voice'] })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: '#C70039' })
|
@ApiPropertyOptional({ example: '#C70039' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 1 })
|
@ApiPropertyOptional({ example: 1 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: true })
|
@ApiPropertyOptional({ example: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReorderProjectsDto {
|
export class ReorderProjectsDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: [
|
example: [
|
||||||
{ id: 'uuid-1', sortOrder: 0 },
|
{ id: 'uuid-1', sortOrder: 0 },
|
||||||
{ id: 'uuid-2', sortOrder: 1 },
|
{ id: 'uuid-2', sortOrder: 1 },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@IsArray()
|
@IsArray()
|
||||||
items: { id: string; sortOrder: number }[];
|
items: { id: string; sortOrder: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Site Content ──────────────────────────────────
|
// ── Site Content ──────────────────────────────────
|
||||||
|
|
||||||
export class UpdateSiteContentDto {
|
export class UpdateSiteContentDto {
|
||||||
@ApiProperty({ example: { title: 'Hello', desc: 'World' } })
|
@ApiProperty({ example: { title: 'Hello', desc: 'World' } })
|
||||||
@IsObject()
|
@IsObject()
|
||||||
content: Record<string, any>;
|
content: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Clients ──────────────────────────────────────
|
// ── Clients ──────────────────────────────────────
|
||||||
|
|
||||||
export class CreateClientDto {
|
export class CreateClientDto {
|
||||||
@ApiProperty({ example: 'Netflix' })
|
@ApiProperty({ example: 'Netflix' })
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'https://example.com/netflix-logo.png' })
|
@ApiProperty({ example: 'https://example.com/netflix-logo.png' })
|
||||||
@IsString()
|
@IsString()
|
||||||
logo: string;
|
logo: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 'https://netflix.com' })
|
@ApiPropertyOptional({ example: 'https://netflix.com' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
website?: string;
|
website?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 0 })
|
@ApiPropertyOptional({ example: 0 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateClientDto {
|
export class UpdateClientDto {
|
||||||
@ApiPropertyOptional({ example: 'Netflix' })
|
@ApiPropertyOptional({ example: 'Netflix' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 'https://example.com/netflix-logo.png' })
|
@ApiPropertyOptional({ example: 'https://example.com/netflix-logo.png' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
logo?: string;
|
logo?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 'https://netflix.com' })
|
@ApiPropertyOptional({ example: 'https://netflix.com' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
website?: string;
|
website?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 0 })
|
@ApiPropertyOptional({ example: 0 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: true })
|
@ApiPropertyOptional({ example: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/modules/mail/mail.controller.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Controller, Post, Body, Get } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { MailService, ContactMailData } from './mail.service';
|
||||||
|
import { Public } from '../../common/decorators';
|
||||||
|
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
class SendContactDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
details: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
captchaId: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
captchaAnswer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('Mail')
|
||||||
|
@Controller('mail')
|
||||||
|
export class MailController {
|
||||||
|
constructor(private readonly mailService: MailService) { }
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('send')
|
||||||
|
@ApiOperation({ summary: 'Send contact form message' })
|
||||||
|
async sendContact(@Body() dto: SendContactDto) {
|
||||||
|
return this.mailService.sendContactMail(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('captcha')
|
||||||
|
@ApiOperation({ summary: 'Get a new math captcha' })
|
||||||
|
async getCaptcha() {
|
||||||
|
return this.mailService.generateCaptcha();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/modules/mail/mail.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MailService } from './mail.service';
|
||||||
|
import { MailController } from './mail.controller';
|
||||||
|
import { CmsModule } from '../cms/cms.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [CmsModule],
|
||||||
|
providers: [MailService],
|
||||||
|
controllers: [MailController],
|
||||||
|
exports: [MailService],
|
||||||
|
})
|
||||||
|
export class MailModule { }
|
||||||
123
src/modules/mail/mail.service.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { Injectable, Logger, Inject, BadRequestException } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
|
import type { Cache } from 'cache-manager';
|
||||||
|
import * as nodemailer from 'nodemailer';
|
||||||
|
import { CmsService } from '../cms/cms.service';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export interface ContactMailData {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
type: string;
|
||||||
|
details: string;
|
||||||
|
captchaId: string;
|
||||||
|
captchaAnswer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
private readonly logger = new Logger(MailService.name);
|
||||||
|
private transporter: nodemailer.Transporter;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private configService: ConfigService,
|
||||||
|
private cmsService: CmsService,
|
||||||
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||||
|
) {
|
||||||
|
this.initTransporter();
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateCaptcha() {
|
||||||
|
const num1 = Math.floor(Math.random() * 10) + 1;
|
||||||
|
const num2 = Math.floor(Math.random() * 10) + 1;
|
||||||
|
const answer = num1 + num2;
|
||||||
|
const id = uuidv4();
|
||||||
|
|
||||||
|
await this.cacheManager.set(`captcha:${id}`, answer.toString(), 300000); // 5 mins
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
question: `${num1} + ${num2} = ?`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyCaptcha(id: string, answer: string) {
|
||||||
|
const cachedAnswer = await this.cacheManager.get(`captcha:${id}`);
|
||||||
|
if (!cachedAnswer) {
|
||||||
|
throw new BadRequestException('Captcha expired or invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(cachedAnswer) !== String(answer).trim()) {
|
||||||
|
throw new BadRequestException('Incorrect captcha answer');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.cacheManager.del(`captcha:${id}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initTransporter(customConfig?: any) {
|
||||||
|
this.transporter = nodemailer.createTransport({
|
||||||
|
host: customConfig?.host || this.configService.get<string>('MAIL_HOST'),
|
||||||
|
port: customConfig?.port || this.configService.get<number>('MAIL_PORT', 587),
|
||||||
|
secure: customConfig?.secure === 'true' || customConfig?.secure === true,
|
||||||
|
auth: {
|
||||||
|
user: customConfig?.user || this.configService.get<string>('MAIL_USER'),
|
||||||
|
pass: customConfig?.pass || this.configService.get<string>('MAIL_PASSWORD'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendContactMail(data: ContactMailData) {
|
||||||
|
// Verify Captcha
|
||||||
|
await this.verifyCaptcha(data.captchaId, data.captchaAnswer);
|
||||||
|
|
||||||
|
const isMailEnabled = this.configService.get<boolean>('features.mail', false);
|
||||||
|
|
||||||
|
// Alıcı mailini veritabanından veya varsayılandan al
|
||||||
|
const settings = await this.cmsService.findContentBySection('mail_settings', 'tr');
|
||||||
|
const targetEmail = settings?.targetEmail || 'haruncanmedia@gmail.com';
|
||||||
|
|
||||||
|
if (!isMailEnabled) {
|
||||||
|
this.logger.warn('📧 Mail service is disabled. Message not sent, but logged:');
|
||||||
|
this.logger.log(`TO: ${targetEmail}, FROM: ${data.name} <${data.email}>, TYPE: ${data.type}, MESSAGE: ${data.details}`);
|
||||||
|
return { success: true, message: 'Mail dev-mode: logged to console' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eğer veritabanında SMTP ayarları varsa transporter'ı güncelle
|
||||||
|
if (settings && settings.host && settings.user && settings.pass) {
|
||||||
|
this.logger.log('📧 Using dynamic SMTP settings from database');
|
||||||
|
this.initTransporter(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await this.transporter.sendMail({
|
||||||
|
from: settings?.from || this.configService.get<string>('MAIL_FROM', '"HarunCAN Studio" <noreply@haruncan.com>'),
|
||||||
|
to: targetEmail,
|
||||||
|
subject: `New Contact Form Message from ${data.name}`,
|
||||||
|
text: `Name: ${data.name}\nEmail: ${data.email}\nInquiry Type: ${data.type}\nDetails: ${data.details}`,
|
||||||
|
html: `
|
||||||
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||||
|
<h2 style="color: #FF5733; border-bottom: 2px solid #FF5733; padding-bottom: 10px;">New Contact Form Submission</h2>
|
||||||
|
<p><strong>Name:</strong> ${data.name}</p>
|
||||||
|
<p><strong>Email:</strong> ${data.email}</p>
|
||||||
|
<p><strong>Inquiry Type:</strong> ${data.type}</p>
|
||||||
|
<div style="background: #f8fafc; padding: 15px; border-left: 4px solid #FF5733; margin-top: 20px;">
|
||||||
|
<p style="margin: 0; color: #475569;"><strong>Message Details:</strong></p>
|
||||||
|
<p style="margin-top: 10px; line-height: 1.6;">${data.details.replace(/\n/g, '<br>')}</p>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 12px; color: #94a3b8; margin-top: 30px; border-top: 1px solid #e2e8f0; padding-top: 10px;">
|
||||||
|
Sent from HarunCAN Studio Contact Form
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`📧 Contact mail sent: ${info.messageId}`);
|
||||||
|
return { success: true, messageId: info.messageId };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Failed to send contact mail:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
uploads/1773064602345-93585883.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
uploads/1773064710998-3327682.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/1773064802421-246519346.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
uploads/1773064868940-732271018.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
uploads/1773064923678-146730379.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
105
uploads/1773066611451-178319299.json
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"name": "bbb",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Generated by Antigravity CLI",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
|
"@google/genai": "^1.35.0",
|
||||||
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
|
"@nestjs/cache-manager": "^3.1.0",
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/platform-socket.io": "^11.1.11",
|
||||||
|
"@nestjs/serve-static": "^5.0.4",
|
||||||
|
"@nestjs/swagger": "^11.2.4",
|
||||||
|
"@nestjs/terminus": "^11.0.0",
|
||||||
|
"@nestjs/throttler": "^6.5.0",
|
||||||
|
"@prisma/client": "^5.22.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"bullmq": "^5.66.4",
|
||||||
|
"cache-manager": "^7.2.7",
|
||||||
|
"cache-manager-redis-yet": "^5.1.5",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.3",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"ioredis": "^5.9.0",
|
||||||
|
"nestjs-i18n": "^10.6.0",
|
||||||
|
"nestjs-pino": "^4.5.0",
|
||||||
|
"nodemailer": "^7.0.12",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"pino": "^10.1.0",
|
||||||
|
"pino-http": "^11.0.0",
|
||||||
|
"prisma": "^5.22.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/multer": "^2.1.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/nodemailer": "^7.0.4",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
uploads/1773067175411-781981796.png
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"name": "bbb",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Generated by Antigravity CLI",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
|
"@google/genai": "^1.35.0",
|
||||||
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
|
"@nestjs/cache-manager": "^3.1.0",
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/platform-socket.io": "^11.1.11",
|
||||||
|
"@nestjs/serve-static": "^5.0.4",
|
||||||
|
"@nestjs/swagger": "^11.2.4",
|
||||||
|
"@nestjs/terminus": "^11.0.0",
|
||||||
|
"@nestjs/throttler": "^6.5.0",
|
||||||
|
"@prisma/client": "^5.22.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"bullmq": "^5.66.4",
|
||||||
|
"cache-manager": "^7.2.7",
|
||||||
|
"cache-manager-redis-yet": "^5.1.5",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.3",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"ioredis": "^5.9.0",
|
||||||
|
"nestjs-i18n": "^10.6.0",
|
||||||
|
"nestjs-pino": "^4.5.0",
|
||||||
|
"nodemailer": "^7.0.12",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"pino": "^10.1.0",
|
||||||
|
"pino-http": "^11.0.0",
|
||||||
|
"prisma": "^5.22.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/multer": "^2.1.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/nodemailer": "^7.0.4",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
uploads/1773067234394-305603703.jpg
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"name": "bbb",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Generated by Antigravity CLI",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
|
"@google/genai": "^1.35.0",
|
||||||
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
|
"@nestjs/cache-manager": "^3.1.0",
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/platform-socket.io": "^11.1.11",
|
||||||
|
"@nestjs/serve-static": "^5.0.4",
|
||||||
|
"@nestjs/swagger": "^11.2.4",
|
||||||
|
"@nestjs/terminus": "^11.0.0",
|
||||||
|
"@nestjs/throttler": "^6.5.0",
|
||||||
|
"@prisma/client": "^5.22.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"bullmq": "^5.66.4",
|
||||||
|
"cache-manager": "^7.2.7",
|
||||||
|
"cache-manager-redis-yet": "^5.1.5",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.3",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"ioredis": "^5.9.0",
|
||||||
|
"nestjs-i18n": "^10.6.0",
|
||||||
|
"nestjs-pino": "^4.5.0",
|
||||||
|
"nodemailer": "^7.0.12",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"pino": "^10.1.0",
|
||||||
|
"pino-http": "^11.0.0",
|
||||||
|
"prisma": "^5.22.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/multer": "^2.1.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/nodemailer": "^7.0.4",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
uploads/1773071218522-54137057.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/1773071231136-538057302.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
uploads/1773098189353-240031221.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/1773098196690-706527504.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/1773658627453-6202062.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
uploads/1773658652801-376000152.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
uploads/1773658935134-49699715.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
uploads/1773659962590-143124373.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
uploads/1773660004362-141903758.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/1773660025672-531339232.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
uploads/1773660176624-509253881.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
uploads/1773660217332-38983824.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
uploads/1773660242228-140851460.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
uploads/1773660275096-653459971.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
uploads/1773660472620-139173940.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
uploads/1773660536048-909194758.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
uploads/1773660645009-272744007.png
Normal file
|
After Width: | Height: | Size: 30 KiB |