fix(collaboration): configure federation from existing deployment settings
This commit is contained in:
30
backend/src/modules/collaboration/config.ts
Normal file
30
backend/src/modules/collaboration/config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { instanceUrl } from "./transport"
|
||||
|
||||
type Settings = {
|
||||
API_BASE_URL?: string;
|
||||
ENCRYPTION_KEY?: string;
|
||||
FEDEO_PUBLIC_URL?: string;
|
||||
FEDEO_FEDERATION_URL?: string;
|
||||
FEDEO_FEDERATION_ENABLED?: string;
|
||||
}
|
||||
|
||||
/** Existing deployment settings are sufficient; extra URLs are optional overrides. */
|
||||
export function collaborationConfig(settings: Settings) {
|
||||
const configuredApi = settings.API_BASE_URL?.trim() || ""
|
||||
let apiUrl = (settings.FEDEO_FEDERATION_URL?.trim() || configuredApi).replace(/\/+$/, "")
|
||||
let publicUrl = settings.FEDEO_PUBLIC_URL?.trim().replace(/\/+$/, "") || ""
|
||||
if (!publicUrl) {
|
||||
try { publicUrl = new URL(configuredApi || apiUrl).origin } catch { /* Local development may have no public URL. */ }
|
||||
}
|
||||
const mode = settings.FEDEO_FEDERATION_ENABLED?.trim().toLowerCase() || "auto"
|
||||
if (!["auto", "true", "false"].includes(mode)) throw new Error("FEDEO_FEDERATION_ENABLED muss auto, true oder false sein")
|
||||
if (mode === "false") return { apiUrl, publicUrl, enabled: false, disabledReason: "Die Föderation wurde vom Betreiber deaktiviert." }
|
||||
|
||||
let disabledReason: string | undefined
|
||||
try { apiUrl = instanceUrl(apiUrl) }
|
||||
catch { disabledReason = "Föderation benötigt eine gültige öffentliche HTTPS-API_BASE_URL auf Port 443." }
|
||||
if (!disabledReason && !/^[a-f0-9]{64}$/i.test(settings.ENCRYPTION_KEY || "")) {
|
||||
disabledReason = "Föderation benötigt einen gültigen ENCRYPTION_KEY mit 64 Hex-Zeichen." }
|
||||
if (mode === "true" && disabledReason) throw new Error(disabledReason)
|
||||
return { apiUrl, publicUrl, enabled: !disabledReason, disabledReason }
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { FederationKey, Headers, instanceUrl, safeTransport, signedHeaders, Tran
|
||||
|
||||
type Options = {
|
||||
apiUrl: string; enabled: boolean;
|
||||
disabledReason?: string;
|
||||
seal: (value: string) => any; unseal: (value: any) => string;
|
||||
transport?: Transport;
|
||||
}
|
||||
@@ -22,7 +23,7 @@ export class FederationService {
|
||||
this.transport = options.transport || safeTransport
|
||||
}
|
||||
enabled() {
|
||||
if (!this.options.enabled) fail("Der Betreiber muss die Föderation zuerst aktivieren", 503)
|
||||
if (!this.options.enabled) fail(this.options.disabledReason || "Der Betreiber muss die Föderation zuerst aktivieren", 503)
|
||||
instanceUrl(this.origin)
|
||||
}
|
||||
async keys() {
|
||||
@@ -71,7 +72,7 @@ export class FederationService {
|
||||
VALUES (${ctx.tenantId},${origin},${instanceId},${keyId},${publicKey}) ON CONFLICT(tenant_id,origin) DO UPDATE SET active=true,updated_at=now()`)
|
||||
}
|
||||
}
|
||||
return { enabled: this.options.enabled, origin: this.origin, partners: (await rows(this.service.db,
|
||||
return { enabled: this.options.enabled, disabledReason: this.options.disabledReason, origin: this.origin, partners: (await rows(this.service.db,
|
||||
sql`SELECT origin,active,public_key FROM collaboration_partners WHERE tenant_id=${ctx.tenantId} ORDER BY origin`)).map(p => ({ origin: p.origin, active: p.active, fingerprint: hash(p.public_key) })) }
|
||||
}
|
||||
async call(tenantId: number, origin: string, action: string, payload: any) {
|
||||
|
||||
@@ -4,16 +4,18 @@ import { sendMail } from "../../utils/mailer"
|
||||
import { CollaborationService } from "./service"
|
||||
import { FederationService } from "./federation"
|
||||
import { encrypt, decrypt } from "../../utils/crypt"
|
||||
import { collaborationConfig } from "./config"
|
||||
|
||||
export function collaborationRuntime(server: FastifyInstance) {
|
||||
const apiUrl = String(process.env.FEDEO_FEDERATION_URL || secrets.API_BASE_URL || "").replace(/\/$/, "")
|
||||
const publicUrl = String(process.env.FEDEO_PUBLIC_URL || (apiUrl ? new URL(apiUrl).origin : "")).replace(/\/$/, "")
|
||||
const { apiUrl, publicUrl, enabled, disabledReason } = collaborationConfig({
|
||||
...process.env, API_BASE_URL: secrets.API_BASE_URL, ENCRYPTION_KEY: secrets.ENCRYPTION_KEY,
|
||||
})
|
||||
const service = new CollaborationService(server.db, {
|
||||
publicUrl,
|
||||
guestPepper: () => secrets.ENCRYPTION_KEY || "",
|
||||
sendMail,
|
||||
})
|
||||
const federation = new FederationService(service, { apiUrl, enabled: process.env.FEDEO_FEDERATION_ENABLED === "true", seal: encrypt, unseal: decrypt })
|
||||
const federation = new FederationService(service, { apiUrl, enabled, disabledReason, seal: encrypt, unseal: decrypt })
|
||||
service.options.changed = (roomId, db) => federation.changed(roomId, db)
|
||||
service.options.invited = (ctx, roomId, target, invitationId, db) => federation.invite(ctx, roomId, target, invitationId, db)
|
||||
let timer: ReturnType<typeof setInterval>
|
||||
|
||||
54
backend/tests/collaborationConfig.test.ts
Normal file
54
backend/tests/collaborationConfig.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { collaborationConfig } from "../src/modules/collaboration/config"
|
||||
|
||||
const deployment = { API_BASE_URL: "https://fedeo.example/backend", ENCRYPTION_KEY: "a".repeat(64) }
|
||||
|
||||
test("existing HTTPS deployment enables federation without additional environment values", () => {
|
||||
const config = collaborationConfig(deployment)
|
||||
assert.equal(config.enabled, true)
|
||||
assert.equal(config.apiUrl, deployment.API_BASE_URL)
|
||||
assert.equal(config.publicUrl, "https://fedeo.example")
|
||||
assert.equal(config.disabledReason, undefined)
|
||||
assert.equal(collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "auto", FEDEO_FEDERATION_URL: "", FEDEO_PUBLIC_URL: "" }).enabled, true)
|
||||
})
|
||||
|
||||
test("custom API paths are retained and instance addresses are canonicalized", () => {
|
||||
const config = collaborationConfig({ ...deployment, API_BASE_URL: "https://FEDEO.example:443/application/api/" })
|
||||
assert.equal(config.apiUrl, "https://fedeo.example/application/api")
|
||||
assert.equal(config.publicUrl, "https://fedeo.example")
|
||||
assert.equal(config.enabled, true)
|
||||
})
|
||||
|
||||
test("explicit opt-out stays disabled even on a correctly configured deployment", () => {
|
||||
const config = collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "false" })
|
||||
assert.equal(config.enabled, false)
|
||||
assert.match(config.disabledReason!, /Betreiber deaktiviert/)
|
||||
})
|
||||
|
||||
test("optional gateway override does not move guest links away from the web application", () => {
|
||||
const config = collaborationConfig({ ...deployment, FEDEO_FEDERATION_URL: "https://gateway.example/federation-api/" })
|
||||
assert.equal(config.apiUrl, "https://gateway.example/federation-api")
|
||||
assert.equal(config.publicUrl, "https://fedeo.example")
|
||||
assert.equal(collaborationConfig({ ...deployment, FEDEO_PUBLIC_URL: "https://portal.example/app/" }).publicUrl, "https://portal.example/app")
|
||||
})
|
||||
|
||||
test("local and incomplete deployments keep local collaboration available without federation", () => {
|
||||
for (const API_BASE_URL of [undefined, "", "invalid", "http://localhost:3100", "https://fedeo.example:8443/backend", "https://user:password@fedeo.example/backend"]) {
|
||||
const config = collaborationConfig({ ...deployment, API_BASE_URL })
|
||||
assert.equal(config.enabled, false, API_BASE_URL)
|
||||
assert.match(config.disabledReason!, /HTTPS-API_BASE_URL/)
|
||||
}
|
||||
for (const ENCRYPTION_KEY of [undefined, "", "invalid", "x".repeat(64)]) {
|
||||
const config = collaborationConfig({ ...deployment, ENCRYPTION_KEY })
|
||||
assert.equal(config.enabled, false)
|
||||
assert.match(config.disabledReason!, /ENCRYPTION_KEY/)
|
||||
}
|
||||
})
|
||||
|
||||
test("forced activation reports invalid configuration instead of silently ignoring it", () => {
|
||||
assert.throws(() => collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "true", API_BASE_URL: "http://localhost" }), /HTTPS/)
|
||||
assert.throws(() => collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "true", ENCRYPTION_KEY: "invalid" }), /ENCRYPTION_KEY/)
|
||||
assert.throws(() => collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "typo" }), /auto, true oder false/)
|
||||
assert.equal(collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "true" }).enabled, true)
|
||||
})
|
||||
Reference in New Issue
Block a user