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:
parent
a2aa70c961
commit
fe5f2403d6
22 changed files with 1057 additions and 8 deletions
55
apps/api/src/group-files/drive.controller.ts
Normal file
55
apps/api/src/group-files/drive.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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],
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue