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:
glass beadian 2026-10-10 11:56:42 +00:00
commit aa5e262f3a
880 changed files with 111042 additions and 0 deletions

View file

@ -0,0 +1,70 @@
import { Body, Controller, Get, HttpCode, Inject, Post, Req, UnauthorizedException, UseGuards } from "@nestjs/common";
import type { Request } from "express";
import { RbacGuard, type AuthenticatedRequest } from "../rbac/rbac.guard.js";
import { optionalEmail, optionalString, parseId, requireObject, requireString, requireUsername, requireSession } from "../http/validation.js";
import { AuthService, type AuthResult, type LoginInput, type MeResponse, type RegisterInput } from "./auth.service.js";
import { verifySession } from "./session.js";
@Controller("auth")
export class AuthController {
constructor(@Inject(AuthService) private readonly auth: AuthService) {}
@Post("register")
async register(@Body() body: unknown): Promise<AuthResult> {
return this.auth.register(parseRegister(body));
}
@Post("login")
@HttpCode(200)
async login(@Body() body: unknown): Promise<AuthResult> {
return this.auth.login(parseLogin(body));
}
/** Any valid, epoch-current session may read itself — no permission gate beyond authentication. */
@Get("me")
@UseGuards(RbacGuard)
async me(@Req() request: Request & AuthenticatedRequest): Promise<MeResponse> {
return this.auth.me(requireSession(request));
}
/** A signed, unexpired session can refresh its epoch and live role. */
@Post("reissue")
@HttpCode(200)
async reissue(@Req() request: Request): Promise<AuthResult> {
const authorization = request.headers.authorization;
if (authorization === undefined || !authorization.startsWith("Bearer ")) throw new UnauthorizedException();
const session = verifySession(authorization.slice("Bearer ".length));
if (session === null) throw new UnauthorizedException();
return this.auth.reissue(session);
}
/** Stateless tokens: logout is client-side (drop the token). Revocation-by-epoch covers role changes. */
@Post("logout")
@HttpCode(200)
logout(): { ok: true } {
return { ok: true };
}
}
function parseRegister(value: unknown): RegisterInput {
const body = requireObject(value, "body");
const username = requireUsername(body.username);
const email = optionalEmail(body.email);
return {
workspaceName: requireString(body.workspaceName, "workspaceName").trim().slice(0, 100),
username,
password: requireString(body.password, "password"),
displayName: (optionalString(body.displayName, "displayName") ?? username).trim().slice(0, 100),
...(email === undefined ? {} : { email }),
};
}
function parseLogin(value: unknown): LoginInput {
const body = requireObject(value, "body");
const workspaceId = optionalString(body.workspaceId, "workspaceId");
return {
username: requireUsername(body.username),
password: requireString(body.password, "password"),
...(workspaceId === undefined ? {} : { workspaceId: parseId(workspaceId, "workspaceId") }),
};
}