import type { NextFunction, Request, Response } from 'express'; import { Router } from 'express'; import { z } from 'zod'; import { ensureMinioBucket, minioBucket, minioClient, minioPresignedExpirySeconds } from '../utils/minio'; const adminUsername = process.env.ADMIN_USERNAME; const adminPassword = process.env.ADMIN_PASSWORD; if (!adminUsername || !adminPassword) { throw new Error('ADMIN_USERNAME and ADMIN_PASSWORD must be set to use the admin dashboard'); } const router = Router(); const uploadSchema = z.object({ fileName: z.string().trim().min(1).max(255), prefix: z.string().trim().optional(), }); const objectKeySchema = z.object({ objectKey: z.string().trim().min(1), }); const listSchema = z.object({ prefix: z.string().trim().optional(), limit: z.coerce.number().int().min(1).max(100).default(20), }); const sanitizeSegment = (value: string): string => value.replace(/[^a-zA-Z0-9._/-]/g, '_'); const buildObjectKey = (fileName: string, prefix?: string): string => { const safePrefix = prefix ? sanitizeSegment(prefix).replace(/^\/+|\/+$/g, '') : 'admin-tests'; const safeFileName = sanitizeSegment(fileName); return `${safePrefix}/admin/${Date.now()}-${safeFileName}`; }; const sendAuthRequired = (res: Response, message: string): Response => { res.setHeader('WWW-Authenticate', 'Basic realm="Video Admin Dashboard"'); return res.status(401).json({ message }); }; const requireAdminAuth = (req: Request, res: Response, next: NextFunction): void => { const authorization = req.headers.authorization; if (!authorization?.startsWith('Basic ')) { sendAuthRequired(res, 'Authorization required for admin dashboard'); return; } const rawCredentials = authorization.slice(6); let decoded: string; try { decoded = Buffer.from(rawCredentials, 'base64').toString('utf8'); } catch { sendAuthRequired(res, 'Malformed authorization header'); return; } const separatorIndex = decoded.indexOf(':'); if (separatorIndex < 0) { sendAuthRequired(res, 'Malformed authorization header'); return; } const username = decoded.slice(0, separatorIndex); const password = decoded.slice(separatorIndex + 1); if (username !== adminUsername || password !== adminPassword) { sendAuthRequired(res, 'Invalid admin credentials'); return; } next(); }; router.use(requireAdminAuth); router.get('/', (_req, res) => { const html = ` Video Admin Dashboard

Video Admin Dashboard

Use this interface to request a presigned upload URL, push a file into MinIO, and inspect the objects stored in ${minioBucket}.

Upload a file

Ready

Bucket contents

`; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(html); }); router.post('/upload-url', async (req, res) => { const parsed = uploadSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() }); return; } await ensureMinioBucket(); const objectKey = buildObjectKey(parsed.data.fileName, parsed.data.prefix); const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds); const expiresAt = new Date(Date.now() + minioPresignedExpirySeconds * 1000); res.status(201).json({ message: 'Upload URL generated', bucket: minioBucket, objectKey, uploadUrl, expiresAt, expiresInSeconds: minioPresignedExpirySeconds, }); }); router.get('/objects', async (req, res) => { const parsed = listSchema.safeParse(req.query); if (!parsed.success) { res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() }); return; } await ensureMinioBucket(); const objects = await new Promise< { objectKey: string | undefined; size: number; etag: string | undefined; lastModified: Date | undefined }[] >((resolve, reject) => { const results: { objectKey: string | undefined; size: number; etag: string | undefined; lastModified: Date | undefined; }[] = []; const stream = minioClient.listObjectsV2(minioBucket, parsed.data.prefix, true); let finished = false; const finish = () => { if (finished) return; finished = true; resolve(results); }; stream.on('data', (item) => { if (results.length >= parsed.data.limit) { stream.destroy(); return; } results.push({ objectKey: item.name, size: item.size, etag: item.etag, lastModified: item.lastModified, }); }); stream.on('error', (error) => { if (!finished) { finished = true; reject(error); } }); stream.on('end', finish); stream.on('close', finish); }); res.json({ bucket: minioBucket, count: objects.length, objects }); }); router.delete('/object', async (req, res) => { const parsed = objectKeySchema.safeParse(req.query); if (!parsed.success) { res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() }); return; } await ensureMinioBucket(); await minioClient.removeObject(minioBucket, parsed.data.objectKey); res.json({ message: 'Object deleted', bucket: minioBucket, objectKey: parsed.data.objectKey }); }); export default router;