6.0 KiB
dcordabuse
Zelf-gehoste, gemodereerde meldingsdatabase voor Discord-accounts, geïnspireerd op Dangercord maar met een ingebouwde moderatiequeue: reports worden pas publiek zichtbaar nadat een beheerder ze goedkeurt. Dit voorkomt dat het systeem zonder controle gebruikt kan worden voor ongefundeerde beschuldigingen, doxxing of stalking.
Stack
Next.js 16 (App Router) + TypeScript
PostgreSQL via Prisma
API-key auth (SHA-256 gehasht, twee rollen: SUBMITTER en ADMIN)
Database-based rate limiting (30 requests/minuut per key, aan te passen in src/lib/rateLimit.ts)
Lokale setup
cp .env.example .env
# vul DATABASE_URL in
npm install
npx prisma migrate dev --name init
npm run dev
Maak daarna je eerste keys aan:
npx tsx scripts/create-api-key.ts "Admin dashboard" ADMIN
npx tsx scripts/create-api-key.ts "Report-bot server X" SUBMITTER
De plaintext key wordt eenmalig getoond — bewaar hem meteen (bv. in een password manager of .env van de client die 'm gebruikt). In de database staat alleen de hash.
Draaien met Docker Compose
echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)" > .env
docker compose up -d --build
docker compose exec app npx prisma migrate deploy
docker compose exec app npx tsx scripts/create-api-key.ts "Admin dashboard" ADMIN
De app luistert intern op poort 3000. Onderstaande de config voor NGINX:
server {
listen 443 ssl;
listen [::]:443 ssl; # niet vergeten, zoals je eerder al vaker tegenkwam
server_name dangerguard.jouwdomein.nl;
ssl_certificate /etc/letsencrypt/live/jouwdomein.nl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jouwdomein.nl/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
De endpoint link is naar eigen wens aan te maken, maar zorg ervoor dat het matched met je keuze, dus ook in alle overige voorbeelden.
Let bij Cloudflare op dat WAF-regels de Authorization-header en PATCH/POST-requests naar /api/* niet blokkeren.
API
Alle endpoints verwachten Authorization: Bearer <key>.
GET /api/v1/users/:discordId
Rol: SUBMITTER of ADMIN. Geeft alleen goedgekeurde reports terug.
curl -H "Authorization: Bearer dg_live_xxx" \
https://dangerguard.jouwdomein.nl/api/v1/users/123456789012345678
POST /api/v1/reports
Rol: SUBMITTER of ADMIN. Dient een nieuwe report in — komt binnen als PENDING.
curl -X POST -H "Authorization: Bearer dg_live_xxx" -H "Content-Type: application/json" \
-d '{"discordId":"123456789012345678","category":"SCAM","description":"Stuurde phishing-links via DM naar meerdere serverleden."}' \
https://dangerguard.jouwdomein.nl/api/v1/reports
GET /api/admin/reports?status=PENDING
Rol: ADMIN. Lijst openstaande reports voor moderatie.
PATCH /api/admin/reports/:id
Rol: ADMIN. Keurt goed of af.
curl -X PATCH -H "Authorization: Bearer dg_live_admin" -H "Content-Type: application/json" \
-d '{"action":"approve"}' \
https://dangerguard.jouwdomein.nl/api/admin/reports/abc123
Frontend
/search — publieke zoekpagina (vraagt om een SUBMITTER/ADMIN key)
/admin — moderatiedashboard (vraagt om een ADMIN key)
Deze pagina's sturen de key vanuit de browser mee als Bearer-token; voor productiegebruik is het verstandiger hier een sessie/login-laag voor te zetten in plaats van de raw key te laten intypen, zeker voor het admin-dashboard.
Discord-integratie: notificatie + moderatie via buttons
Bij elke nieuwe report wordt (indien DISCORD_WEBHOOK_URL gezet is) een bericht gestuurd met een Approve/Reject-button. Klikken op een button verwerkt de moderatie direct in Discord — geen aparte bot-proces nodig, alleen een HTTPS endpoint dat Discord kan bereiken.
Setup:
Maak een Discord Application aan op de Discord applicatie portaal (of gebruik een bestaande).
Kopieer de Public Key (General Information) naar DISCORD_PUBLIC_KEY.
Zet de Interactions Endpoint URL op bijvoorbeeld https://dangerguard.jouwdomein.nl/api/discord/interactions (zorg ervoor dat het matched met wat je hebt gekozen).
Discord test deze meteen met een PING-request; die moet dus al live staan vóórdat je 'm opslaat.
Maak in het doelkanaal een webhook aan (Kanaal-instellingen > Integraties) en zet de URL in DISCORD_WEBHOOK_URL.
Zet de Role ID van je moderator-rol in DISCORD_ADMIN_ROLE_ID. Alleen leden met deze rol kunnen de buttons daadwerkelijk gebruiken; anderen krijgen een ephemeral "geen rechten"-melding.
Belangrijk: de rolcheck werkt alleen als de bot/applicatie ook daadwerkelijk in de server (guild) aanwezig is — anders stuurt Discord geen member.roles mee. Voeg de applicatie dus toe als bot met minimale permissies (alleen View Channels + Send Messages op het meldingskanaal is genoeg, want de buttons lopen via de Interactions endpoint, niet via bot-acties).
Uitbreidingsmogelijkheden die je zelf makkelijk kan toevoegen:
Een modal (InteractionResponseType.MODAL) bij "Afwijzen" om een eigen reden te typen in plaats van de generieke tekst.
Een slash command /report <discordId> <categorie> <omschrijving> als alternatief voor de HTTP API.
Per-server DISCORD_ADMIN_ROLE_ID als je dit ooit voor meerdere servers tegelijk wil draaien (nu is het één globale rol).
Waarom de moderatiequeue er is
Een open systeem waarin iedereen direct publiek zichtbare beschuldigingen over een account kan plaatsen, is een uitnodiging tot misbruik (valse reports, laster, doxxing). Door reports pas na goedkeuring zichtbaar te maken, houd je de nuttige functie (een gedeelde blocklist) zonder dat blackbox-beschuldigingen direct schade aanrichten. Overweeg daarnaast:
Alleen vertrouwde SUBMITTER-keys uitdelen (bv. aan bekende moderatiebots van servers).
evidenceUrl verplicht te maken voor bepaalde categorieën.
Een audit-log export (AuditLog-tabel is al aanwezig) periodiek te reviewen.