feat: in-app Chat (Matrix JWT SSO) + Drive (embedded oCIS)

- api: matrix-sso module (tuwunel jwt -> login/get_token), POST /matrix/session, GET /matrix/identity, drive controller
- web: /chat full-height Element view, /files Library+Drive tabs, embedded-app frame component
- edge: /hub-sso.html handshake route (origin-pinned CSP), X-Frame-Options strip on files
- oCIS: csp.yaml frame-ancestors app.occupymedia.cc
- server config still required: tuwunel [global.jwt], MATRIX_SSO_JWT_SECRET, oCIS restart
This commit is contained in:
glass beadian 2026-10-13 14:57:07 +00:00
parent a2aa70c961
commit fe5f2403d6
22 changed files with 1057 additions and 8 deletions

View file

@ -0,0 +1,55 @@
import { Controller, Get, UseGuards } from "@nestjs/common";
import { RbacGuard, RequirePermission } from "../rbac/rbac.guard.js";
/**
* How the in-app Drive view reaches the oCIS web UI.
*
* interactive oCIS web uses its own identity provider. The user signs in
* ONCE inside the embedded frame; oCIS web keeps its OIDC
* tokens in the files origin's storage, which the Hub frame
* shares (app.* and files.* are the same site), so later
* visits open signed in.
* oidc oCIS and the Hub trust the same external OIDC issuer; the
* embedded oCIS web completes its authorization-code flow
* silently against the issuer's existing session.
*/
export type DriveAuthMode = "interactive" | "oidc";
export interface DriveSession {
/** Origin of the oCIS web UI, or null when the drive is not deployed. */
driveUrl: string | null;
authMode: DriveAuthMode;
}
/**
* ENV CONTRACT (read per call):
*
* FILES_DRIVE_URL oCIS web origin; default FILES_PUBLIC_BASE (the same
* host the library links already point at).
* FILES_DRIVE_AUTH "oidc" once oCIS and the Hub share an issuer; anything
* else (or unset) is "interactive".
*/
export function driveSessionFromEnv(env: Readonly<Record<string, string | undefined>> = process.env): DriveSession {
const raw = env.FILES_DRIVE_URL?.trim() || env.FILES_PUBLIC_BASE?.trim() || "";
let driveUrl: string | null = null;
if (raw !== "") {
try {
const url = new URL(raw);
if (url.protocol === "https:" || url.protocol === "http:") driveUrl = url.origin;
} catch {
driveUrl = null;
}
}
return { driveUrl, authMode: env.FILES_DRIVE_AUTH?.trim() === "oidc" ? "oidc" : "interactive" };
}
/** GET /drive/session — runtime drive location for the embedded Drive view. */
@Controller("drive")
@UseGuards(RbacGuard)
export class DriveController {
@Get("session")
@RequirePermission("media.view")
session(): DriveSession {
return driveSessionFromEnv();
}
}

View file

@ -1,6 +1,7 @@
import { Module } from "@nestjs/common";
import { GroupFilesController, PublicFileShareController } from "./group-files.controller.js";
import { GroupFilesService } from "./group-files.service.js";
import { DriveController } from "./drive.controller.js";
/**
* Group files module — the metadata index + share/link lifecycle over
@ -20,7 +21,7 @@ import { GroupFilesService } from "./group-files.service.js";
* deployment is absent instead of breaking startup.
*/
@Module({
controllers: [GroupFilesController, PublicFileShareController],
controllers: [GroupFilesController, PublicFileShareController, DriveController],
providers: [GroupFilesService],
exports: [GroupFilesService],
})