feat: update server validation for expanded resolutions and framerates

Replace hardcoded VALID_RESOLUTIONS/VALID_FRAMERATES with shared STANDARD_RESOLUTIONS/STANDARD_FRAMERATES constants. Update rowToLimits to handle the 'native' resolution value in DB parsing, and update all PATCH validation blocks to accept the expanded resolution/framerate sets and the 'native' option.
This commit is contained in:
Jannis Braun
2026-03-21 23:23:25 +01:00
parent a02b098fa7
commit b9f11489fe
+24 -14
View File
@@ -4,17 +4,22 @@ import { getDb, schema } from '../db/index.js';
import { authenticate, requireAdmin } from '../utils/auth.js'; import { authenticate, requireAdmin } from '../utils/auth.js';
import { config } from '../config.js'; import { config } from '../config.js';
import type { InstanceStreamingLimits, InstanceAdminSettings } from '@backspace/shared'; import type { InstanceStreamingLimits, InstanceAdminSettings } from '@backspace/shared';
import { STANDARD_RESOLUTIONS, STANDARD_FRAMERATES } from '@backspace/shared/src/constants.js';
const VALID_RESOLUTIONS = [540, 720, 1080];
const VALID_FRAMERATES = [30, 45, 60];
function rowToLimits(row: typeof schema.instanceSettings.$inferSelect): InstanceStreamingLimits { function rowToLimits(row: typeof schema.instanceSettings.$inferSelect): InstanceStreamingLimits {
return { return {
maxBitrateKbps: row.maxBitrateKbps, maxBitrateKbps: row.maxBitrateKbps,
minBitrateKbps: row.minBitrateKbps, minBitrateKbps: row.minBitrateKbps,
bitrateStepKbps: row.bitrateStepKbps, bitrateStepKbps: row.bitrateStepKbps,
allowedResolutions: row.allowedResolutions.split(',').map(Number).filter((n) => VALID_RESOLUTIONS.includes(n)), allowedResolutions: row.allowedResolutions.split(',')
allowedFramerates: row.allowedFramerates.split(',').map(Number).filter((n) => VALID_FRAMERATES.includes(n)), .map((s) => s.trim())
.map((s) => s === 'native' ? 'native' as const : Number(s))
.filter((v): v is number | 'native' =>
v === 'native' || (typeof v === 'number' && !isNaN(v) && (STANDARD_RESOLUTIONS as readonly number[]).includes(v))
),
allowedFramerates: row.allowedFramerates.split(',')
.map(Number)
.filter((n) => (STANDARD_FRAMERATES as readonly number[]).includes(n)),
maxResolution: row.maxResolution, maxResolution: row.maxResolution,
maxFramerate: row.maxFramerate, maxFramerate: row.maxFramerate,
discoveryEnabled: row.discoveryEnabled === 1, discoveryEnabled: row.discoveryEnabled === 1,
@@ -64,34 +69,39 @@ export async function settingsRoutes(app: FastifyInstance): Promise<void> {
if (!Array.isArray(body.allowedResolutions) || body.allowedResolutions.length === 0) { if (!Array.isArray(body.allowedResolutions) || body.allowedResolutions.length === 0) {
return reply.code(400).send({ error: 'allowedResolutions must be a non-empty array', statusCode: 400 }); 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)); const invalid = body.allowedResolutions.filter((r) =>
r !== 'native' && !(STANDARD_RESOLUTIONS as readonly number[]).includes(r as number)
);
if (invalid.length > 0) { if (invalid.length > 0) {
return reply.code(400).send({ error: `Invalid resolutions: ${invalid.join(', ')}. Allowed: ${VALID_RESOLUTIONS.join(', ')}`, statusCode: 400 }); return reply.code(400).send({ error: `Invalid resolutions: ${invalid.join(', ')}. Allowed: ${[...STANDARD_RESOLUTIONS, 'native'].join(', ')}`, statusCode: 400 });
} }
updateData.allowedResolutions = body.allowedResolutions.sort((a, b) => a - b).join(','); // Serialize: numbers sorted ascending, 'native' always last
const nums = body.allowedResolutions.filter((r): r is number => r !== 'native').sort((a, b) => a - b);
const hasNative = body.allowedResolutions.includes('native');
updateData.allowedResolutions = [...nums, ...(hasNative ? ['native'] : [])].join(',');
} }
if (body.allowedFramerates !== undefined) { if (body.allowedFramerates !== undefined) {
if (!Array.isArray(body.allowedFramerates) || body.allowedFramerates.length === 0) { if (!Array.isArray(body.allowedFramerates) || body.allowedFramerates.length === 0) {
return reply.code(400).send({ error: 'allowedFramerates must be a non-empty array', statusCode: 400 }); 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)); const invalid = body.allowedFramerates.filter((f) => !(STANDARD_FRAMERATES as readonly number[]).includes(f));
if (invalid.length > 0) { if (invalid.length > 0) {
return reply.code(400).send({ error: `Invalid framerates: ${invalid.join(', ')}. Allowed: ${VALID_FRAMERATES.join(', ')}`, statusCode: 400 }); return reply.code(400).send({ error: `Invalid framerates: ${invalid.join(', ')}. Allowed: ${STANDARD_FRAMERATES.join(', ')}`, statusCode: 400 });
} }
updateData.allowedFramerates = body.allowedFramerates.sort((a, b) => a - b).join(','); updateData.allowedFramerates = body.allowedFramerates.sort((a, b) => a - b).join(',');
} }
if (body.maxResolution !== undefined) { if (body.maxResolution !== undefined) {
if (!VALID_RESOLUTIONS.includes(body.maxResolution)) { if (!(STANDARD_RESOLUTIONS as readonly number[]).includes(body.maxResolution)) {
return reply.code(400).send({ error: `maxResolution must be one of: ${VALID_RESOLUTIONS.join(', ')}`, statusCode: 400 }); return reply.code(400).send({ error: `maxResolution must be one of: ${STANDARD_RESOLUTIONS.join(', ')}`, statusCode: 400 });
} }
updateData.maxResolution = body.maxResolution; updateData.maxResolution = body.maxResolution;
} }
if (body.maxFramerate !== undefined) { if (body.maxFramerate !== undefined) {
if (!VALID_FRAMERATES.includes(body.maxFramerate)) { if (!(STANDARD_FRAMERATES as readonly number[]).includes(body.maxFramerate)) {
return reply.code(400).send({ error: `maxFramerate must be one of: ${VALID_FRAMERATES.join(', ')}`, statusCode: 400 }); return reply.code(400).send({ error: `maxFramerate must be one of: ${STANDARD_FRAMERATES.join(', ')}`, statusCode: 400 });
} }
updateData.maxFramerate = body.maxFramerate; updateData.maxFramerate = body.maxFramerate;
} }