Files
BelgianBitcoinEmbassy/backend/src/api/submissions.ts
T
bbeandCursor 99380ef6aa fix: auto-publish approved blog submissions on approval
Approved user submissions now import into the blog automatically, with a
backfill script for existing approvals and a WebSocket polyfill so backend
Nostr relay queries work on Node 20.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 23:38:48 +02:00

132 lines
3.6 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { prisma } from '../db/prisma';
import { requireAuth, requires } from '../middleware/auth';
import { importPostFromNostr, resolveSubmissionImport } from '../services/postImport';
const router = Router();
router.post(
'/',
requireAuth,
async (req: Request, res: Response) => {
try {
const { eventId, naddr, title } = req.body;
if (!title || (!eventId && !naddr)) {
res.status(400).json({ error: 'title and either eventId or naddr are required' });
return;
}
const submission = await prisma.submission.create({
data: {
eventId: eventId || null,
naddr: naddr || null,
title,
authorPubkey: req.user!.pubkey,
},
});
res.status(201).json(submission);
} catch (err) {
console.error('Create submission error:', err);
res.status(500).json({ error: 'Internal server error' });
}
}
);
router.get(
'/mine',
requireAuth,
async (req: Request, res: Response) => {
try {
const submissions = await prisma.submission.findMany({
where: { authorPubkey: req.user!.pubkey },
orderBy: { createdAt: 'desc' },
});
res.json(submissions);
} catch (err) {
console.error('List own submissions error:', err);
res.status(500).json({ error: 'Internal server error' });
}
}
);
router.get(
'/',
requireAuth,
requires('submissions.review'),
async (req: Request, res: Response) => {
try {
const status = req.query.status as string | undefined;
const where: any = {};
if (status) where.status = status;
const submissions = await prisma.submission.findMany({
where,
orderBy: { createdAt: 'desc' },
});
res.json(submissions);
} catch (err) {
console.error('List submissions error:', err);
res.status(500).json({ error: 'Internal server error' });
}
}
);
router.patch(
'/:id',
requireAuth,
requires('submissions.review'),
async (req: Request, res: Response) => {
try {
const { status, reviewNote } = req.body;
if (!status || !['APPROVED', 'REJECTED'].includes(status)) {
res.status(400).json({ error: 'status must be APPROVED or REJECTED' });
return;
}
const submission = await prisma.submission.findUnique({
where: { id: req.params.id as string },
});
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
return;
}
// Approval publishes the referenced Nostr article to the blog. Do this
// before flipping the status so a failed import doesn't leave a submission
// marked APPROVED without a corresponding live post.
let post = null;
if (status === 'APPROVED') {
const importInput = await resolveSubmissionImport(submission);
if (!importInput) {
res.status(422).json({
error:
'Could not resolve the submitted Nostr event from relays. The post was not published, so the submission was left unchanged.',
});
return;
}
post = await importPostFromNostr(importInput);
}
const updated = await prisma.submission.update({
where: { id: req.params.id as string },
data: {
status,
reviewedBy: req.user!.pubkey,
reviewNote: reviewNote || null,
},
});
res.json({ submission: updated, post });
} catch (err) {
console.error('Review submission error:', err);
res.status(500).json({ error: 'Internal server error' });
}
}
);
export default router;