11 Commits

12 changed files with 343 additions and 15 deletions

View File

@@ -35,7 +35,8 @@ enum UserRole {
ADMIN ADMIN
HR HR
@@schema("usr") @@schema("usr")
} }
model Blog { model Blog {
@@ -151,7 +152,6 @@ model KlcArchive {
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
@@map("klc_archive") @@map("klc_archive")
@@schema("cnt") @@schema("cnt")
} }

View File

@@ -10,6 +10,7 @@ import { CaseStudiesModule } from './modules/case-studies/case-studies.module';
import { KlcArchiveModule } from './modules/klc-archive/klc-archive.module'; import { KlcArchiveModule } from './modules/klc-archive/klc-archive.module';
import { ReadingMaterialsModule } from './modules/reading-materials/reading-materials.module'; import { ReadingMaterialsModule } from './modules/reading-materials/reading-materials.module';
import { TrainingMaterialsModule } from './modules/training-materials/training-materials.module'; import { TrainingMaterialsModule } from './modules/training-materials/training-materials.module';
import { WebcastsModule } from './modules/webcast/webcasts.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
@@ -41,6 +42,7 @@ import { AppService } from './app.service';
KlcArchiveModule, KlcArchiveModule,
ReadingMaterialsModule, ReadingMaterialsModule,
TrainingMaterialsModule, TrainingMaterialsModule,
WebcastsModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@@ -4,6 +4,8 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import helmet from 'helmet'; import helmet from 'helmet';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { writeFileSync } from 'fs';
import { join } from 'path';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@@ -12,12 +14,20 @@ async function bootstrap() {
// Security // Security
app.use(helmet()); app.use(helmet());
app.enableCors({ app.enableCors({
origin: configService.get('CORS_ORIGIN', 'http://localhost:3000'), origin: [
'http://localhost:3000', // local Swagger UI
'http://localhost:3006', // local frontend url
'https://editor.swagger.io', // Swagger Editor online
'https://klc-admin.wdiprojects.com',
'https://admin-uat.klc.betadelivery.com'
],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
credentials: true, credentials: true,
}); });
// Global prefix // Global prefix
app.setGlobalPrefix(configService.get('API_PREFIX', 'api/v1')); const globalPrefix = configService.get('API_PREFIX', 'api/v1');
app.setGlobalPrefix(globalPrefix);
// Request logging middleware // Request logging middleware
app.use((req, res, next) => { app.use((req, res, next) => {
@@ -36,6 +46,7 @@ async function bootstrap() {
}, },
}), }),
); );
const port = configService.get('PORT', 3000);
// Swagger documentation (only in development) // Swagger documentation (only in development)
if (configService.get('NODE_ENV') !== 'production') { if (configService.get('NODE_ENV') !== 'production') {
@@ -44,13 +55,18 @@ async function bootstrap() {
.setDescription('Professional NestJS serverless application with user CRUD and authentication') .setDescription('Professional NestJS serverless application with user CRUD and authentication')
.setVersion(configService.get('API_VERSION', '1.0.0')) .setVersion(configService.get('API_VERSION', '1.0.0'))
.addBearerAuth() .addBearerAuth()
.addServer(`${process.env.SERVER_URL}/`, 'Local Server')
.build(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document); SwaggerModule.setup('api/docs', app, document);
const outputPath = join(process.cwd(), 'swagger.json');
writeFileSync(outputPath, JSON.stringify(document, null, 2), 'utf8');
console.log(`✅ Swagger JSON file generated at: ${outputPath}`);
} }
const port = configService.get('PORT', 3000); // const port = configService.get('PORT', 3000);
await app.listen(port); await app.listen(port);
console.log(`🚀 Application is running on: http://localhost:${port}`); console.log(`🚀 Application is running on: http://localhost:${port}`);

View File

@@ -81,13 +81,13 @@ export class BlogsController {
} }
@Patch(':id') @Patch(':id')
@UseGuards(JwtAuthGuard, RolesGuard) // @UseGuards(JwtAuthGuard, RolesGuard)
@Roles('ADMIN', 'HR') // @Roles('ADMIN', 'HR')
@ApiBearerAuth() // @ApiBearerAuth()
@ApiOperation({ summary: 'Update blog post' }) @ApiOperation({ summary: 'Update blog post' })
@ApiResponse({ status: 200, description: 'Blog updated successfully', type: BlogResponseDto }) @ApiResponse({ status: 200, description: 'Blog updated successfully', type: BlogResponseDto })
@ApiResponse({ status: 404, description: 'Blog not found' }) @ApiResponse({ status: 404, description: 'Blog not found' })
@ApiResponse({ status: 401, description: 'Unauthorized' }) // @ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' }) @ApiResponse({ status: 403, description: 'Forbidden' })
async update( async update(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@@ -97,13 +97,13 @@ export class BlogsController {
} }
@Delete(':id') @Delete(':id')
@UseGuards(JwtAuthGuard, RolesGuard) // @UseGuards(JwtAuthGuard, RolesGuard)
@Roles('ADMIN', 'HR') // @Roles('ADMIN', 'HR')
@ApiBearerAuth() // @ApiBearerAuth()
@ApiOperation({ summary: 'Delete blog post (soft delete)' }) @ApiOperation({ summary: 'Delete blog post (soft delete)' })
@ApiResponse({ status: 200, description: 'Blog deleted successfully' }) @ApiResponse({ status: 200, description: 'Blog deleted successfully' })
@ApiResponse({ status: 404, description: 'Blog not found' }) @ApiResponse({ status: 404, description: 'Blog not found' })
@ApiResponse({ status: 401, description: 'Unauthorized' }) // @ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' }) @ApiResponse({ status: 403, description: 'Forbidden' })
async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> { async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> {
await this.blogsService.remove(id); await this.blogsService.remove(id);

View File

@@ -44,5 +44,5 @@ export class CreateBlogDto {
@ApiPropertyOptional({ description: 'Publication date' }) @ApiPropertyOptional({ description: 'Publication date' })
@IsDateString() @IsDateString()
@IsOptional() @IsOptional()
publishedAt?: Date; publishedAt?: string;
} }

View File

@@ -15,6 +15,7 @@ export class BlogsService {
data: { data: {
...createBlogDto, ...createBlogDto,
tags: createBlogDto.tags || [], tags: createBlogDto.tags || [],
publishedAt: createBlogDto.publishedAt ? new Date(createBlogDto.publishedAt) : null
}, },
}); });

