feat(board): Lightning-paid message board with LNbits and admin moderation

Add public /board flow: create invoice, webhook + confirm reconciliation, list
active messages, likes (Nostr), zap fallbacks. Admin table for hide/delete.

Include LNbits webhook body normalization (double-encoded JSON), POST
/api/messages/confirm/:hash, and root npm db:push script. Prisma models for
pending invoices and board messages.

Made-with: Cursor
This commit is contained in:
bbe
2026-04-03 18:37:52 +02:00
parent 7acff1ae38
commit 586b572f73
14 changed files with 1257 additions and 5 deletions
+68
View File
@@ -0,0 +1,68 @@
import { Router, Request, Response } from 'express';
import { prisma } from '../db/prisma';
import { requireAuth, requireRole } from '../middleware/auth';
const router = Router();
router.use(requireAuth, requireRole(['ADMIN', 'MODERATOR']));
router.get('/', async (_req: Request, res: Response) => {
try {
const messages = await prisma.boardMessage.findMany({
orderBy: { createdAt: 'desc' },
});
res.json(messages);
} catch (err) {
console.error('Admin list board messages error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
/** Toggle active <-> hidden */
router.post('/:id/hide', async (req: Request, res: Response) => {
try {
const rawId = req.params.id;
const id = Array.isArray(rawId) ? rawId[0] : rawId;
const msg = await prisma.boardMessage.findUnique({ where: { id } });
if (!msg) {
res.status(404).json({ error: 'Not found' });
return;
}
if (msg.status === 'deleted') {
res.status(400).json({ error: 'Message is deleted' });
return;
}
const next = msg.status === 'hidden' ? 'active' : 'hidden';
const updated = await prisma.boardMessage.update({
where: { id: msg.id },
data: { status: next },
});
res.json(updated);
} catch (err) {
console.error('Admin hide board message error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
/** Soft-delete */
router.delete('/:id', async (req: Request, res: Response) => {
try {
const rawId = req.params.id;
const id = Array.isArray(rawId) ? rawId[0] : rawId;
const msg = await prisma.boardMessage.findUnique({ where: { id } });
if (!msg) {
res.status(404).json({ error: 'Not found' });
return;
}
const updated = await prisma.boardMessage.update({
where: { id: msg.id },
data: { status: 'deleted' },
});
res.json(updated);
} catch (err) {
console.error('Admin delete board message error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;