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
70
apps/api/src/auth/auth.controller.ts
Normal file
70
apps/api/src/auth/auth.controller.ts
Normal 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") }),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue