-
Notifications
You must be signed in to change notification settings - Fork 2
feat: REST API layer for web dashboard #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 12 commits
Commits
Show all changes
72 commits
Select commit
Hold shift + click to select a range
a1da2d3
feat: REST API layer for web dashboard
BillChirico 8aaa933
fix: use timing-safe comparison for API secret validation
BillChirico baed233
fix: use safeSend for mention sanitization and validate content length
BillChirico d57908d
fix: filter sensitive data from config endpoints
BillChirico 77e8c1f
fix: only send CORS preflight 204 when DASHBOARD_URL is configured
BillChirico 5b45bd1
fix: close orphaned server before starting a new one
BillChirico ba57163
fix: use cursor-based pagination for guild members endpoint
BillChirico 2b0e545
fix: hide detailed memory usage behind auth on health endpoint
BillChirico 021e1be
fix: prevent REST API startup failure from crashing the bot
BillChirico 0b9a7ba
fix: scope conversations stats query to guild channels
BillChirico 79de322
fix: restore fake timers in afterEach to prevent test pollution
BillChirico 5ba0753
fix: centralize vi.unstubAllEnvs in afterEach for server tests
BillChirico bbbcb47
fix: scope conversations stats query to guild channel IDs
BillChirico caf1e52
fix: remove redundant content length validation in sendMessage action
BillChirico ab16588
fix: return sent message content instead of echoing request body
BillChirico 83637d0
fix: add destroy method to rate limiter for interval cleanup
BillChirico 96a87bc
fix: remove DELETE from CORS allowed methods
BillChirico 58e7037
fix: move discord WebSocket details behind auth on health endpoint
BillChirico 4445a24
fix: centralize vi.unstubAllEnvs in afterEach for health tests
BillChirico 8685bcb
fix: add guild_id column to conversations table for guild-scoped queries
BillChirico f83b0ac
fix: harden server startup and shutdown in server.js
BillChirico 8300a21
fix: validate Discord message content length (2000 char limit)
BillChirico bcb1fcf
fix: avoid reflecting user input in action error response
BillChirico 319654a
fix: remove 'logging' from SAFE_CONFIG_KEYS in guilds route
BillChirico ee2e84c
fix: handle CORS preflight without DASHBOARD_URL
BillChirico 255145e
refactor: extract shared isValidSecret helper, use in health route
BillChirico a9dd4ae
refactor: use shared isValidSecret helper in health route
BillChirico 4aad93e
fix: guard against undefined req.body in actions endpoint
BillChirico 7a4d6be
fix: raise content length limit to 10000 chars in actions endpoint
BillChirico bbc780d
style: fix chain formatting in health tests
BillChirico ab8f6b4
refactor: add isValidSecret helper to auth middleware
BillChirico 118bdd9
fix: add ALTER TABLE migration for guild_id column in conversations
BillChirico 79851a1
fix: prevent moderation config writes via PATCH endpoint
BillChirico 501918f
fix: guard against undefined req.body in PATCH config endpoint
BillChirico d68f669
docs: note parsePagination is only used by moderation endpoint
BillChirico 86fc94c
fix: only send CORS preflight when DASHBOARD_URL is configured
BillChirico f821367
fix: destroy rate limiter on server shutdown
BillChirico 35d7ae9
Merge remote-tracking branch 'origin/feat/rest-api-batch-b' into feat…
BillChirico c542737
Merge branch 'main' into feat/rest-api
BillChirico e75ddcb
fix: remove DELETE from CORS Allow-Methods and set header on all resp…
BillChirico 134de65
fix: wrap ALTER TABLE guild_id backfill in try-catch for PG < 9.6
BillChirico 75c4665
fix: document NULL guild_id caveat on stats conversations query
BillChirico 24c3751
fix: extract content length limit to named constant
BillChirico b1a42d5
fix: sanitize mentions in API sendMessage before calling safeSend
BillChirico b9989e7
fix: clarify config endpoint is global scope, not per-guild
BillChirico 9b781bf
fix: reject single-segment config paths in PATCH endpoint
BillChirico 6a93df6
fix: destroy leaked rate limiter on repeated createApp calls
BillChirico 17f2048
chore: apply biome formatting fixes
BillChirico 42a64c6
fix: remove redundant sanitizeMentions in actions route
BillChirico dedb6f1
docs: update JSDoc return type to include destroy method
BillChirico c59722f
fix: cap channel list in guild info response
BillChirico 69d1e85
fix: guard guild_id index creation against missing column
BillChirico be78c27
docs: add API files to AGENTS.md Key Files table
BillChirico 661cf4b
fix: use explicit columns in moderation query
BillChirico 2435e64
test: assert capped limit forwarded to guild.members.list
BillChirico 1adb144
test: use vi.stubEnv instead of delete process.env
BillChirico 0d9a8a5
fix(api): pass through error status codes in global error handler
BillChirico 1fc69e7
fix(api): reset server variable on listen failure
BillChirico 58c889a
fix(api): use err.status only for valid 4xx codes in error handler
BillChirico 557b5f4
fix(api): use Buffer.byteLength for timingSafeEqual guard
BillChirico 3194071
fix(db): ensure guild_id index is always created
BillChirico e8af0b2
fix(api): move CORS middleware before body parser
BillChirico 6166a1f
fix(api): validate BOT_API_PORT range (0-65535)
BillChirico 20897d6
fix(api): use once() for server startup error listener
BillChirico ba19613
fix(api): add timeout and force-close to prevent server.close() hanging
BillChirico fafb83d
fix(api): add string type check for message content
BillChirico b7ded26
fix(api): set CORS allow-headers on all responses not just preflight
BillChirico f382875
fix(api): limit channel iteration instead of materializing full array
BillChirico baccd88
docs: update README to reflect new REST API server
BillChirico d12272b
fix(api): add trust proxy configuration for rate limiting behind reve…
BillChirico f41bf18
fix: address round 5 review comments
BillChirico 5255c94
fix: address round 6 review nitpicks
BillChirico File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| /** | ||
| * API Router Aggregation | ||
| * Mounts all v1 API route groups | ||
| */ | ||
|
|
||
| import { Router } from 'express'; | ||
| import { requireAuth } from './middleware/auth.js'; | ||
| import guildsRouter from './routes/guilds.js'; | ||
| import healthRouter from './routes/health.js'; | ||
|
|
||
| const router = Router(); | ||
|
|
||
| // Health check — public (no auth required) | ||
| router.use('/health', healthRouter); | ||
|
|
||
| // Guild routes — require API secret | ||
| router.use('/guilds', requireAuth(), guildsRouter); | ||
|
|
||
| export default router; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /** | ||
| * Authentication Middleware | ||
| * Validates requests using a shared API secret | ||
| */ | ||
|
|
||
| import crypto from 'node:crypto'; | ||
| import { warn } from '../../logger.js'; | ||
|
|
||
| /** | ||
| * Creates middleware that validates the x-api-secret header against BOT_API_SECRET. | ||
| * Returns 401 JSON error if the header is missing or does not match. | ||
| * | ||
| * @returns {import('express').RequestHandler} Express middleware function | ||
| */ | ||
| export function requireAuth() { | ||
| return (req, res, next) => { | ||
| const secret = req.headers['x-api-secret']; | ||
| const expected = process.env.BOT_API_SECRET; | ||
|
|
||
| if (!expected) { | ||
| warn('BOT_API_SECRET not configured — rejecting API request'); | ||
| return res.status(401).json({ error: 'API authentication not configured' }); | ||
| } | ||
|
|
||
| if ( | ||
| !secret || | ||
| secret.length !== expected.length || | ||
| !crypto.timingSafeEqual(Buffer.from(secret), Buffer.from(expected)) | ||
| ) { | ||
| warn('Unauthorized API request', { ip: req.ip, path: req.path }); | ||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return res.status(401).json({ error: 'Unauthorized' }); | ||
| } | ||
|
|
||
| next(); | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| /** | ||
| * Rate Limiting Middleware | ||
| * Simple in-memory per-IP rate limiter with no external dependencies | ||
| */ | ||
|
|
||
| /** | ||
| * Creates rate-limiting middleware that tracks requests per IP address. | ||
| * Returns 429 JSON error when the limit is exceeded. | ||
| * | ||
| * @param {Object} [options] - Rate limiter configuration | ||
| * @param {number} [options.windowMs=900000] - Time window in milliseconds (default: 15 minutes) | ||
| * @param {number} [options.max=100] - Maximum requests per window per IP (default: 100) | ||
| * @returns {import('express').RequestHandler} Express middleware function | ||
| */ | ||
| export function rateLimit({ windowMs = 15 * 60 * 1000, max = 100 } = {}) { | ||
| /** @type {Map<string, { count: number, resetAt: number }>} */ | ||
| const clients = new Map(); | ||
|
|
||
| // Periodically clean up expired entries to prevent memory leaks | ||
| const cleanup = setInterval(() => { | ||
| const now = Date.now(); | ||
| for (const [ip, entry] of clients) { | ||
| if (now >= entry.resetAt) { | ||
| clients.delete(ip); | ||
| } | ||
| } | ||
| }, windowMs); | ||
|
|
||
| // Allow the timer to not prevent process exit | ||
| cleanup.unref(); | ||
|
|
||
| return (req, res, next) => { | ||
| const ip = req.ip; | ||
| const now = Date.now(); | ||
|
|
||
| let entry = clients.get(ip); | ||
| if (!entry || now >= entry.resetAt) { | ||
| entry = { count: 0, resetAt: now + windowMs }; | ||
| clients.set(ip, entry); | ||
| } | ||
|
|
||
| entry.count++; | ||
|
|
||
| if (entry.count > max) { | ||
| const retryAfter = Math.ceil((entry.resetAt - now) / 1000); | ||
| res.set('Retry-After', String(retryAfter)); | ||
| return res.status(429).json({ error: 'Too many requests, please try again later' }); | ||
| } | ||
|
|
||
| next(); | ||
| }; | ||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.