132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Param,
|
|
Query,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Res,
|
|
NotFoundException,
|
|
Inject,
|
|
Logger,
|
|
StreamableFile,
|
|
} from '@nestjs/common';
|
|
import { Response } from 'express';
|
|
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
|
import { Public } from '../decorators/public.decorator';
|
|
import { BlogService } from '../services/blog.service';
|
|
import { BlogPost } from '@domain/entities/blog-post.entity';
|
|
import { BlogPostResponseDto, BlogPostListResponseDto } from '../dto/blog-post.dto';
|
|
import type { BlogPostCategory } from '@domain/entities/blog-post.entity';
|
|
import { StoragePort, STORAGE_PORT } from '@domain/ports/out/storage.port';
|
|
|
|
const BLOG_IMAGES_BUCKET = 'xpeditis-blog';
|
|
|
|
@ApiTags('Blog')
|
|
@Controller('blog')
|
|
@Public()
|
|
export class BlogController {
|
|
private readonly logger = new Logger(BlogController.name);
|
|
|
|
constructor(
|
|
private readonly blogService: BlogService,
|
|
@Inject(STORAGE_PORT) private readonly storage: StoragePort
|
|
) {}
|
|
|
|
@Get()
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({ summary: 'List published blog posts' })
|
|
@ApiQuery({
|
|
name: 'category',
|
|
required: false,
|
|
enum: ['industry', 'technology', 'guides', 'news'],
|
|
})
|
|
@ApiQuery({ name: 'search', required: false })
|
|
@ApiQuery({ name: 'limit', required: false, type: Number })
|
|
@ApiQuery({ name: 'offset', required: false, type: Number })
|
|
@ApiResponse({ status: 200, type: BlogPostListResponseDto })
|
|
async listPosts(
|
|
@Query('category') category?: BlogPostCategory,
|
|
@Query('search') search?: string,
|
|
@Query('limit') limit = 20,
|
|
@Query('offset') offset = 0
|
|
): Promise<BlogPostListResponseDto> {
|
|
const { posts, total } = await this.blogService.listPublishedPosts({
|
|
category,
|
|
search,
|
|
limit: Number(limit),
|
|
offset: Number(offset),
|
|
});
|
|
|
|
return {
|
|
posts: posts.map(this.mapToDto),
|
|
total,
|
|
limit: Number(limit),
|
|
offset: Number(offset),
|
|
};
|
|
}
|
|
|
|
@Get('images/:filename')
|
|
@ApiOperation({ summary: 'Serve a blog image from storage' })
|
|
@ApiParam({ name: 'filename' })
|
|
async serveImage(
|
|
@Param('filename') filename: string,
|
|
@Res({ passthrough: true }) res: Response
|
|
): Promise<StreamableFile> {
|
|
const key = `blog-images/${filename}`;
|
|
|
|
let buffer: Buffer;
|
|
try {
|
|
buffer = await this.storage.download({ bucket: BLOG_IMAGES_BUCKET, key });
|
|
} catch (err: any) {
|
|
this.logger.error(`Failed to serve blog image "${key}": ${err?.message}`);
|
|
throw new NotFoundException(`Image not found: ${filename}`);
|
|
}
|
|
|
|
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
|
const contentTypeMap: Record<string, string> = {
|
|
jpg: 'image/jpeg',
|
|
jpeg: 'image/jpeg',
|
|
png: 'image/png',
|
|
webp: 'image/webp',
|
|
gif: 'image/gif',
|
|
svg: 'image/svg+xml',
|
|
};
|
|
const contentType = contentTypeMap[ext] ?? 'application/octet-stream';
|
|
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
return new StreamableFile(buffer);
|
|
}
|
|
|
|
@Get(':slug')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({ summary: 'Get a published blog post by slug' })
|
|
@ApiParam({ name: 'slug' })
|
|
@ApiResponse({ status: 200, type: BlogPostResponseDto })
|
|
async getPost(@Param('slug') slug: string): Promise<BlogPostResponseDto> {
|
|
const post = await this.blogService.getPublishedPostBySlug(slug);
|
|
return this.mapToDto(post);
|
|
}
|
|
|
|
private mapToDto(post: BlogPost): BlogPostResponseDto {
|
|
return {
|
|
id: post.id,
|
|
title: post.title,
|
|
slug: post.slug,
|
|
excerpt: post.excerpt,
|
|
content: post.content,
|
|
coverImageUrl: post.coverImageUrl,
|
|
category: post.category,
|
|
tags: post.tags,
|
|
authorName: post.authorName,
|
|
status: post.status,
|
|
isFeatured: post.isFeatured,
|
|
publishedAt: post.publishedAt,
|
|
createdAt: post.createdAt,
|
|
updatedAt: post.updatedAt,
|
|
};
|
|
}
|
|
}
|