feat: add admin authentication to media and voice routes for secure access
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Router } from "express";
|
||||
import type { NextFunction, Request, Response, Router } from "express";
|
||||
import express from "express";
|
||||
import { AppError } from "../errors";
|
||||
import type { MediaController } from "../media/mediaController";
|
||||
@@ -9,10 +9,28 @@ export type MediaRouteController = Pick<
|
||||
"getState" | "queue" | "skip" | "stop"
|
||||
>;
|
||||
|
||||
export function createMediaRoutes(controller: MediaRouteController): Router {
|
||||
const router = express.Router();
|
||||
export interface MediaRouteOptions {
|
||||
adminPassword?: string;
|
||||
}
|
||||
|
||||
router.get("/media/status", (_req, res, next) => {
|
||||
export function createMediaRoutes(
|
||||
controller: MediaRouteController,
|
||||
options: MediaRouteOptions = {},
|
||||
): Router {
|
||||
const router = express.Router();
|
||||
const { adminPassword } = options;
|
||||
|
||||
const adminAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!adminPassword) return next();
|
||||
const authHeader = req.headers["x-admin-password"];
|
||||
if (authHeader === adminPassword) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: "Unauthorized access to admin features" });
|
||||
}
|
||||
};
|
||||
|
||||
router.get("/media/status", (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(controller.getState());
|
||||
} catch (error) {
|
||||
@@ -20,7 +38,7 @@ export function createMediaRoutes(controller: MediaRouteController): Router {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/media/queue", async (req, res, next) => {
|
||||
router.post("/media/queue", adminAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { source, mode = "music" } = req.body as {
|
||||
source?: string;
|
||||
@@ -42,7 +60,7 @@ export function createMediaRoutes(controller: MediaRouteController): Router {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/media/skip", async (_req, res, next) => {
|
||||
router.post("/media/skip", adminAuth, async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await controller.skip());
|
||||
} catch (error) {
|
||||
@@ -50,7 +68,7 @@ export function createMediaRoutes(controller: MediaRouteController): Router {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/media/stop", async (_req, res, next) => {
|
||||
router.post("/media/stop", adminAuth, async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await controller.stop());
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Router } from "express";
|
||||
import type { NextFunction, Request, Response, Router } from "express";
|
||||
import express from "express";
|
||||
import { AppError } from "../errors";
|
||||
import { createChildLogger } from "../logger";
|
||||
@@ -12,6 +12,7 @@ export interface VoiceRouteOptions {
|
||||
voiceController: VoiceController;
|
||||
patchSharedUIState: (patch: Partial<SharedUIState>) => SharedUIState;
|
||||
broadcaster: ModerationBroadcaster;
|
||||
adminPassword?: string;
|
||||
}
|
||||
|
||||
export function createVoiceRoutes(
|
||||
@@ -25,6 +26,7 @@ export function createVoiceRoutes(
|
||||
| ((patch: Partial<SharedUIState>) => SharedUIState)
|
||||
| undefined;
|
||||
let broadcaster: ModerationBroadcaster | undefined;
|
||||
let adminPassword: string | undefined;
|
||||
|
||||
if ("connect" in options && "disconnect" in options) {
|
||||
// Old signature: just VoiceController
|
||||
@@ -35,10 +37,21 @@ export function createVoiceRoutes(
|
||||
voiceController = opts.voiceController;
|
||||
patchSharedUIState = opts.patchSharedUIState;
|
||||
broadcaster = opts.broadcaster;
|
||||
adminPassword = opts.adminPassword;
|
||||
}
|
||||
|
||||
const adminAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!adminPassword) return next();
|
||||
const authHeader = req.headers["x-admin-password"];
|
||||
if (authHeader === adminPassword) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: "Unauthorized access to admin features" });
|
||||
}
|
||||
};
|
||||
|
||||
// GET /api/status - Get voice connection status
|
||||
router.get("/status", (_req, res, next) => {
|
||||
router.get("/status", (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const status = voiceController.getStatus();
|
||||
res.json(status);
|
||||
@@ -48,7 +61,7 @@ export function createVoiceRoutes(
|
||||
});
|
||||
|
||||
// GET /api/guilds - List available guilds
|
||||
router.get("/guilds", (_req, res, next) => {
|
||||
router.get("/guilds", (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const guilds = voiceController.listGuilds();
|
||||
res.json(guilds);
|
||||
@@ -58,7 +71,7 @@ export function createVoiceRoutes(
|
||||
});
|
||||
|
||||
// GET /api/guilds/:guildId/voice-channels - List voice channels in a guild
|
||||
router.get("/guilds/:guildId/voice-channels", async (req, res, next) => {
|
||||
router.get("/guilds/:guildId/voice-channels", async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { guildId } = req.params;
|
||||
|
||||
@@ -66,7 +79,7 @@ export function createVoiceRoutes(
|
||||
throw new AppError("Guild ID is required", "MISSING_GUILD_ID", 400);
|
||||
}
|
||||
|
||||
const channels = await voiceController.listVoiceChannels(guildId);
|
||||
const channels = await voiceController.listVoiceChannels(guildId as string);
|
||||
res.json(channels);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -74,7 +87,7 @@ export function createVoiceRoutes(
|
||||
});
|
||||
|
||||
// GET /api/guilds/:guildId/channels - List text channels in a guild
|
||||
router.get("/guilds/:guildId/channels", async (req, res, next) => {
|
||||
router.get("/guilds/:guildId/channels", async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { guildId } = req.params;
|
||||
|
||||
@@ -82,7 +95,7 @@ export function createVoiceRoutes(
|
||||
throw new AppError("Guild ID is required", "MISSING_GUILD_ID", 400);
|
||||
}
|
||||
|
||||
const channels = await voiceController.listWatchableChannels(guildId);
|
||||
const channels = await voiceController.listWatchableChannels(guildId as string);
|
||||
res.json(channels);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -90,7 +103,7 @@ export function createVoiceRoutes(
|
||||
});
|
||||
|
||||
// POST /api/connect - Connect to a voice channel
|
||||
router.post("/connect", async (req, res, next) => {
|
||||
router.post("/connect", adminAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { guildId, channelId } = req.body as {
|
||||
guildId?: string;
|
||||
@@ -125,7 +138,7 @@ export function createVoiceRoutes(
|
||||
});
|
||||
|
||||
// POST /api/disconnect - Disconnect from voice channel
|
||||
router.post("/disconnect", async (_req, res, next) => {
|
||||
router.post("/disconnect", adminAuth, async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
logger.info("Disconnecting from voice channel");
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ export async function startWebserver(
|
||||
// Create broadcaster instance
|
||||
const broadcaster = createBroadcaster();
|
||||
(globalThis as VoiceGlobals).moderationBroadcaster = broadcaster;
|
||||
(globalThis as any).ADMIN_PASSWORD = config.ADMIN_PASSWORD;
|
||||
|
||||
const streamer = new Streamer(_client);
|
||||
const screenController = createScreenShareController({
|
||||
@@ -268,15 +269,6 @@ export async function startWebserver(
|
||||
}
|
||||
});
|
||||
|
||||
const adminAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
const authHeader = req.headers["x-admin-password"];
|
||||
if (authHeader === config.ADMIN_PASSWORD) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: "Unauthorized access to admin features" });
|
||||
}
|
||||
};
|
||||
|
||||
// Register route modules
|
||||
app.use(
|
||||
"/api",
|
||||
@@ -284,17 +276,22 @@ export async function startWebserver(
|
||||
);
|
||||
app.use(
|
||||
"/api",
|
||||
adminAuth,
|
||||
createVoiceRoutes({
|
||||
voiceController,
|
||||
patchSharedUIState,
|
||||
broadcaster,
|
||||
adminPassword: config.ADMIN_PASSWORD,
|
||||
}),
|
||||
);
|
||||
app.use("/api", createMessageRoutes());
|
||||
app.use("/api", createAnalysisRoutes());
|
||||
app.use("/api", createSyncRoutes(_client));
|
||||
app.use("/api", adminAuth, createMediaRoutes(mediaController));
|
||||
app.use(
|
||||
"/api",
|
||||
createMediaRoutes(mediaController, {
|
||||
adminPassword: config.ADMIN_PASSWORD,
|
||||
}),
|
||||
);
|
||||
|
||||
// Inbound: Discord PCM → tagged chunks → browser
|
||||
(globalThis as VoiceGlobals).broadcastPcmToWeb = (
|
||||
|
||||
Reference in New Issue
Block a user