import type { FastifyPluginAsync } from 'fastify'; import { z } from 'zod'; import { prisma } from '../db.js'; import { requireAuth } from '../plugins/session.js'; const submitWikiSchema = z.object({ articleId: z.string(), title: z.string().min(1).max(200), summary: z.string().min(1).max(500), content: z.string().min(1).max(500_000), }); const submitGlossarySchema = z.object({ term: z.string().min(1).max(100), definition: z.string().min(1).max(50_000), sameAs: z.array(z.string().url()).default([]), }); const reviewProposalSchema = z.object({ status: z.enum(['approved', 'rejected']), rejectionReason: z.string().optional(), }); export const wikiProposalRoutes: FastifyPluginAsync = async (app) => { app.addHook('preHandler', requireAuth()); // Submit a Wiki Edit Proposal app.post('/api/v1/wiki/proposals', async (req, reply) => { const parsed = submitWikiSchema.safeParse(req.body); if (!parsed.success) return reply.code(400).send({ error: 'invalid_input', details: parsed.error.issues }); const article = await prisma.article.findUnique({ where: { id: parsed.data.articleId } }); if (!article) return reply.code(404).send({ error: 'article_not_found' }); const proposal = await prisma.wikiProposal.create({ data: { articleId: article.id, title: parsed.data.title, summary: parsed.data.summary, content: parsed.data.content, suggestedById: req.session!.userId, }, }); return { success: true, proposal }; }); // Submit a Glossary Proposal app.post('/api/v1/glossary/proposals', async (req, reply) => { const parsed = submitGlossarySchema.safeParse(req.body); if (!parsed.success) return reply.code(400).send({ error: 'invalid_input', details: parsed.error.issues }); const proposal = await prisma.glossaryProposal.create({ data: { term: parsed.data.term, definition: parsed.data.definition, sameAs: parsed.data.sameAs, suggestedById: req.session!.userId, }, }); return { success: true, proposal }; }); // Admin routes for reviewing proposals app.register(async (adminApp) => { adminApp.addHook('preHandler', async (req, reply) => { if (req.session?.role !== 'admin' && req.session?.role !== 'editor') { return reply.code(403).send({ error: 'forbidden' }); } }); adminApp.get('/api/v1/admin/wiki/proposals', async (req) => { const proposals = await prisma.wikiProposal.findMany({ where: { status: 'pending' }, include: { article: { select: { title: true, slug: true } }, suggestedBy: { select: { name: true, username: true } } }, orderBy: { createdAt: 'desc' }, }); return { proposals }; }); adminApp.patch('/api/v1/admin/wiki/proposals/:id', async (req, reply) => { const { id } = req.params as { id: string }; const parsed = reviewProposalSchema.safeParse(req.body); if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' }); const proposal = await prisma.wikiProposal.findUnique({ where: { id } }); if (!proposal) return reply.code(404).send({ error: 'not_found' }); const updated = await prisma.wikiProposal.update({ where: { id }, data: { status: parsed.data.status, rejectionReason: parsed.data.rejectionReason, reviewedById: req.session!.userId, }, }); if (parsed.data.status === 'approved') { // Merge text and record in ArticleRevision await prisma.articleRevision.create({ data: { articleId: proposal.articleId, savedById: req.session!.userId, title: proposal.title, summary: proposal.summary, content: proposal.content, status: 'wiki_merge', }, }); await prisma.article.update({ where: { id: proposal.articleId }, data: { title: proposal.title, summary: proposal.summary, content: proposal.content, dateModified: new Date(), }, }); } return { success: true, proposal: updated }; }); }); };