import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { prisma } from '@shieldai/db'; interface CreatePostBody { slug: string; title: string; excerpt?: string; content: string; authorName?: string; coverImageUrl?: string; tags?: string[]; published?: boolean; publishedAt?: string; } export async function blogAdminRoutes(fastify: FastifyInstance) { fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { const authReq = request as FastifyRequest & { user?: { id: string; role?: string } }; const user = authReq.user; if (!user) { return reply.code(401).send({ error: 'Unauthorized' }); } if (user.role !== 'support') { return reply.code(403).send({ error: 'Admin access required' }); } }); fastify.post('/admin/blog', async (request: FastifyRequest, reply: FastifyReply) => { const body = request.body as CreatePostBody; if (!body.slug || !/^[a-z0-9-]+$/.test(body.slug)) { return reply.code(400).send({ error: 'Invalid slug: must be lowercase alphanumeric with hyphens' }); } if (!body.title || body.title.length > 200) { return reply.code(400).send({ error: 'Title is required (max 200 chars)' }); } if (!body.content) { return reply.code(400).send({ error: 'Content is required' }); } const existing = await prisma.blogPost.findUnique({ where: { slug: body.slug }, }); if (existing) { return reply.code(409).send({ error: 'A post with this slug already exists' }); } const post = await prisma.blogPost.create({ data: { slug: body.slug, title: body.title, excerpt: body.excerpt || null, content: body.content, authorName: body.authorName || null, coverImageUrl: body.coverImageUrl || null, tags: body.tags || [], published: body.published || false, publishedAt: body.publishedAt ? new Date(body.publishedAt) : body.published ? new Date() : null, }, }); return reply.code(201).send({ post }); }); fastify.put('/admin/blog/:id', async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as { id: string }; const body = request.body as Partial; const existing = await prisma.blogPost.findUnique({ where: { id } }); if (!existing) { return reply.code(404).send({ error: 'Post not found' }); } if (body.slug && body.slug !== existing.slug) { const slugExists = await prisma.blogPost.findUnique({ where: { slug: body.slug } }); if (slugExists) { return reply.code(409).send({ error: 'A post with this slug already exists' }); } } const post = await prisma.blogPost.update({ where: { id }, data: { ...(body.slug !== undefined && { slug: body.slug }), ...(body.title !== undefined && { title: body.title }), ...(body.excerpt !== undefined && { excerpt: body.excerpt }), ...(body.content !== undefined && { content: body.content }), ...(body.authorName !== undefined && { authorName: body.authorName }), ...(body.coverImageUrl !== undefined && { coverImageUrl: body.coverImageUrl }), ...(body.tags !== undefined && { tags: body.tags }), ...(body.published !== undefined && { published: body.published }), publishedAt: body.publishedAt ? new Date(body.publishedAt) : body.published === true && !existing.published ? new Date() : undefined, }, }); return reply.send({ post }); }); fastify.delete('/admin/blog/:id', async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as { id: string }; await prisma.blogPost.delete({ where: { id } }); return reply.code(204).send(); }); fastify.get('/admin/blog', async (request: FastifyRequest, reply: FastifyReply) => { const query = request.query as { page?: string; limit?: string }; const page = Math.max(1, parseInt(query.page || '1', 10)); const limit = Math.min(50, Math.max(1, parseInt(query.limit || '20', 10))); const skip = (page - 1) * limit; const [posts, total] = await Promise.all([ prisma.blogPost.findMany({ orderBy: { createdAt: 'desc' }, skip, take: limit, }), prisma.blogPost.count(), ]); return reply.send({ posts, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }, }); }); }