fix: backend and discord-gateway improvements

- Update shared database schema
- Add shared utils
- Refactor backend middleware, auth routes, and dashboard repository
- Improve media analysis client with better error handling
- Fix searxng search URL construction
- Update URL fetcher for robustness

Co-authored-by: workflow agents
This commit is contained in:
asepharyana
2026-07-02 06:02:07 +07:00
co-authored by workflow agents
parent 81a004d250
commit ade5d6a7c3
9 changed files with 174 additions and 53 deletions
+11 -9
View File
@@ -19,6 +19,7 @@ import { createUiStateRouter } from "../modules/ui-state/ui-state.routes.js";
import { createGuildsRouter } from "../modules/voice/guilds.routes.js";
import { createVoiceRouter } from "../modules/voice/voice.routes.js";
import {
adminAuth,
errorHandler,
} from "../shared/middlewares/index.js";
import { config } from "../shared/config/index.js";
@@ -73,17 +74,18 @@ export function createHttpApp(): Express {
app.use("/api", createConfigRouter());
app.use("/api", createDashboardRouter());
// Protected routes — all routes are now public
app.use("/api", createMessagesRouter());
app.use("/api", createAnalysisRouter());
app.use("/api", createMascotChatRouter());
app.use("/api", createMediaRouter());
app.use("/api", createVoiceRouter());
app.use("/api", createRecordingsRouter());
app.use("/api", createUiStateRouter());
// Protected routes — require admin authentication (X-Admin-Password header)
const adminAuthMiddleware = adminAuth(ADMIN_PASSWORD);
app.use("/api", adminAuthMiddleware, createMessagesRouter());
app.use("/api", adminAuthMiddleware, createAnalysisRouter());
app.use("/api", adminAuthMiddleware, createMascotChatRouter());
app.use("/api", adminAuthMiddleware, createMediaRouter());
app.use("/api", adminAuthMiddleware, createVoiceRouter());
app.use("/api", adminAuthMiddleware, createRecordingsRouter());
app.use("/api", adminAuthMiddleware, createUiStateRouter());
// Guilds routes
app.use("/api/guilds", createGuildsRouter());
app.use("/api/guilds", adminAuthMiddleware, createGuildsRouter());
// 404 handler
app.use((_req: Request, res: Response) => {
@@ -3,18 +3,22 @@ import { createChildLogger } from "@bete/shared/logger";
import type { Request, Response, Router } from "express";
import express from "express";
import { config } from "../../shared/config/index.js";
import { asyncHandler } from "../../shared/middlewares/index.js";
import { asyncHandler, rateLimit } from "../../shared/middlewares/index.js";
const logger = createChildLogger("auth.routes");
const adminPassword = config.ADMIN_PASSWORD || "admin";
// Rate limit: max 10 login attempts per IP per 15 minutes
const loginRateLimit = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });
export function createAuthRouter(): Router {
const router = express.Router();
// POST /api/auth/login
router.post(
"/auth/login",
loginRateLimit,
asyncHandler(async (req: Request, res: Response) => {
const { password } = req.body as { password?: string };
@@ -43,9 +43,10 @@ export class DashboardRepository {
// Top channels by message count
const topChannels = await pool.query(`
SELECT channel_id,
(metadata::jsonb -> 'channel' ->> 'channelName') AS channel_name,
COALESCE(NULLIF((metadata::jsonb -> 'channel' ->> 'channelName'), ''), channel_id) AS channel_name,
COUNT(*)::int AS message_count
FROM messages
WHERE metadata IS NOT NULL AND metadata != ''
GROUP BY channel_id, (metadata::jsonb -> 'channel' ->> 'channelName')
ORDER BY COUNT(*) DESC
LIMIT 10
@@ -50,6 +50,48 @@ export function asyncHandler(
};
}
/**
* Simple in-memory rate limiter (no external dependency).
* Tracks request counts per IP within a rolling window.
* Use for auth endpoints to prevent brute-force attacks.
*/
export function rateLimit(opts: { windowMs: number; max: number }) {
const { windowMs, max } = opts;
const hits = new Map<string, { count: number; resetAt: number }>();
// Periodic cleanup of stale entries to prevent unbounded memory growth
const cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, value] of hits) {
if (now >= value.resetAt) hits.delete(key);
}
}, windowMs * 2);
cleanupInterval.unref();
return (req: Request, res: Response, next: NextFunction) => {
const ip = req.ip ?? req.socket.remoteAddress ?? "unknown";
const now = Date.now();
const entry = hits.get(ip);
if (!entry || now >= entry.resetAt) {
hits.set(ip, { count: 1, resetAt: now + windowMs });
next();
return;
}
entry.count++;
if (entry.count > max) {
res.status(429).json({
error: "TOO_MANY_REQUESTS",
message: `Rate limit exceeded. Try again in ${Math.ceil((entry.resetAt - now) / 1000)}s.`,
});
return;
}
next();
};
}
/**
* Validate that a value is a non-empty string, or throw a descriptive error.
* Use for both route params and query string values.