AI Implementation feature(869): Admin Area Blog and Blog Page (#58)
This commit was merged in pull request #58.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from '../../common/guards/admin.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe';
|
||||
import {
|
||||
AdminBlogPost,
|
||||
CreatePostPayload,
|
||||
CreatePostSchema,
|
||||
PostIdParamSchema,
|
||||
UpdatePostPayload,
|
||||
UpdatePostSchema,
|
||||
} from './dto/blog.dto';
|
||||
import { AdminBlogService } from './admin-blog.service';
|
||||
|
||||
@ApiTags('admin')
|
||||
@ApiBearerAuth()
|
||||
@Controller('api/v1/admin/blog/posts')
|
||||
@UseGuards(AdminGuard)
|
||||
@Roles('admin')
|
||||
export class AdminBlogController {
|
||||
constructor(private readonly admin: AdminBlogService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List all blog posts (admin only, drafts included)' })
|
||||
async list(): Promise<AdminBlogPost[]> {
|
||||
return this.admin.listAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a single blog post by id (admin only)' })
|
||||
async getOne(
|
||||
@Param(new ZodValidationPipe(PostIdParamSchema)) params: { id: string },
|
||||
): Promise<AdminBlogPost> {
|
||||
return this.admin.getById(params.id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new blog post (admin only)' })
|
||||
async create(
|
||||
@Body(new ZodValidationPipe(CreatePostSchema)) body: CreatePostPayload,
|
||||
): Promise<AdminBlogPost> {
|
||||
return this.admin.create(body);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Update a blog post (admin only)' })
|
||||
async update(
|
||||
@Param(new ZodValidationPipe(PostIdParamSchema)) params: { id: string },
|
||||
@Body(new ZodValidationPipe(UpdatePostSchema)) body: UpdatePostPayload,
|
||||
): Promise<AdminBlogPost> {
|
||||
return this.admin.update(params.id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
@ApiOperation({ summary: 'Delete a blog post (admin only)' })
|
||||
async remove(
|
||||
@Param(new ZodValidationPipe(PostIdParamSchema)) params: { id: string },
|
||||
): Promise<void> {
|
||||
await this.admin.remove(params.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { BlogPostEntity } from '../../database/entities/blog-post.entity';
|
||||
import { ApiError } from '../../common/errors/api-error';
|
||||
import { AdminBlogPost, CreatePostPayload, UpdatePostPayload } from './dto/blog.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminBlogService {
|
||||
constructor(
|
||||
@InjectRepository(BlogPostEntity)
|
||||
private readonly posts: Repository<BlogPostEntity>,
|
||||
) {}
|
||||
|
||||
async listAll(): Promise<AdminBlogPost[]> {
|
||||
const rows = await this.posts.find({ order: { updatedAt: 'DESC' } });
|
||||
return rows.map((r) => this.toView(r));
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<AdminBlogPost> {
|
||||
const row = await this.posts.findOne({ where: { id } });
|
||||
if (!row) throw ApiError.notFound('Post not found');
|
||||
return this.toView(row);
|
||||
}
|
||||
|
||||
async create(payload: CreatePostPayload): Promise<AdminBlogPost> {
|
||||
const status = payload.status ?? 'draft';
|
||||
const row = this.posts.create({
|
||||
id: uuid(),
|
||||
title: payload.title.trim(),
|
||||
bodyMd: payload.bodyMd ?? '',
|
||||
status,
|
||||
publishedAt: status === 'published' ? new Date().toISOString() : null,
|
||||
});
|
||||
await this.posts.save(row);
|
||||
return this.toView(row);
|
||||
}
|
||||
|
||||
async update(id: string, payload: UpdatePostPayload): Promise<AdminBlogPost> {
|
||||
const row = await this.posts.findOne({ where: { id } });
|
||||
if (!row) throw ApiError.notFound('Post not found');
|
||||
|
||||
if (payload.title !== undefined) row.title = payload.title.trim();
|
||||
if (payload.bodyMd !== undefined) row.bodyMd = payload.bodyMd;
|
||||
|
||||
if (payload.status !== undefined) {
|
||||
if (payload.status === 'published') {
|
||||
row.status = 'published';
|
||||
// Only stamp publishedAt if this post has never been published before;
|
||||
// preserve the original publishedAt for re-publications.
|
||||
if (!row.publishedAt) {
|
||||
row.publishedAt = new Date().toISOString();
|
||||
}
|
||||
} else if (payload.status === 'draft') {
|
||||
row.status = 'draft';
|
||||
// preserve publishedAt so a later re-publish keeps the original publication timestamp.
|
||||
}
|
||||
}
|
||||
|
||||
await this.posts.save(row);
|
||||
return this.toView(row);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const row = await this.posts.findOne({ where: { id } });
|
||||
if (!row) throw ApiError.notFound('Post not found');
|
||||
await this.posts.delete({ id });
|
||||
}
|
||||
|
||||
private toView(r: BlogPostEntity): AdminBlogPost {
|
||||
return {
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
bodyMd: r.bodyMd ?? '',
|
||||
status: r.status,
|
||||
createdAt: r.createdAt,
|
||||
updatedAt: r.updatedAt,
|
||||
publishedAt: r.publishedAt ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BlogPostEntity } from '../../database/entities/blog-post.entity';
|
||||
import { BlogController } from './blog.controller';
|
||||
import { BlogService } from './blog.service';
|
||||
import { AdminBlogController } from './admin-blog.controller';
|
||||
import { AdminBlogService } from './admin-blog.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([BlogPostEntity])],
|
||||
controllers: [BlogController],
|
||||
providers: [BlogService],
|
||||
controllers: [BlogController, AdminBlogController],
|
||||
providers: [BlogService, AdminBlogService],
|
||||
})
|
||||
export class BlogModule {}
|
||||
export class BlogModule {}
|
||||
|
||||
@@ -11,4 +11,40 @@ export type PublicBlogPost = z.infer<typeof PublicBlogPostSchema>;
|
||||
export const PublicBlogListSchema = z.object({
|
||||
posts: z.array(PublicBlogPostSchema),
|
||||
});
|
||||
export type PublicBlogList = z.infer<typeof PublicBlogListSchema>;
|
||||
export type PublicBlogList = z.infer<typeof PublicBlogListSchema>;
|
||||
|
||||
export const AdminBlogPostSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
bodyMd: z.string(),
|
||||
status: z.enum(['draft', 'published']),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
publishedAt: z.string().nullable(),
|
||||
});
|
||||
export type AdminBlogPost = z.infer<typeof AdminBlogPostSchema>;
|
||||
|
||||
export const CreatePostSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(1, 'Title is required')
|
||||
.max(200, 'Title must be at most 200 characters')
|
||||
.refine((t) => t.trim().length > 0, { message: 'Title is required' }),
|
||||
bodyMd: z.string().max(200_000, 'Body must be at most 200000 characters').default(''),
|
||||
status: z.enum(['draft', 'published']).optional(),
|
||||
});
|
||||
export type CreatePostPayload = z.infer<typeof CreatePostSchema>;
|
||||
|
||||
export const UpdatePostSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(1, 'Title is required')
|
||||
.max(200, 'Title must be at most 200 characters')
|
||||
.refine((t) => t.trim().length > 0, { message: 'Title is required' })
|
||||
.optional(),
|
||||
bodyMd: z.string().max(200_000, 'Body must be at most 200000 characters').optional(),
|
||||
status: z.enum(['draft', 'published']).optional(),
|
||||
});
|
||||
export type UpdatePostPayload = z.infer<typeof UpdatePostSchema>;
|
||||
|
||||
export const PostIdParamSchema = z.object({ id: z.string().min(1).max(64) });
|
||||
|
||||
Reference in New Issue
Block a user