diff --git a/packages/api/src/routes/admin/security.ts b/packages/api/src/routes/admin/security.ts index dff7265..65bd614 100644 --- a/packages/api/src/routes/admin/security.ts +++ b/packages/api/src/routes/admin/security.ts @@ -359,6 +359,32 @@ export default async function adminSecurityRoutes(app: FastifyInstance) { } ); + // bulk update board security settings + app.put( + "/admin/boards/bulk-security", + { preHandler: [app.requireAdmin, app.requireRole("SUPER_ADMIN", "ADMIN")], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } }, + async (req, reply) => { + const body = z.object({ + boardIds: z.array(z.string().min(1)).min(1), + settings: boardSecurityBody, + }).parse(req.body); + + const data: Record = {}; + if (body.settings.sensitivityLevel !== undefined) data.sensitivityLevel = body.settings.sensitivityLevel; + if (body.settings.velocityThreshold !== undefined) data.velocityThreshold = body.settings.velocityThreshold; + if (body.settings.quarantined !== undefined) data.quarantined = body.settings.quarantined; + if (body.settings.requireVoteVerification !== undefined) data.requireVoteVerification = body.settings.requireVoteVerification; + + await prisma.board.updateMany({ + where: { id: { in: body.boardIds } }, + data, + }); + + req.log.info({ adminId: req.adminId, count: body.boardIds.length }, "bulk board security update"); + reply.send({ updated: body.boardIds.length }); + } + ); + // 10. Admin notification webhook config - CRUD app.get( "/admin/security/webhooks", diff --git a/packages/web/src/pages/admin/AdminSecurity.tsx b/packages/web/src/pages/admin/AdminSecurity.tsx index 9584068..9c91799 100644 --- a/packages/web/src/pages/admin/AdminSecurity.tsx +++ b/packages/web/src/pages/admin/AdminSecurity.tsx @@ -431,21 +431,149 @@ function AlertsTab() { {/* Board security settings */} {boards.length > 0 && ( -
-

api.get('/admin/boards').then(setBoards).catch(() => {})} /> + )} +

+ ) +} + +function BoardSecuritySection({ boards, onRefresh }: { boards: BoardSummary[]; onRefresh: () => void }) { + const toast = useToast() + const [selected, setSelected] = useState>(new Set()) + const [showBulk, setShowBulk] = useState(false) + const [bulkSensitivity, setBulkSensitivity] = useState('normal') + const [bulkThreshold, setBulkThreshold] = useState(0) + const [bulkQuarantine, setBulkQuarantine] = useState(false) + const [bulkVerification, setBulkVerification] = useState(false) + const [bulkSaving, setBulkSaving] = useState(false) + + const toggleSelect = (id: string) => { + setSelected((prev) => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } + + const selectAll = () => setSelected(new Set(boards.map((b) => b.id))) + const selectNone = () => setSelected(new Set()) + + const applyBulk = async () => { + if (selected.size === 0) return + setBulkSaving(true) + try { + await api.put('/admin/boards/bulk-security', { + boardIds: [...selected], + settings: { + sensitivityLevel: bulkSensitivity, + velocityThreshold: bulkThreshold || null, + quarantined: bulkQuarantine, + requireVoteVerification: bulkVerification, + }, + }) + toast.success(`Updated ${selected.size} board${selected.size > 1 ? 's' : ''}`) + selectNone() + setShowBulk(false) + onRefresh() + } catch { + toast.error('Failed to apply bulk settings') + } finally { + setBulkSaving(false) + } + } + + return ( +
+
+

+ Board security settings +

+
+ {selected.size > 0 && ( + + )} + +
+
+ + + + {/* Bulk settings panel */} + {showBulk && selected.size > 0 && ( +
+
+ Apply to {selected.size} selected board{selected.size > 1 ? 's' : ''} +
+
+
+ + +
+
+ + +
+ + + +
)} + +
+ {boards.map((board) => ( + toggleSelect(board.id)} + /> + ))} +
) } @@ -463,41 +591,73 @@ function SecurityExplainer() { localStorage.setItem(storageKey, String(next)) } + const items: { label: string; color: string; text: string }[] = [ + { + label: 'Sensitivity', + color: 'var(--accent)', + text: 'Controls how aggressively the system flags suspicious activity. Normal works for most boards. High increases proof-of-work difficulty for new or flagged users and tightens anomaly detection thresholds. Use High for boards where votes directly influence product decisions.', + }, + { + label: 'Velocity threshold', + color: 'var(--admin-accent)', + text: 'The maximum number of votes per hour considered normal. When votes exceed this, an alert is triggered. Set to 0 for automatic calculation based on the board\'s historical average. Override if your board has unusual traffic patterns.', + }, + { + label: 'Quarantine', + color: 'var(--error)', + text: 'Emergency lockdown during an active attack. All new posts need admin approval, vote counts are hidden, and identities less than an hour old can browse but not interact. Existing users are unaffected. Turn it off once the situation passes.', + }, + { + label: 'Vote verification', + color: 'var(--info)', + text: 'Extra proof-of-work difficulty for identities less than an hour old voting on this board. Makes it more expensive for brigaders to create throwaway accounts and vote in bulk, without adding friction for established users.', + }, + ] + return ( -
+
{open && ( -
-
- Sensitivity - controls how aggressively the system flags suspicious activity on this board. Normal is fine for most boards. High increases the proof-of-work difficulty for new or flagged users and tightens anomaly detection thresholds, making it harder for bots and brigaders to operate. Use High for boards where votes directly influence product decisions. -
-
- Velocity threshold - the maximum number of votes per hour that the system considers normal for this board. When votes exceed this number, it triggers an alert. Set to 0 to let the system calculate it automatically from the board's historical average. Override it if you know your board has unusual traffic patterns. -
-
- Quarantine - locks down the board during an active attack. When enabled, all new posts require admin approval before appearing publicly, vote counts are hidden, and new identities (created less than an hour ago) can browse but not vote, post, or comment. Existing users are unaffected. Turn it off once the situation is resolved. -
-
- Vote verification - adds extra proof-of-work difficulty for new identities (less than an hour old) voting on this board. This makes it more expensive for brigaders to create throwaway accounts and vote in bulk, without adding friction for established users. -
+
+ {items.map((item) => ( +
+
+ {item.label} +
+
+ {item.text} +
+
+ ))}
)}
) } -function BoardSecurityCard({ board }: { board: BoardSummary }) { +function BoardSecurityCard({ board, selected, onToggleSelect }: { board: BoardSummary; selected?: boolean; onToggleSelect?: () => void }) { const toast = useToast() const [sensitivity, setSensitivity] = useState(board.sensitivityLevel || 'normal') const [threshold, setThreshold] = useState(board.velocityThreshold ?? 0) @@ -527,9 +687,20 @@ function BoardSecurityCard({ board }: { board: BoardSummary }) { style={{ background: 'var(--surface)', border: '1px solid var(--border)' }} >
- - {board.name} - +
+ {onToggleSelect && ( + + )} + + {board.name} + +
{quarantined && (
-
+