View File

@@ -0,0 +1,94 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
ParseIntPipe,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { WebcastsService } from '../services/webcasts.service';
import { CreateWebcastDto } from '../dto/create-webcast.dto';
import { UpdateWebcastDto } from '../dto/update-webcast.dto';
import { WebcastResponseDto } from '../dto/webcast-response.dto';
import { PaginationDto } from '../../../common/dto/pagination.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../../common/guards/roles.guard';
import { Roles } from '../../../common/decorators/roles.decorator';
@ApiTags('webcasts')
@Controller('webcasts')
export class WebcastsController {
constructor(private readonly webcastsService: WebcastsService) {}
@Post()
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('ADMIN', 'HR')
// @ApiBearerAuth()
@ApiOperation({ summary: 'Create a new webcast' })
@ApiResponse({ status: 201, description: 'Webcast created successfully', type: WebcastResponseDto })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' })
async create(@Body() createWebcastDto: CreateWebcastDto): Promise<WebcastResponseDto> {
return this.webcastsService.create(createWebcastDto);
}
@Get()
@ApiOperation({ summary: 'Get all webcasts with pagination' })
@ApiResponse({ status: 200, description: 'Webcasts retrieved successfully' })
async findAll(@Query() paginationDto: PaginationDto) {
return this.webcastsService.findAll(paginationDto);
}
@Get('tag/:tag')
@ApiOperation({ summary: 'Get webcasts by tag' })
@ApiResponse({ status: 200, description: 'Webcasts retrieved successfully' })
async findByTag(
@Param('tag') tag: string,
@Query() paginationDto: PaginationDto,
) {
return this.webcastsService.findByTag(tag, paginationDto);
}
@Get(':id')
@ApiOperation({ summary: 'Get webcast by ID' })
@ApiResponse({ status: 200, description: 'Webcast retrieved successfully', type: WebcastResponseDto })
@ApiResponse({ status: 404, description: 'Webcast not found' })
async findOne(@Param('id', ParseIntPipe) id: number): Promise<WebcastResponseDto> {
return this.webcastsService.findOne(id);
}
@Patch(':id')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('ADMIN', 'HR')
// @ApiBearerAuth()
@ApiOperation({ summary: 'Update webcast' })
@ApiResponse({ status: 200, description: 'Webcast updated successfully', type: WebcastResponseDto })
@ApiResponse({ status: 404, description: 'Webcast not found' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateWebcastDto: UpdateWebcastDto,
): Promise<WebcastResponseDto> {
return this.webcastsService.update(id, updateWebcastDto);
}
@Delete(':id')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('ADMIN', 'HR')
// @ApiBearerAuth()
@ApiOperation({ summary: 'Delete webcast (soft delete)' })
@ApiResponse({ status: 200, description: 'Webcast deleted successfully' })
@ApiResponse({ status: 404, description: 'Webcast not found' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' })
async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> {
await this.webcastsService.remove(id);
return { message: 'Webcast deleted successfully' };
}
}

View File

@@ -0,0 +1,22 @@
import { IsString, IsOptional, IsArray } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateWebcastDto {
@ApiProperty({ description: 'Webcast title' })
@IsString()
title: string;
@ApiProperty({ description: 'Webcast description' })
@IsString()
description: string;
@ApiProperty({ description: 'Webcast file URL' })
@IsString()
fileUrl: string;
@ApiPropertyOptional({ description: 'Webcast tags', type: [String] })
@IsArray()
@IsString({ each: true })
@IsOptional()
tags?: string[];
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateWebcastDto } from './create-webcast.dto';
export class UpdateWebcastDto extends PartialType(CreateWebcastDto) {}

View File

@@ -0,0 +1,24 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class WebcastResponseDto {
@ApiProperty({ description: 'Webcast ID' })
id: number;
@ApiProperty({ description: 'Webcast title' })
title: string;
@ApiProperty({ description: 'Webcast description' })
description: string;
@ApiProperty({ description: 'Webcast file URL' })
fileUrl: string;
@ApiProperty({ description: 'Webcast tags', type: [String] })
tags: string[];
@ApiPropertyOptional({ description: 'Creation date' })
createdAt: Date;
@ApiPropertyOptional({ description: 'Last update date' })
updatedAt: Date;
}

View File

@@ -0,0 +1,153 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../../common/database/prisma.service';
import { CreateWebcastDto } from '../dto/create-webcast.dto';
import { UpdateWebcastDto } from '../dto/update-webcast.dto';
import { WebcastResponseDto } from '../dto/webcast-response.dto';
import { PaginationDto } from '../../../common/dto/pagination.dto';
import { paginate } from '../../../common/utils/pagination.util';
@Injectable()
export class WebcastsService {
constructor(private readonly prisma: PrismaService) {}
async create(createWebcastDto: CreateWebcastDto): Promise<WebcastResponseDto> {
const webcast = await this.prisma.webcast.create({
data: {
...createWebcastDto,
tags: createWebcastDto.tags || [],
},
});
return this.mapToResponseDto(webcast);
}
async findAll(paginationDto: PaginationDto) {
const { page, limit } = paginationDto;
const skip = (page - 1) * limit;
const [webcasts, total] = await Promise.all([
this.prisma.webcast.findMany({
where: {
deletedAt: null,
},
orderBy: {
createdAt: 'desc',
},
skip,
take: limit,
}),
this.prisma.webcast.count({
where: {
deletedAt: null,
},
}),
]);
const mappedWebcasts = webcasts.map(webcast => this.mapToResponseDto(webcast));
return paginate(mappedWebcasts, { page, limit }, total);
}
async findOne(id: number): Promise<WebcastResponseDto> {
const webcast = await this.prisma.webcast.findFirst({
where: {
id,
deletedAt: null,
},
});
if (!webcast) {
throw new NotFoundException(`Webcast with ID ${id} not found`);
}
return this.mapToResponseDto(webcast);
}
async update(id: number, updateWebcastDto: UpdateWebcastDto): Promise<WebcastResponseDto> {
const existingWebcast = await this.prisma.webcast.findFirst({
where: {
id,
deletedAt: null,
},
});
if (!existingWebcast) {
throw new NotFoundException(`Webcast with ID ${id} not found`);
}
const webcast = await this.prisma.webcast.update({
where: { id },
data: {
...updateWebcastDto,
updatedAt: new Date(),
},
});
return this.mapToResponseDto(webcast);
}
async remove(id: number): Promise<void> {
const existingWebcast = await this.prisma.webcast.findFirst({
where: {
id,
deletedAt: null,
},
});
if (!existingWebcast) {
throw new NotFoundException(`Webcast with ID ${id} not found`);
}
await this.prisma.webcast.update({
where: { id },
data: {
deletedAt: new Date(),
},
});
}
async findByTag(tag: string, paginationDto: PaginationDto) {
const { page, limit } = paginationDto;
const skip = (page - 1) * limit;
const [webcasts, total] = await Promise.all([
this.prisma.webcast.findMany({
where: {
tags: {
has: tag,
},
deletedAt: null,
},
orderBy: {
createdAt: 'desc',
},
skip,
take: limit,
}),
this.prisma.webcast.count({
where: {
tags: {
has: tag,
},
deletedAt: null,
},
}),
]);
const mappedWebcasts = webcasts.map(webcast => this.mapToResponseDto(webcast));
return paginate(mappedWebcasts, { page, limit }, total);
}
private mapToResponseDto(webcast: any): WebcastResponseDto {
return {
id: webcast.id,
title: webcast.title,
description: webcast.description,
fileUrl: webcast.fileUrl,
tags: webcast.tags,
createdAt: webcast.createdAt,
updatedAt: webcast.updatedAt,
};
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WebcastsService } from './services/webcasts.service';
import { WebcastsController } from './controllers/webcasts.controller';
import { PrismaModule } from '../../common/database/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [WebcastsController],
providers: [WebcastsService],
exports: [WebcastsService],
})
export class WebcastsModule {}