Files
backspace/packages/server/src/routes/settings.ts
T
Jannis Braun e219229b63 feat: add Backspace design prototype and rebrand from Opencord
- Add Backspace-design-prototype.html: finalized "Aether Drift" design
  language with warm matte surfaces and subtle frosted glass accents
- Update CLAUDE.md with DESIGN SYSTEM section and remove Discord clone references
- Rename all Opencord references to Backspace across the full codebase
- Archive outdated design experiments and Discord-specific assets
- Add science-backed accessibility fallback (prefers-reduced-transparency)
2026-03-01 21:26:57 +01:00

125 lines
5.9 KiB
TypeScript

import type { FastifyInstance } from 'fastify';
import { eq } from 'drizzle-orm';
import { getDb, schema } from '../db/index.js';
import { authenticate } from '../utils/auth.js';
import type { InstanceStreamingLimits } from '@backspace/shared';
const VALID_RESOLUTIONS = [540, 720, 1080];
const VALID_FRAMERATES = [30, 45, 60];
function rowToLimits(row: typeof schema.instanceSettings.$inferSelect): InstanceStreamingLimits {
return {
maxBitrateKbps: row.maxBitrateKbps,
minBitrateKbps: row.minBitrateKbps,
bitrateStepKbps: row.bitrateStepKbps,
allowedResolutions: row.allowedResolutions.split(',').map(Number).filter((n) => VALID_RESOLUTIONS.includes(n)),
allowedFramerates: row.allowedFramerates.split(',').map(Number).filter((n) => VALID_FRAMERATES.includes(n)),
maxResolution: row.maxResolution,
maxFramerate: row.maxFramerate,
};
}
export async function settingsRoutes(app: FastifyInstance): Promise<void> {
// GET /api/settings/streaming — any authenticated user can read instance limits
app.get('/api/settings/streaming', { preHandler: authenticate }, async (_request, reply) => {
const db = getDb();
const row = db.select().from(schema.instanceSettings).where(eq(schema.instanceSettings.id, 1)).get();
if (!row) {
return reply.code(500).send({ error: 'Instance settings not initialized', statusCode: 500 });
}
return reply.code(200).send(rowToLimits(row));
});
// PATCH /api/settings/streaming — admin only
app.patch<{ Body: Partial<InstanceStreamingLimits> }>('/api/settings/streaming', { preHandler: authenticate }, async (request, reply) => {
const db = getDb();
// Verify caller is an instance admin
const caller = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
if (!caller || caller.isAdmin !== 1) {
return reply.code(403).send({ error: 'Only instance admins can modify streaming settings', statusCode: 403 });
}
const body = request.body;
const updateData: Record<string, number | string> = { updatedAt: Date.now() };
if (body.maxBitrateKbps !== undefined) {
if (typeof body.maxBitrateKbps !== 'number' || body.maxBitrateKbps < 500 || body.maxBitrateKbps > 50000) {
return reply.code(400).send({ error: 'maxBitrateKbps must be between 500 and 50000', statusCode: 400 });
}
updateData.maxBitrateKbps = body.maxBitrateKbps;
}
if (body.minBitrateKbps !== undefined) {
if (typeof body.minBitrateKbps !== 'number' || body.minBitrateKbps < 100 || body.minBitrateKbps > 50000) {
return reply.code(400).send({ error: 'minBitrateKbps must be between 100 and 50000', statusCode: 400 });
}
updateData.minBitrateKbps = body.minBitrateKbps;
}
if (body.bitrateStepKbps !== undefined) {
if (typeof body.bitrateStepKbps !== 'number' || body.bitrateStepKbps < 50 || body.bitrateStepKbps > 5000) {
return reply.code(400).send({ error: 'bitrateStepKbps must be between 50 and 5000', statusCode: 400 });
}
updateData.bitrateStepKbps = body.bitrateStepKbps;
}
if (body.allowedResolutions !== undefined) {
if (!Array.isArray(body.allowedResolutions) || body.allowedResolutions.length === 0) {
return reply.code(400).send({ error: 'allowedResolutions must be a non-empty array', statusCode: 400 });
}
const invalid = body.allowedResolutions.filter((r) => !VALID_RESOLUTIONS.includes(r));
if (invalid.length > 0) {
return reply.code(400).send({ error: `Invalid resolutions: ${invalid.join(', ')}. Allowed: ${VALID_RESOLUTIONS.join(', ')}`, statusCode: 400 });
}
updateData.allowedResolutions = body.allowedResolutions.sort((a, b) => a - b).join(',');
}
if (body.allowedFramerates !== undefined) {
if (!Array.isArray(body.allowedFramerates) || body.allowedFramerates.length === 0) {
return reply.code(400).send({ error: 'allowedFramerates must be a non-empty array', statusCode: 400 });
}
const invalid = body.allowedFramerates.filter((f) => !VALID_FRAMERATES.includes(f));
if (invalid.length > 0) {
return reply.code(400).send({ error: `Invalid framerates: ${invalid.join(', ')}. Allowed: ${VALID_FRAMERATES.join(', ')}`, statusCode: 400 });
}
updateData.allowedFramerates = body.allowedFramerates.sort((a, b) => a - b).join(',');
}
if (body.maxResolution !== undefined) {
if (!VALID_RESOLUTIONS.includes(body.maxResolution)) {
return reply.code(400).send({ error: `maxResolution must be one of: ${VALID_RESOLUTIONS.join(', ')}`, statusCode: 400 });
}
updateData.maxResolution = body.maxResolution;
}
if (body.maxFramerate !== undefined) {
if (!VALID_FRAMERATES.includes(body.maxFramerate)) {
return reply.code(400).send({ error: `maxFramerate must be one of: ${VALID_FRAMERATES.join(', ')}`, statusCode: 400 });
}
updateData.maxFramerate = body.maxFramerate;
}
// Cross-field validation: min < max
const currentRow = db.select().from(schema.instanceSettings).where(eq(schema.instanceSettings.id, 1)).get();
if (!currentRow) {
return reply.code(500).send({ error: 'Instance settings not initialized', statusCode: 500 });
}
const effectiveMin = (updateData.minBitrateKbps as number | undefined) ?? currentRow.minBitrateKbps;
const effectiveMax = (updateData.maxBitrateKbps as number | undefined) ?? currentRow.maxBitrateKbps;
if (effectiveMin >= effectiveMax) {
return reply.code(400).send({ error: 'minBitrateKbps must be less than maxBitrateKbps', statusCode: 400 });
}
db.update(schema.instanceSettings).set(updateData).where(eq(schema.instanceSettings.id, 1)).run();
const updatedRow = db.select().from(schema.instanceSettings).where(eq(schema.instanceSettings.id, 1)).get();
if (!updatedRow) {
return reply.code(500).send({ error: 'Failed to read updated settings', statusCode: 500 });
}
return reply.code(200).send(rowToLimits(updatedRow));
});
}