Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
79
apps/api/src/http/validation.ts
Normal file
79
apps/api/src/http/validation.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { BadRequestException } from "@nestjs/common";
|
||||
import type { SessionPayload } from "../auth/session.js";
|
||||
import type { AuthenticatedRequest } from "../rbac/rbac.guard.js";
|
||||
|
||||
/** Shared request-shape guards for controllers that take `unknown` bodies. */
|
||||
|
||||
export function requireSession(request: AuthenticatedRequest): SessionPayload {
|
||||
if (request.session === undefined) throw new Error("unreachable: RBAC guard did not attach a session");
|
||||
return request.session;
|
||||
}
|
||||
|
||||
export function requireObject(value: unknown, field: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new BadRequestException(`${field} must be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function requireString(value: unknown, field: string): string {
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
throw new BadRequestException(`${field} must be a non-empty string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function optionalString(value: unknown, field: string): string | undefined {
|
||||
if (value === undefined || value === null || value === "") return undefined;
|
||||
return requireString(value, field);
|
||||
}
|
||||
|
||||
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export function requireEmail(value: unknown, field = "email"): string {
|
||||
const email = requireString(value, field).trim().toLowerCase();
|
||||
if (!EMAIL.test(email) || email.length > 254) throw new BadRequestException(`${field} must be an email address`);
|
||||
return email;
|
||||
}
|
||||
|
||||
export function optionalEmail(value: unknown, field = "email"): string | undefined {
|
||||
const email = optionalString(value, field);
|
||||
return email === undefined ? undefined : requireEmail(email, field);
|
||||
}
|
||||
|
||||
export const USERNAME_PATTERN = /^[a-z0-9][a-z0-9_.-]{1,31}$/;
|
||||
|
||||
export function requireUsername(value: unknown, field = "username"): string {
|
||||
const username = requireString(value, field).trim().toLowerCase();
|
||||
if (!USERNAME_PATTERN.test(username)) {
|
||||
throw new BadRequestException(`${field} must be 2-32 characters: lowercase letters, digits, dot, underscore or hyphen`);
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
/** Positive integer within an inclusive cap (e.g. expiresInHours 1..720, maxUses 1..10000). */
|
||||
export function requirePositiveInt(value: unknown, field: string, max: number): number {
|
||||
const n = typeof value === "number" ? value : NaN;
|
||||
if (!Number.isInteger(n) || n < 1 || n > max) {
|
||||
throw new BadRequestException(`${field} must be an integer between 1 and ${max}`);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function isUuid(value: string): boolean {
|
||||
return UUID.test(value);
|
||||
}
|
||||
|
||||
export function parseId(value: string, field = "id"): string {
|
||||
if (!isUuid(value)) throw new BadRequestException(`${field} must be a UUID`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseLimit(value: unknown, fallback: number, max: number): number {
|
||||
if (value === undefined || value === "") return fallback;
|
||||
const n = typeof value === "string" ? Number(value) : NaN;
|
||||
if (!Number.isInteger(n) || n < 1) throw new BadRequestException("limit must be a positive integer");
|
||||
return Math.min(n, max);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue