KI-AGENT: Matrix durch nativen FEDEO-Chat ersetzen
This commit is contained in:
47
.env.example
47
.env.example
@@ -117,50 +117,3 @@ FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
|
|||||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
||||||
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
||||||
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||||
FEDEO_BOOTSTRAP_MATRIX=true
|
|
||||||
|
|
||||||
# FEDEO Matrix-Kommunikation
|
|
||||||
#
|
|
||||||
# Diese Werte werden von docker-compose.selfhost.yml für den integrierten
|
|
||||||
# Matrix-Stack gelesen. Für produktive Systeme müssen alle Geheimnisse ersetzt
|
|
||||||
# werden.
|
|
||||||
|
|
||||||
MATRIX_SERVER_NAME=app.example.com
|
|
||||||
|
|
||||||
MATRIX_POSTGRES_DB=synapse
|
|
||||||
MATRIX_POSTGRES_USER=synapse
|
|
||||||
MATRIX_POSTGRES_PASSWORD=change-this-matrix-db-password
|
|
||||||
|
|
||||||
MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
|
|
||||||
|
|
||||||
LIVEKIT_KEY=fedeo-livekit
|
|
||||||
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
|
|
||||||
|
|
||||||
# Backend-Integration im Selfhost-Stack
|
|
||||||
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
|
|
||||||
MATRIX_RTC_HOST=app.example.com
|
|
||||||
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
|
|
||||||
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
|
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET=change-this-matrix-registration-secret
|
|
||||||
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
|
||||||
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
|
|
||||||
|
|
||||||
# Lokale Matrix-Entwicklung
|
|
||||||
MATRIX_DEV_SYNAPSE_PORT=8008
|
|
||||||
MATRIX_DEV_ELEMENT_PORT=8080
|
|
||||||
MATRIX_DEV_RTC_JWT_PORT=8081
|
|
||||||
MATRIX_DEV_LIVEKIT_PORT=7880
|
|
||||||
MATRIX_DEV_LIVEKIT_TCP_PORT=7881
|
|
||||||
MATRIX_DEV_LIVEKIT_RTC_MIN_PORT=50000
|
|
||||||
MATRIX_DEV_LIVEKIT_RTC_MAX_PORT=50100
|
|
||||||
MATRIX_DEV_LIVEKIT_NODE_IP=127.0.0.1
|
|
||||||
MATRIX_DEV_TURN_PORT=3478
|
|
||||||
MATRIX_DEV_TURN_MIN_PORT=49160
|
|
||||||
MATRIX_DEV_TURN_MAX_PORT=49200
|
|
||||||
|
|
||||||
# Lokale Backend-Integration gegen den Matrix-Entwicklungsstack
|
|
||||||
# MATRIX_HOMESERVER_URL=http://localhost:8008
|
|
||||||
# MATRIX_RTC_JWT_URL=http://localhost:8081
|
|
||||||
# MATRIX_LIVEKIT_URL=ws://localhost:7880
|
|
||||||
# MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
|
|
||||||
# NUXT_PUBLIC_MATRIX_ELEMENT_URL=http://localhost:8080
|
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -239,33 +239,16 @@ FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
|
|||||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
||||||
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
||||||
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||||
|
|
||||||
MATRIX_SERVER_NAME=app.example.com
|
|
||||||
MATRIX_POSTGRES_DB=synapse
|
|
||||||
MATRIX_POSTGRES_USER=synapse
|
|
||||||
MATRIX_POSTGRES_PASSWORD=change-this-matrix-db-password
|
|
||||||
MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
|
|
||||||
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
|
|
||||||
MATRIX_RTC_HOST=app.example.com
|
|
||||||
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
|
|
||||||
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
|
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET=change-this-matrix-registration-secret
|
|
||||||
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
|
||||||
LIVEKIT_KEY=fedeo-livekit
|
|
||||||
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
|
|
||||||
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Die `FEDEO_BOOTSTRAP_*`-Werte sind für den ersten Start gedacht. Wenn `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der `.env` entfernen oder ändern.
|
Die `FEDEO_BOOTSTRAP_*`-Werte sind für den ersten Start gedacht. Wenn `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der `.env` entfernen oder ändern.
|
||||||
|
|
||||||
## Docker Compose mit optionalem S3 und Matrix
|
## Docker Compose mit optionalem S3
|
||||||
|
|
||||||
Die Selfhost-Konfiguration wird im Betriebsverzeichnis als `docker-compose.yml` abgelegt. Sie startet MinIO standardmäßig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services `minio` und `createbuckets` entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.
|
Die Selfhost-Konfiguration wird im Betriebsverzeichnis als `docker-compose.yml` abgelegt. Sie startet MinIO standardmäßig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services `minio` und `createbuckets` entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.
|
||||||
|
|
||||||
Seafile wird bewusst nicht im Standard-Compose-Stack gestartet. FEDEO kann später gegen einen extern betriebenen Seafile-Dienst sprechen; dafür bleiben `SEAFILE_BASE_URL`, `SEAFILE_INTERNAL_URL`, `SEAFILE_ADMIN_EMAIL` und `SEAFILE_ADMIN_PASSWORD` als generische Anbindungswerte vorgesehen. `FEDEO_FILE_BACKEND=s3` bleibt der Standard, bis die Backend-Integration für Seafile vollständig umgesetzt ist.
|
Seafile wird bewusst nicht im Standard-Compose-Stack gestartet. FEDEO kann später gegen einen extern betriebenen Seafile-Dienst sprechen; dafür bleiben `SEAFILE_BASE_URL`, `SEAFILE_INTERNAL_URL`, `SEAFILE_ADMIN_EMAIL` und `SEAFILE_ADMIN_PASSWORD` als generische Anbindungswerte vorgesehen. `FEDEO_FILE_BACKEND=s3` bleibt der Standard, bis die Backend-Integration für Seafile vollständig umgesetzt ist.
|
||||||
|
|
||||||
Der Matrix-Stack ist im Selfhost-Compose direkt enthalten. Er umfasst Synapse, eine eigene PostgreSQL-Datenbank für Synapse, Redis, `.well-known/matrix`, coturn, LiveKit, den LiveKit-JWT-Service und Element Web. Das einfache Selfhost-Setup nutzt nur `DOMAIN`: Synapse läuft unter `https://DOMAIN/_matrix`, Matrix-Well-Known unter `https://DOMAIN/.well-known/matrix`, LiveKit unter `https://DOMAIN/livekit/sfu`, der JWT-Service unter `https://DOMAIN/livekit/jwt` und Element Web unter `https://DOMAIN/element`.
|
|
||||||
|
|
||||||
Das Backend führt beim Containerstart standardmäßig `npm run migrate` aus. Setze `FEDEO_RUN_MIGRATIONS=false`, wenn du Migrationen bewusst manuell ausführen möchtest.
|
Das Backend führt beim Containerstart standardmäßig `npm run migrate` aus. Setze `FEDEO_RUN_MIGRATIONS=false`, wenn du Migrationen bewusst manuell ausführen möchtest.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -463,7 +446,6 @@ Im Deploy-Verzeichnis:
|
|||||||
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml up -d
|
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Synapse erzeugt `matrix/synapse/homeserver.yaml` beim ersten Start automatisch und aktualisiert die für FEDEO relevanten Werte aus der `.env`. `MATRIX_REGISTRATION_SHARED_SECRET` muss in der `.env` gesetzt und geheim bleiben, weil FEDEO damit Matrix-Nutzer provisioniert.
|
|
||||||
|
|
||||||
Danach Status prufen:
|
Danach Status prufen:
|
||||||
|
|
||||||
@@ -541,8 +523,6 @@ Regelmassig sichern:
|
|||||||
|
|
||||||
- `./postgres`
|
- `./postgres`
|
||||||
- `./minio` falls MinIO lokal genutzt wird
|
- `./minio` falls MinIO lokal genutzt wird
|
||||||
- `./matrix/postgres` falls Matrix lokal betrieben wird
|
|
||||||
- `./matrix/synapse` falls Matrix lokal betrieben wird
|
|
||||||
- `./traefik/letsencrypt/acme.json`
|
- `./traefik/letsencrypt/acme.json`
|
||||||
- deine `.env`
|
- deine `.env`
|
||||||
- deine dokumentierten Secret-Werte aus der `.env` oder deinem Secret-Management
|
- deine dokumentierten Secret-Werte aus der `.env` oder deinem Secret-Management
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ CREATE TABLE "communication_rooms" (
|
|||||||
"entity_type" text,
|
"entity_type" text,
|
||||||
"entity_id" bigint,
|
"entity_id" bigint,
|
||||||
"entity_uuid" uuid,
|
"entity_uuid" uuid,
|
||||||
"matrix_room_id" text,
|
|
||||||
"matrix_alias" text,
|
|
||||||
"parent_space_room_id" text,
|
|
||||||
"archived" boolean DEFAULT false NOT NULL,
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
"updated_at" timestamp with time zone,
|
"updated_at" timestamp with time zone,
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ CREATE TABLE IF NOT EXISTS "communication_rooms" (
|
|||||||
"entity_type" text,
|
"entity_type" text,
|
||||||
"entity_id" bigint,
|
"entity_id" bigint,
|
||||||
"entity_uuid" uuid,
|
"entity_uuid" uuid,
|
||||||
"matrix_room_id" text,
|
|
||||||
"matrix_alias" text,
|
|
||||||
"parent_space_room_id" text,
|
|
||||||
"archived" boolean DEFAULT false NOT NULL,
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
"updated_at" timestamp with time zone,
|
"updated_at" timestamp with time zone,
|
||||||
|
|||||||
42
backend/db/migrations/0070_native_communication_chat.sql
Normal file
42
backend/db/migrations/0070_native_communication_chat.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
ALTER TABLE "communication_rooms" DROP COLUMN IF EXISTS "matrix_room_id";
|
||||||
|
ALTER TABLE "communication_rooms" DROP COLUMN IF EXISTS "matrix_alias";
|
||||||
|
ALTER TABLE "communication_rooms" DROP COLUMN IF EXISTS "parent_space_room_id";
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "communication_room_members" (
|
||||||
|
"room_id" uuid NOT NULL REFERENCES "communication_rooms"("id") ON DELETE cascade,
|
||||||
|
"user_id" uuid NOT NULL REFERENCES "auth_users"("id") ON DELETE cascade,
|
||||||
|
"joined_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "communication_room_members_room_id_user_id_pk" PRIMARY KEY ("room_id", "user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "communication_room_members_user_idx"
|
||||||
|
ON "communication_room_members" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "communication_messages" (
|
||||||
|
"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
"tenant_id" bigint NOT NULL REFERENCES "tenants"("id") ON DELETE cascade,
|
||||||
|
"room_id" uuid NOT NULL REFERENCES "communication_rooms"("id") ON DELETE cascade,
|
||||||
|
"author_user_id" uuid NOT NULL REFERENCES "auth_users"("id"),
|
||||||
|
"body" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "communication_messages_room_message_idx"
|
||||||
|
ON "communication_messages" ("room_id", "id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "communication_messages_tenant_idx"
|
||||||
|
ON "communication_messages" ("tenant_id");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "communication_room_reads" (
|
||||||
|
"room_id" uuid NOT NULL REFERENCES "communication_rooms"("id") ON DELETE cascade,
|
||||||
|
"user_id" uuid NOT NULL REFERENCES "auth_users"("id") ON DELETE cascade,
|
||||||
|
"last_read_message_id" bigint,
|
||||||
|
"read_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "communication_room_reads_room_id_user_id_pk" PRIMARY KEY ("room_id", "user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO "communication_room_members" ("room_id", "user_id")
|
||||||
|
SELECT room.id, tenant_user.user_id
|
||||||
|
FROM "communication_rooms" room
|
||||||
|
JOIN "auth_tenant_users" tenant_user ON tenant_user.tenant_id = room.tenant_id
|
||||||
|
WHERE room.type IN ('general', 'room')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
@@ -470,6 +470,13 @@
|
|||||||
"when": 1788803000000,
|
"when": 1788803000000,
|
||||||
"tag": "0069_reset_email_entity_suggestions",
|
"tag": "0069_reset_email_entity_suggestions",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 67,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788850800000,
|
||||||
|
"tag": "0070_native_communication_chat",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
34
backend/db/schema/communication_messages.ts
Normal file
34
backend/db/schema/communication_messages.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { bigint, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { communicationRooms } from "./communication_rooms"
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
|
||||||
|
export const communicationMessages = pgTable(
|
||||||
|
"communication_messages",
|
||||||
|
{
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => communicationRooms.id, { onDelete: "cascade" }),
|
||||||
|
authorUserId: uuid("author_user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id),
|
||||||
|
body: text("body").notNull(),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
roomMessageIdx: index("communication_messages_room_message_idx").on(table.roomId, table.id),
|
||||||
|
tenantIdx: index("communication_messages_tenant_idx").on(table.tenantId),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommunicationMessage = typeof communicationMessages.$inferSelect
|
||||||
|
export type NewCommunicationMessage = typeof communicationMessages.$inferInsert
|
||||||
26
backend/db/schema/communication_room_members.ts
Normal file
26
backend/db/schema/communication_room_members.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { index, pgTable, primaryKey, timestamp, uuid } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { communicationRooms } from "./communication_rooms"
|
||||||
|
|
||||||
|
export const communicationRoomMembers = pgTable(
|
||||||
|
"communication_room_members",
|
||||||
|
{
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => communicationRooms.id, { onDelete: "cascade" }),
|
||||||
|
userId: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||||
|
joinedAt: timestamp("joined_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
pk: primaryKey({ columns: [table.roomId, table.userId] }),
|
||||||
|
userIdx: index("communication_room_members_user_idx").on(table.userId),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommunicationRoomMember = typeof communicationRoomMembers.$inferSelect
|
||||||
|
export type NewCommunicationRoomMember = typeof communicationRoomMembers.$inferInsert
|
||||||
26
backend/db/schema/communication_room_reads.ts
Normal file
26
backend/db/schema/communication_room_reads.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { bigint, pgTable, primaryKey, timestamp, uuid } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { communicationRooms } from "./communication_rooms"
|
||||||
|
|
||||||
|
export const communicationRoomReads = pgTable(
|
||||||
|
"communication_room_reads",
|
||||||
|
{
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => communicationRooms.id, { onDelete: "cascade" }),
|
||||||
|
userId: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||||
|
lastReadMessageId: bigint("last_read_message_id", { mode: "number" }),
|
||||||
|
readAt: timestamp("read_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
pk: primaryKey({ columns: [table.roomId, table.userId] }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommunicationRoomRead = typeof communicationRoomReads.$inferSelect
|
||||||
|
export type NewCommunicationRoomRead = typeof communicationRoomReads.$inferInsert
|
||||||
@@ -30,10 +30,6 @@ export const communicationRooms = pgTable(
|
|||||||
entityId: bigint("entity_id", { mode: "number" }),
|
entityId: bigint("entity_id", { mode: "number" }),
|
||||||
entityUuid: uuid("entity_uuid"),
|
entityUuid: uuid("entity_uuid"),
|
||||||
|
|
||||||
matrixRoomId: text("matrix_room_id"),
|
|
||||||
matrixAlias: text("matrix_alias"),
|
|
||||||
parentSpaceRoomId: text("parent_space_room_id"),
|
|
||||||
|
|
||||||
archived: boolean("archived").notNull().default(false),
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
createdAt: timestamp("created_at", { withTimezone: true })
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export * from "./checkexecutions"
|
|||||||
export * from "./checks"
|
export * from "./checks"
|
||||||
export * from "./citys"
|
export * from "./citys"
|
||||||
export * from "./communication_rooms"
|
export * from "./communication_rooms"
|
||||||
|
export * from "./communication_room_members"
|
||||||
|
export * from "./communication_messages"
|
||||||
|
export * from "./communication_room_reads"
|
||||||
export * from "./contacts"
|
export * from "./contacts"
|
||||||
export * from "./contracts"
|
export * from "./contracts"
|
||||||
export * from "./contracttypes"
|
export * from "./contracttypes"
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import {loadSecrets, secrets} from "./utils/secrets";
|
|||||||
import {initMailer} from "./utils/mailer"
|
import {initMailer} from "./utils/mailer"
|
||||||
import {initS3} from "./utils/s3";
|
import {initS3} from "./utils/s3";
|
||||||
import { runBootstrap } from "./modules/bootstrap.service";
|
import { runBootstrap } from "./modules/bootstrap.service";
|
||||||
import { startMatrixPushWorker } from "./modules/matrix-push-worker.service";
|
|
||||||
import { startCentralServicesHeartbeat } from "./modules/central-services-heartbeat.service";
|
import { startCentralServicesHeartbeat } from "./modules/central-services-heartbeat.service";
|
||||||
import { startDocumentImportWorker } from "./modules/document-import/document-import.worker";
|
import { startDocumentImportWorker } from "./modules/document-import/document-import.worker";
|
||||||
|
|
||||||
@@ -89,7 +88,6 @@ async function main() {
|
|||||||
await app.register(dbPlugin);
|
await app.register(dbPlugin);
|
||||||
await app.register(servicesPlugin);
|
await app.register(servicesPlugin);
|
||||||
await runBootstrap(app);
|
await runBootstrap(app);
|
||||||
startMatrixPushWorker(app);
|
|
||||||
startCentralServicesHeartbeat(app);
|
startCentralServicesHeartbeat(app);
|
||||||
startDocumentImportWorker(app);
|
startDocumentImportWorker(app);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
tenants,
|
tenants,
|
||||||
texttemplates,
|
texttemplates,
|
||||||
} from "../../db/schema"
|
} from "../../db/schema"
|
||||||
import { matrixService } from "./matrix.service"
|
|
||||||
|
|
||||||
const adminPermissions = [
|
const adminPermissions = [
|
||||||
"mcp.tokens.write",
|
"mcp.tokens.write",
|
||||||
@@ -456,18 +455,4 @@ export async function runBootstrap(server: FastifyInstance) {
|
|||||||
await ensureTenantBaseData(server, tenant.id, adminUser.id)
|
await ensureTenantBaseData(server, tenant.id, adminUser.id)
|
||||||
console.log("✅ Bootstrap-Grunddaten geprüft")
|
console.log("✅ Bootstrap-Grunddaten geprüft")
|
||||||
|
|
||||||
if (process.env.FEDEO_BOOTSTRAP_MATRIX === "true") {
|
|
||||||
try {
|
|
||||||
const matrix = matrixService(server)
|
|
||||||
await matrix.provisionTenantRoom(adminUser.id, tenant.id, {
|
|
||||||
key: "allgemein",
|
|
||||||
name: "Allgemeiner Chat",
|
|
||||||
type: "general",
|
|
||||||
})
|
|
||||||
console.log("✅ Bootstrap-Matrix-Kommunikation geprüft")
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Bootstrap-Matrix-Kommunikation fehlgeschlagen:", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,377 +0,0 @@
|
|||||||
import { createHash } from "node:crypto"
|
|
||||||
import type { FastifyInstance } from "fastify"
|
|
||||||
import { and, desc, eq, inArray, isNotNull, ne } from "drizzle-orm"
|
|
||||||
import { authProfiles, authTenantUsers, authUsers, communicationRooms, notificationsItems } from "../../db/schema"
|
|
||||||
import { matrixService } from "./matrix.service"
|
|
||||||
import { NotificationService, UserDirectory } from "./notification.service"
|
|
||||||
|
|
||||||
type ChatRecipient = {
|
|
||||||
userId: string
|
|
||||||
email?: string | null
|
|
||||||
firstName?: string | null
|
|
||||||
lastName?: string | null
|
|
||||||
fullName?: string | null
|
|
||||||
matrixUserId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MatrixPushWorkerEvent = {
|
|
||||||
at: string
|
|
||||||
type: string
|
|
||||||
roomKey?: string
|
|
||||||
roomId?: string | null
|
|
||||||
messageId?: string
|
|
||||||
sender?: string
|
|
||||||
targets?: number
|
|
||||||
created?: number
|
|
||||||
delivered?: number
|
|
||||||
failed?: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const matrixPushWorkerState = {
|
|
||||||
enabled: false,
|
|
||||||
startedAt: null as string | null,
|
|
||||||
lastRunAt: null as string | null,
|
|
||||||
lastJoinAt: null as string | null,
|
|
||||||
lastJoinTotal: 0,
|
|
||||||
lastJoinJoined: 0,
|
|
||||||
lastJoinFailed: 0,
|
|
||||||
hasSyncToken: false,
|
|
||||||
lastSyncRooms: 0,
|
|
||||||
lastSyncMessages: 0,
|
|
||||||
lastMatchedRooms: 0,
|
|
||||||
lastNotificationsCreated: 0,
|
|
||||||
lastNotificationsDelivered: 0,
|
|
||||||
lastNotificationsFailed: 0,
|
|
||||||
lastError: null as string | null,
|
|
||||||
events: [] as MatrixPushWorkerEvent[],
|
|
||||||
}
|
|
||||||
|
|
||||||
const rememberWorkerEvent = (event: MatrixPushWorkerEvent) => {
|
|
||||||
matrixPushWorkerState.events = [
|
|
||||||
{
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
...event,
|
|
||||||
},
|
|
||||||
...matrixPushWorkerState.events,
|
|
||||||
].slice(0, 25)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMatrixPushWorkerState = () => ({
|
|
||||||
...matrixPushWorkerState,
|
|
||||||
events: [...matrixPushWorkerState.events],
|
|
||||||
})
|
|
||||||
|
|
||||||
const getUserDirectory: UserDirectory = async (server: FastifyInstance, userId) => {
|
|
||||||
const rows = await server.db
|
|
||||||
.select({ email: authUsers.email })
|
|
||||||
.from(authUsers)
|
|
||||||
.where(eq(authUsers.id, userId))
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
return rows[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayUserName = (user: { fullName?: string | null; firstName?: string | null; lastName?: string | null; email?: string | null }) => {
|
|
||||||
const name = user.fullName || [user.firstName, user.lastName].filter(Boolean).join(" ")
|
|
||||||
return name || user.email || "Benutzer"
|
|
||||||
}
|
|
||||||
|
|
||||||
const directRoomKey = (firstUserId: string, secondUserId: string) => {
|
|
||||||
const hash = createHash("sha256")
|
|
||||||
.update([firstUserId, secondUserId].sort().join(":"))
|
|
||||||
.digest("hex")
|
|
||||||
.slice(0, 16)
|
|
||||||
|
|
||||||
return `direct_${hash}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const mentionAliasesForUser = (user: ChatRecipient) => {
|
|
||||||
const name = displayUserName(user)
|
|
||||||
return Array.from(new Set([
|
|
||||||
name,
|
|
||||||
user.fullName,
|
|
||||||
[user.firstName, user.lastName].filter(Boolean).join(" "),
|
|
||||||
user.firstName,
|
|
||||||
user.email,
|
|
||||||
].filter(Boolean).map((value) => String(value).toLowerCase())))
|
|
||||||
}
|
|
||||||
|
|
||||||
const mentionedRecipientIds = (text: string, recipients: ChatRecipient[]) => {
|
|
||||||
const normalizedText = text.toLowerCase()
|
|
||||||
|
|
||||||
return recipients
|
|
||||||
.filter((recipient) => mentionAliasesForUser(recipient).some((alias) =>
|
|
||||||
normalizedText.includes(`@${alias}`)
|
|
||||||
))
|
|
||||||
.map((recipient) => recipient.userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startMatrixPushWorker(server: FastifyInstance) {
|
|
||||||
if (process.env.MATRIX_PUSH_WORKER_DISABLED === "1") {
|
|
||||||
server.log.info("Matrix-Push-Worker ist deaktiviert")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
matrixPushWorkerState.enabled = true
|
|
||||||
matrixPushWorkerState.startedAt = new Date().toISOString()
|
|
||||||
rememberWorkerEvent({ at: new Date().toISOString(), type: "started" })
|
|
||||||
|
|
||||||
const matrix = matrixService(server)
|
|
||||||
const notifications = new NotificationService(server, getUserDirectory)
|
|
||||||
const intervalMs = Math.max(Number(process.env.MATRIX_PUSH_WORKER_INTERVAL_MS || 3000), 1000)
|
|
||||||
let since: string | undefined
|
|
||||||
let running = false
|
|
||||||
let stopped = false
|
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
|
||||||
let lastServiceJoinSyncAt = 0
|
|
||||||
let errorBackoffMs = 0
|
|
||||||
|
|
||||||
const getTenantRecipients = async (tenantId: number) => {
|
|
||||||
const rows = await server.db
|
|
||||||
.select({
|
|
||||||
userId: authTenantUsers.user_id,
|
|
||||||
email: authUsers.email,
|
|
||||||
firstName: authProfiles.first_name,
|
|
||||||
lastName: authProfiles.last_name,
|
|
||||||
fullName: authProfiles.full_name,
|
|
||||||
})
|
|
||||||
.from(authTenantUsers)
|
|
||||||
.innerJoin(authUsers, eq(authUsers.id, authTenantUsers.user_id))
|
|
||||||
.leftJoin(authProfiles, and(
|
|
||||||
eq(authProfiles.user_id, authTenantUsers.user_id),
|
|
||||||
eq(authProfiles.tenant_id, tenantId)
|
|
||||||
))
|
|
||||||
.where(eq(authTenantUsers.tenant_id, tenantId))
|
|
||||||
|
|
||||||
return await Promise.all(rows.map(async (row) => ({
|
|
||||||
...row,
|
|
||||||
matrixUserId: await matrix.matrixUserIdForUser(row.userId, tenantId),
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChatNotificationForMessage = async (tenantId: number, userId: string, messageId: string) => {
|
|
||||||
const rows = await server.db
|
|
||||||
.select({
|
|
||||||
payload: notificationsItems.payload,
|
|
||||||
})
|
|
||||||
.from(notificationsItems)
|
|
||||||
.where(and(
|
|
||||||
eq(notificationsItems.tenantId, tenantId),
|
|
||||||
eq(notificationsItems.userId, userId),
|
|
||||||
eq(notificationsItems.eventType, "communication.message.new")
|
|
||||||
))
|
|
||||||
.orderBy(desc(notificationsItems.createdAt))
|
|
||||||
.limit(200)
|
|
||||||
|
|
||||||
return rows.some((row) => (row.payload as any)?.messageId === messageId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsForMessage = (
|
|
||||||
room: typeof communicationRooms.$inferSelect,
|
|
||||||
recipients: ChatRecipient[],
|
|
||||||
senderUserId: string | null,
|
|
||||||
text: string
|
|
||||||
) => {
|
|
||||||
const candidates = senderUserId
|
|
||||||
? recipients.filter((recipient) => recipient.userId !== senderUserId)
|
|
||||||
: recipients
|
|
||||||
const mentioned = new Set(mentionedRecipientIds(text, candidates))
|
|
||||||
const directRecipients = new Set<string>()
|
|
||||||
|
|
||||||
if (room.type === "direct" && room.entityUuid && room.entityUuid !== senderUserId) {
|
|
||||||
directRecipients.add(room.entityUuid)
|
|
||||||
} else if (room.type === "direct" && senderUserId) {
|
|
||||||
candidates
|
|
||||||
.filter((recipient) => directRoomKey(senderUserId, recipient.userId) === room.key)
|
|
||||||
.forEach((recipient) => directRecipients.add(recipient.userId))
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates
|
|
||||||
.filter((recipient) => directRecipients.has(recipient.userId) || mentioned.has(recipient.userId))
|
|
||||||
.map((recipient) => ({
|
|
||||||
...recipient,
|
|
||||||
mentioned: mentioned.has(recipient.userId),
|
|
||||||
direct: directRecipients.has(recipient.userId),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const deliverMessageNotification = async (
|
|
||||||
room: typeof communicationRooms.$inferSelect,
|
|
||||||
message: any,
|
|
||||||
recipients: ChatRecipient[]
|
|
||||||
) => {
|
|
||||||
if (!message.id || message.own) return
|
|
||||||
|
|
||||||
const sender = recipients.find((recipient) => recipient.matrixUserId === message.sender) || null
|
|
||||||
const text = message.body || message.attachment?.fileName || "Neue Nachricht"
|
|
||||||
const targets = recipientsForMessage(room, recipients, sender?.userId || null, text)
|
|
||||||
rememberWorkerEvent({
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
type: "message_seen",
|
|
||||||
roomKey: room.key,
|
|
||||||
roomId: room.matrixRoomId,
|
|
||||||
messageId: message.id,
|
|
||||||
sender: message.sender,
|
|
||||||
targets: targets.length,
|
|
||||||
})
|
|
||||||
if (!targets.length) return
|
|
||||||
|
|
||||||
const senderName = sender ? displayUserName(sender) : message.senderDisplayName || message.sender || "Matrix"
|
|
||||||
const preview = text.length > 160 ? `${text.slice(0, 157)}...` : text
|
|
||||||
|
|
||||||
for (const target of targets) {
|
|
||||||
if (await hasChatNotificationForMessage(room.tenantId, target.userId, message.id)) {
|
|
||||||
rememberWorkerEvent({
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
type: "notification_skipped_duplicate",
|
|
||||||
roomKey: room.key,
|
|
||||||
roomId: room.matrixRoomId,
|
|
||||||
messageId: message.id,
|
|
||||||
sender: message.sender,
|
|
||||||
targets: 1,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await notifications.trigger({
|
|
||||||
tenantId: room.tenantId,
|
|
||||||
userId: target.userId,
|
|
||||||
eventType: "communication.message.new",
|
|
||||||
title: target.mentioned ? `${senderName} hat dich erwähnt` : `Neue Direktnachricht von ${senderName}`,
|
|
||||||
message: preview,
|
|
||||||
payload: {
|
|
||||||
link: `/communication/chat?room=${encodeURIComponent(room.key)}`,
|
|
||||||
roomKey: room.key,
|
|
||||||
roomName: room.name,
|
|
||||||
roomType: room.type,
|
|
||||||
messageId: message.id,
|
|
||||||
matrixSender: message.sender,
|
|
||||||
mentioned: target.mentioned,
|
|
||||||
direct: target.direct,
|
|
||||||
},
|
|
||||||
channels: ["inapp", "push"],
|
|
||||||
})
|
|
||||||
matrixPushWorkerState.lastNotificationsCreated += result.created || 0
|
|
||||||
matrixPushWorkerState.lastNotificationsDelivered += result.delivered || 0
|
|
||||||
matrixPushWorkerState.lastNotificationsFailed += result.failed || 0
|
|
||||||
rememberWorkerEvent({
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
type: "notification_triggered",
|
|
||||||
roomKey: room.key,
|
|
||||||
roomId: room.matrixRoomId,
|
|
||||||
messageId: message.id,
|
|
||||||
sender: message.sender,
|
|
||||||
targets: 1,
|
|
||||||
created: result.created || 0,
|
|
||||||
delivered: result.delivered || 0,
|
|
||||||
failed: result.failed || 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runOnce = async () => {
|
|
||||||
if (running || stopped) return
|
|
||||||
running = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
matrixPushWorkerState.lastRunAt = new Date().toISOString()
|
|
||||||
matrixPushWorkerState.lastError = null
|
|
||||||
matrixPushWorkerState.lastSyncRooms = 0
|
|
||||||
matrixPushWorkerState.lastSyncMessages = 0
|
|
||||||
matrixPushWorkerState.lastMatchedRooms = 0
|
|
||||||
matrixPushWorkerState.lastNotificationsCreated = 0
|
|
||||||
matrixPushWorkerState.lastNotificationsDelivered = 0
|
|
||||||
matrixPushWorkerState.lastNotificationsFailed = 0
|
|
||||||
|
|
||||||
if (!lastServiceJoinSyncAt || Date.now() - lastServiceJoinSyncAt > 60_000) {
|
|
||||||
const joinResult = await matrix.syncServiceJoinedTenantRooms()
|
|
||||||
lastServiceJoinSyncAt = Date.now()
|
|
||||||
matrixPushWorkerState.lastJoinAt = new Date().toISOString()
|
|
||||||
matrixPushWorkerState.lastJoinTotal = joinResult.total
|
|
||||||
matrixPushWorkerState.lastJoinJoined = joinResult.joined
|
|
||||||
matrixPushWorkerState.lastJoinFailed = joinResult.failed
|
|
||||||
rememberWorkerEvent({
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
type: "service_join_sync",
|
|
||||||
targets: joinResult.total,
|
|
||||||
delivered: joinResult.joined,
|
|
||||||
failed: joinResult.failed,
|
|
||||||
})
|
|
||||||
if (joinResult.failed) {
|
|
||||||
console.warn("Matrix-Push-Worker: Service-User konnte nicht alle Räume joinen", {
|
|
||||||
total: joinResult.total,
|
|
||||||
joined: joinResult.joined,
|
|
||||||
failed: joinResult.failed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initial = !since
|
|
||||||
const sync = await matrix.syncServiceRoomEvents(since, initial)
|
|
||||||
since = sync.nextBatch || since
|
|
||||||
matrixPushWorkerState.hasSyncToken = Boolean(since)
|
|
||||||
matrixPushWorkerState.lastSyncRooms = sync.rooms?.length || 0
|
|
||||||
matrixPushWorkerState.lastSyncMessages = (sync.rooms || [])
|
|
||||||
.reduce((sum: number, room: any) => sum + (room.messages?.length || 0), 0)
|
|
||||||
|
|
||||||
if (!initial && sync.rooms?.length) {
|
|
||||||
const roomIds = sync.rooms.map((room: any) => room.roomId).filter(Boolean)
|
|
||||||
const rooms = roomIds.length
|
|
||||||
? await server.db
|
|
||||||
.select()
|
|
||||||
.from(communicationRooms)
|
|
||||||
.where(and(
|
|
||||||
inArray(communicationRooms.matrixRoomId, roomIds),
|
|
||||||
ne(communicationRooms.archived, true),
|
|
||||||
isNotNull(communicationRooms.matrixRoomId)
|
|
||||||
))
|
|
||||||
: []
|
|
||||||
const roomsByMatrixId = new Map(rooms.map((room) => [room.matrixRoomId, room]))
|
|
||||||
matrixPushWorkerState.lastMatchedRooms = rooms.length
|
|
||||||
const recipientsByTenant = new Map<number, ChatRecipient[]>()
|
|
||||||
|
|
||||||
for (const syncedRoom of sync.rooms) {
|
|
||||||
const room = roomsByMatrixId.get(syncedRoom.roomId)
|
|
||||||
if (!room || !syncedRoom.messages?.length) continue
|
|
||||||
|
|
||||||
if (!recipientsByTenant.has(room.tenantId)) {
|
|
||||||
recipientsByTenant.set(room.tenantId, await getTenantRecipients(room.tenantId))
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = recipientsByTenant.get(room.tenantId) || []
|
|
||||||
for (const message of syncedRoom.messages) {
|
|
||||||
await deliverMessageNotification(room, message, recipients)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errorBackoffMs = 0
|
|
||||||
} catch (err) {
|
|
||||||
matrixPushWorkerState.lastError = err instanceof Error ? err.message : String(err)
|
|
||||||
const retryAfterMs = Number((err as any)?.retryAfterMs || (err as any)?.body?.retry_after_ms || 0)
|
|
||||||
errorBackoffMs = Math.min(
|
|
||||||
Math.max(retryAfterMs || (errorBackoffMs ? errorBackoffMs * 2 : 30_000), 30_000),
|
|
||||||
5 * 60_000
|
|
||||||
)
|
|
||||||
rememberWorkerEvent({
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
type: "error",
|
|
||||||
error: matrixPushWorkerState.lastError,
|
|
||||||
})
|
|
||||||
console.error("Matrix-Push-Worker konnte Matrix-Events nicht verarbeiten", err)
|
|
||||||
server.log.error({ err }, "Matrix-Push-Worker konnte Matrix-Events nicht verarbeiten")
|
|
||||||
} finally {
|
|
||||||
running = false
|
|
||||||
if (!stopped) {
|
|
||||||
const nextDelay = errorBackoffMs || (since ? 0 : intervalMs)
|
|
||||||
timer = setTimeout(() => void runOnce(), nextDelay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timer = setTimeout(() => void runOnce(), intervalMs)
|
|
||||||
server.addHook("onClose", async () => {
|
|
||||||
stopped = true
|
|
||||||
if (timer) clearTimeout(timer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { matrixService } from "./matrix.service"
|
|
||||||
|
|
||||||
type MetricSample = {
|
type MetricSample = {
|
||||||
labels: Record<string, string>
|
labels: Record<string, string>
|
||||||
@@ -117,10 +116,6 @@ export const buildSystemStatus = async (server: FastifyInstance) => {
|
|||||||
const uname = nodeMetrics?.get("node_uname_info")?.[0]?.labels || null
|
const uname = nodeMetrics?.get("node_uname_info")?.[0]?.labels || null
|
||||||
|
|
||||||
const databaseCheck = await server.db.execute("SELECT NOW() as now")
|
const databaseCheck = await server.db.execute("SELECT NOW() as now")
|
||||||
const matrixStatus = await matrixService(server).getStatus().catch((err: any) => ({
|
|
||||||
reachable: false,
|
|
||||||
error: err?.message || "Matrix-Status nicht verfügbar",
|
|
||||||
}))
|
|
||||||
const minioUrl = s3EndpointUrl()
|
const minioUrl = s3EndpointUrl()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -165,7 +160,6 @@ export const buildSystemStatus = async (server: FastifyInstance) => {
|
|||||||
url: nodeExporterMetricsUrl,
|
url: nodeExporterMetricsUrl,
|
||||||
error: nodeExporterError,
|
error: nodeExporterError,
|
||||||
}),
|
}),
|
||||||
matrix: serviceState(Boolean((matrixStatus as any).reachable), matrixStatus as Record<string, any>),
|
|
||||||
minio: minioUrl ? await checkHttp(`${minioUrl}/minio/health/live`) : serviceState(false, {
|
minio: minioUrl ? await checkHttp(`${minioUrl}/minio/health/live`) : serviceState(false, {
|
||||||
error: "S3_ENDPOINT ist nicht gesetzt",
|
error: "S3_ENDPOINT ist nicht gesetzt",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
} from "../utils/tenantFullExport";
|
} from "../utils/tenantFullExport";
|
||||||
import type { TenantFullExport } from "../utils/tenantFullExport";
|
import type { TenantFullExport } from "../utils/tenantFullExport";
|
||||||
import { buildSystemStatus } from "../modules/system-status.service";
|
import { buildSystemStatus } from "../modules/system-status.service";
|
||||||
import { matrixService } from "../modules/matrix.service";
|
|
||||||
import { s3 } from "../utils/s3";
|
import { s3 } from "../utils/s3";
|
||||||
import { secrets } from "../utils/secrets";
|
import { secrets } from "../utils/secrets";
|
||||||
|
|
||||||
@@ -385,27 +384,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let matrixProvisioned = false;
|
return { chatReady: true };
|
||||||
let matrixProvisioningError: string | null = null;
|
|
||||||
if (process.env.MATRIX_REGISTRATION_SHARED_SECRET) {
|
|
||||||
try {
|
|
||||||
const matrix = matrixService(server);
|
|
||||||
await matrix.provisionTenantRoom(currentUser.id, result.tenantId, {
|
|
||||||
key: "allgemein",
|
|
||||||
name: "Allgemeiner Chat",
|
|
||||||
type: "general",
|
|
||||||
});
|
|
||||||
matrixProvisioned = true;
|
|
||||||
} catch (err: any) {
|
|
||||||
matrixProvisioningError = err?.message || String(err);
|
|
||||||
server.log.warn({ err }, "Matrix-Räume konnten nach Tenant-Import nicht neu provisioniert werden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
matrixProvisioned,
|
|
||||||
matrixProvisioningError,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startTenantExportJob = async (jobId: string, tenantId: number, filename: string) => {
|
const startTenantExportJob = async (jobId: string, tenantId: number, filename: string) => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -38,15 +38,6 @@ export let secrets = {
|
|||||||
DOKUBOX_IMAP_PASSWORD: string
|
DOKUBOX_IMAP_PASSWORD: string
|
||||||
OPENAI_API_KEY: string
|
OPENAI_API_KEY: string
|
||||||
STIRLING_API_KEY: string
|
STIRLING_API_KEY: string
|
||||||
MATRIX_HOMESERVER_URL?: string
|
|
||||||
MATRIX_SERVER_NAME?: string
|
|
||||||
MATRIX_RTC_HOST?: string
|
|
||||||
MATRIX_RTC_JWT_URL?: string
|
|
||||||
MATRIX_LIVEKIT_URL?: string
|
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET?: string
|
|
||||||
MATRIX_SERVICE_USER_LOCALPART?: string
|
|
||||||
LIVEKIT_KEY?: string
|
|
||||||
LIVEKIT_SECRET?: string
|
|
||||||
WEB_PUSH_PUBLIC_KEY?: string
|
WEB_PUSH_PUBLIC_KEY?: string
|
||||||
WEB_PUSH_PRIVATE_KEY?: string
|
WEB_PUSH_PRIVATE_KEY?: string
|
||||||
WEB_PUSH_SUBJECT?: string
|
WEB_PUSH_SUBJECT?: string
|
||||||
@@ -88,15 +79,6 @@ const secretKeys = [
|
|||||||
"DOKUBOX_IMAP_PASSWORD",
|
"DOKUBOX_IMAP_PASSWORD",
|
||||||
"OPENAI_API_KEY",
|
"OPENAI_API_KEY",
|
||||||
"STIRLING_API_KEY",
|
"STIRLING_API_KEY",
|
||||||
"MATRIX_HOMESERVER_URL",
|
|
||||||
"MATRIX_SERVER_NAME",
|
|
||||||
"MATRIX_RTC_HOST",
|
|
||||||
"MATRIX_RTC_JWT_URL",
|
|
||||||
"MATRIX_LIVEKIT_URL",
|
|
||||||
"MATRIX_REGISTRATION_SHARED_SECRET",
|
|
||||||
"MATRIX_SERVICE_USER_LOCALPART",
|
|
||||||
"LIVEKIT_KEY",
|
|
||||||
"LIVEKIT_SECRET",
|
|
||||||
"WEB_PUSH_PUBLIC_KEY",
|
"WEB_PUSH_PUBLIC_KEY",
|
||||||
"WEB_PUSH_PRIVATE_KEY",
|
"WEB_PUSH_PRIVATE_KEY",
|
||||||
"WEB_PUSH_SUBJECT",
|
"WEB_PUSH_SUBJECT",
|
||||||
|
|||||||
@@ -86,47 +86,6 @@ const ENTITY_BANKACCOUNT_PLAIN_FIELDS = {
|
|||||||
const GLOBAL_MIGRATION_TABLES = new Set(["accounts", "units", "citys", "countrys"])
|
const GLOBAL_MIGRATION_TABLES = new Set(["accounts", "units", "citys", "countrys"])
|
||||||
|
|
||||||
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
|
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
|
||||||
const matrixServerName = () =>
|
|
||||||
process.env.MATRIX_SERVER_NAME ||
|
|
||||||
secrets.MATRIX_SERVER_NAME ||
|
|
||||||
process.env.DOMAIN ||
|
|
||||||
"localhost"
|
|
||||||
|
|
||||||
const normalizeMatrixLocalpartSeed = (value: string) => {
|
|
||||||
const normalized = value
|
|
||||||
.toLowerCase()
|
|
||||||
.normalize("NFKD")
|
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
|
||||||
.replace(/ä/g, "a")
|
|
||||||
.replace(/ö/g, "o")
|
|
||||||
.replace(/ü/g, "u")
|
|
||||||
.replace(/ß/g, "ss")
|
|
||||||
.replace(/[^a-z0-9._=-]+/g, "_")
|
|
||||||
.replace(/_+/g, "_")
|
|
||||||
.replace(/^[._=-]+|[._=-]+$/g, "")
|
|
||||||
|
|
||||||
return normalized || "user"
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeMatrixAliasSeed = (value: string) =>
|
|
||||||
normalizeMatrixLocalpartSeed(value)
|
|
||||||
.replace(/[.=]/g, "_")
|
|
||||||
.replace(/_+/g, "_")
|
|
||||||
|
|
||||||
const tenantRoomAliasLocalpart = (
|
|
||||||
tenant: { id: number, short?: string | null, name?: string | null },
|
|
||||||
roomKey: string
|
|
||||||
) => {
|
|
||||||
const tenantSeed = normalizeMatrixAliasSeed(tenant.short || tenant.name || `tenant_${tenant.id}`)
|
|
||||||
const roomSeed = normalizeMatrixAliasSeed(roomKey)
|
|
||||||
return `fedeo_${tenantSeed}_${tenant.id}_${roomSeed}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenantRoomAlias = (
|
|
||||||
tenant: { id: number, short?: string | null, name?: string | null },
|
|
||||||
roomKey: string
|
|
||||||
) => `#${tenantRoomAliasLocalpart(tenant, roomKey)}:${matrixServerName()}`
|
|
||||||
|
|
||||||
const tableColumns = async (client: any) => {
|
const tableColumns = async (client: any) => {
|
||||||
const result = await client.query(`
|
const result = await client.query(`
|
||||||
select table_name, column_name, data_type, is_generated
|
select table_name, column_name, data_type, is_generated
|
||||||
@@ -343,6 +302,12 @@ export const buildTenantFullExport = async (
|
|||||||
addRows(tables, "auth_profile_teams", await loadRows(client, "auth_profile_teams", "profile_id = any($1::uuid[])", [profileIds]))
|
addRows(tables, "auth_profile_teams", await loadRows(client, "auth_profile_teams", "profile_id = any($1::uuid[])", [profileIds]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const communicationRoomIds = collectIds(tables.communication_rooms || [], "id")
|
||||||
|
if (communicationRoomIds.length) {
|
||||||
|
addRows(tables, "communication_room_members", await loadRows(client, "communication_room_members", "room_id = any($1::uuid[])", [communicationRoomIds]))
|
||||||
|
addRows(tables, "communication_room_reads", await loadRows(client, "communication_room_reads", "room_id = any($1::uuid[])", [communicationRoomIds]))
|
||||||
|
}
|
||||||
|
|
||||||
if (tables.entitybankaccounts?.length) {
|
if (tables.entitybankaccounts?.length) {
|
||||||
tables.entitybankaccounts = decryptEntityBankAccountsForExport(tables.entitybankaccounts)
|
tables.entitybankaccounts = decryptEntityBankAccountsForExport(tables.entitybankaccounts)
|
||||||
}
|
}
|
||||||
@@ -636,73 +601,6 @@ const encryptEntityBankAccountRowsForImport = (exportData: TenantFullExport) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const prepareCommunicationRoomsForImport = (exportData: TenantFullExport) => {
|
|
||||||
const rows = exportData.tables.communication_rooms || []
|
|
||||||
if (!rows.length) return
|
|
||||||
|
|
||||||
const tenantById = new Map((exportData.tables.tenants || []).map((tenant) => [
|
|
||||||
Number(tenant.id),
|
|
||||||
{
|
|
||||||
id: Number(tenant.id),
|
|
||||||
name: tenant.name,
|
|
||||||
short: tenant.short,
|
|
||||||
},
|
|
||||||
]))
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const tenantId = Number(row.tenant_id)
|
|
||||||
const tenant = tenantById.get(tenantId)
|
|
||||||
|
|
||||||
row.matrix_room_id = null
|
|
||||||
row.parent_space_room_id = null
|
|
||||||
|
|
||||||
if (tenant && row.key) {
|
|
||||||
row.matrix_alias = tenantRoomAlias(tenant, String(row.key))
|
|
||||||
} else {
|
|
||||||
row.matrix_alias = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupImportedCommunicationRooms = async (client: any, exportData: TenantFullExport) => {
|
|
||||||
const rows = exportData.tables.communication_rooms || []
|
|
||||||
if (!rows.length) return 0
|
|
||||||
|
|
||||||
const tenantById = new Map((exportData.tables.tenants || []).map((tenant) => [
|
|
||||||
Number(tenant.id),
|
|
||||||
{
|
|
||||||
id: Number(tenant.id),
|
|
||||||
name: tenant.name,
|
|
||||||
short: tenant.short,
|
|
||||||
},
|
|
||||||
]))
|
|
||||||
let cleaned = 0
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const tenantId = Number(row.tenant_id)
|
|
||||||
const key = String(row.key || "")
|
|
||||||
const tenant = tenantById.get(tenantId)
|
|
||||||
if (!tenantId || !key || !tenant) continue
|
|
||||||
|
|
||||||
const alias = tenantRoomAlias(tenant, key)
|
|
||||||
const result = await client.query(
|
|
||||||
`
|
|
||||||
update communication_rooms
|
|
||||||
set matrix_room_id = null,
|
|
||||||
parent_space_room_id = null,
|
|
||||||
matrix_alias = $3,
|
|
||||||
updated_at = now()
|
|
||||||
where tenant_id = $1 and key = $2
|
|
||||||
`,
|
|
||||||
[tenantId, key, alias]
|
|
||||||
)
|
|
||||||
|
|
||||||
cleaned += result.rowCount || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned
|
|
||||||
}
|
|
||||||
|
|
||||||
const prepareColumnValue = (value: any, isJsonColumn: boolean) => {
|
const prepareColumnValue = (value: any, isJsonColumn: boolean) => {
|
||||||
if (!isJsonColumn || value === null || typeof value === "undefined") return value
|
if (!isJsonColumn || value === null || typeof value === "undefined") return value
|
||||||
if (typeof value === "string") return value
|
if (typeof value === "string") return value
|
||||||
@@ -853,7 +751,6 @@ export const importTenantFullExport = async (
|
|||||||
|
|
||||||
const exportData = rawExportData
|
const exportData = rawExportData
|
||||||
encryptEntityBankAccountRowsForImport(exportData)
|
encryptEntityBankAccountRowsForImport(exportData)
|
||||||
prepareCommunicationRoomsForImport(exportData)
|
|
||||||
const client = await pool.connect()
|
const client = await pool.connect()
|
||||||
const importOrder = [
|
const importOrder = [
|
||||||
"tenants",
|
"tenants",
|
||||||
@@ -955,13 +852,6 @@ export const importTenantFullExport = async (
|
|||||||
await reportProgress(`${table} importiert`)
|
await reportProgress(`${table} importiert`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanedCommunicationRooms = await cleanupImportedCommunicationRooms(client, exportData)
|
|
||||||
if (cleanedCommunicationRooms) {
|
|
||||||
importedTables.push({ table: "communication_rooms_matrix_reset", rows: cleanedCommunicationRooms })
|
|
||||||
}
|
|
||||||
progressDone += 1
|
|
||||||
await reportProgress("Kommunikationsräume bereinigt")
|
|
||||||
|
|
||||||
await refreshSequences(client, columnsByTable)
|
await refreshSequences(client, columnsByTable)
|
||||||
progressDone = progressTotal
|
progressDone = progressTotal
|
||||||
await reportProgress("Import abgeschlossen")
|
await reportProgress("Import abgeschlossen")
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
createbuckets:
|
createbuckets:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
matrix-synapse:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
FEDEO_RUN_MIGRATIONS: ${FEDEO_RUN_MIGRATIONS:-true}
|
FEDEO_RUN_MIGRATIONS: ${FEDEO_RUN_MIGRATIONS:-true}
|
||||||
@@ -144,16 +142,6 @@ services:
|
|||||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME: ${FEDEO_BOOTSTRAP_ADMIN_LAST_NAME:-Benutzer}
|
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME: ${FEDEO_BOOTSTRAP_ADMIN_LAST_NAME:-Benutzer}
|
||||||
FEDEO_BOOTSTRAP_TENANT_NAME: ${FEDEO_BOOTSTRAP_TENANT_NAME:-FEDEO}
|
FEDEO_BOOTSTRAP_TENANT_NAME: ${FEDEO_BOOTSTRAP_TENANT_NAME:-FEDEO}
|
||||||
FEDEO_BOOTSTRAP_TENANT_SHORT: ${FEDEO_BOOTSTRAP_TENANT_SHORT:-FEDEO}
|
FEDEO_BOOTSTRAP_TENANT_SHORT: ${FEDEO_BOOTSTRAP_TENANT_SHORT:-FEDEO}
|
||||||
FEDEO_BOOTSTRAP_MATRIX: ${FEDEO_BOOTSTRAP_MATRIX:-true}
|
|
||||||
MATRIX_HOMESERVER_URL: ${MATRIX_HOMESERVER_URL:-http://matrix-synapse:8008}
|
|
||||||
MATRIX_SERVER_NAME: ${MATRIX_SERVER_NAME:-${DOMAIN}}
|
|
||||||
MATRIX_RTC_HOST: ${MATRIX_RTC_HOST:-${DOMAIN}}
|
|
||||||
MATRIX_RTC_JWT_URL: ${MATRIX_RTC_JWT_URL:-}
|
|
||||||
MATRIX_LIVEKIT_URL: ${MATRIX_LIVEKIT_URL:-}
|
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET: ${MATRIX_REGISTRATION_SHARED_SECRET:-change-this-matrix-registration-secret}
|
|
||||||
MATRIX_SERVICE_USER_LOCALPART: ${MATRIX_SERVICE_USER_LOCALPART:-fedeo_service}
|
|
||||||
LIVEKIT_KEY: ${LIVEKIT_KEY:-fedeo-livekit}
|
|
||||||
LIVEKIT_SECRET: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
|
||||||
NODE_EXPORTER_URL: ${NODE_EXPORTER_URL:-http://node-exporter:9100}
|
NODE_EXPORTER_URL: ${NODE_EXPORTER_URL:-http://node-exporter:9100}
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
@@ -195,7 +183,6 @@ services:
|
|||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
NUXT_PUBLIC_API_BASE: https://${DOMAIN}/backend
|
NUXT_PUBLIC_API_BASE: https://${DOMAIN}/backend
|
||||||
NUXT_PUBLIC_PDF_LICENSE: ${NUXT_PUBLIC_PDF_LICENSE}
|
NUXT_PUBLIC_PDF_LICENSE: ${NUXT_PUBLIC_PDF_LICENSE}
|
||||||
NUXT_PUBLIC_MATRIX_ELEMENT_URL: ${NUXT_PUBLIC_MATRIX_ELEMENT_URL:-}
|
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`)
|
- traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`)
|
||||||
@@ -207,306 +194,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
matrix-db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: fedeo-matrix-db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: ${MATRIX_POSTGRES_DB:-synapse}
|
|
||||||
POSTGRES_USER: ${MATRIX_POSTGRES_USER:-synapse}
|
|
||||||
POSTGRES_PASSWORD: ${MATRIX_POSTGRES_PASSWORD:-change-this-matrix-db-password}
|
|
||||||
POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate=C --lc-ctype=C
|
|
||||||
volumes:
|
|
||||||
- ./matrix/postgres:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${MATRIX_POSTGRES_USER:-synapse} -d ${MATRIX_POSTGRES_DB:-synapse}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
matrix-redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: fedeo-matrix-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
matrix-synapse:
|
|
||||||
image: ghcr.io/element-hq/synapse:latest
|
|
||||||
container_name: fedeo-matrix-synapse
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
matrix-db:
|
|
||||||
condition: service_healthy
|
|
||||||
matrix-redis:
|
|
||||||
condition: service_started
|
|
||||||
environment:
|
|
||||||
DOMAIN: ${DOMAIN}
|
|
||||||
MATRIX_POSTGRES_DB: ${MATRIX_POSTGRES_DB:-synapse}
|
|
||||||
MATRIX_POSTGRES_USER: ${MATRIX_POSTGRES_USER:-synapse}
|
|
||||||
MATRIX_POSTGRES_PASSWORD: ${MATRIX_POSTGRES_PASSWORD:-change-this-matrix-db-password}
|
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET: ${MATRIX_REGISTRATION_SHARED_SECRET:-change-this-matrix-registration-secret}
|
|
||||||
MATRIX_SERVER_NAME: ${MATRIX_SERVER_NAME:-${DOMAIN}}
|
|
||||||
MATRIX_TURN_SHARED_SECRET: ${MATRIX_TURN_SHARED_SECRET:-change-this-turn-secret}
|
|
||||||
SYNAPSE_CONFIG_PATH: /data/homeserver.yaml
|
|
||||||
SYNAPSE_REPORT_STATS: "no"
|
|
||||||
SYNAPSE_SERVER_NAME: ${MATRIX_SERVER_NAME:-${DOMAIN}}
|
|
||||||
entrypoint: /bin/sh
|
|
||||||
command:
|
|
||||||
- -ec
|
|
||||||
- |
|
|
||||||
if [ ! -f /data/homeserver.yaml ]; then
|
|
||||||
/start.py generate
|
|
||||||
fi
|
|
||||||
python - <<'PY'
|
|
||||||
import os
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
path = "/data/homeserver.yaml"
|
|
||||||
with open(path, "r", encoding="utf-8") as handle:
|
|
||||||
config = yaml.safe_load(handle) or {}
|
|
||||||
|
|
||||||
domain = os.environ["DOMAIN"]
|
|
||||||
server_name = os.environ.get("MATRIX_SERVER_NAME") or domain
|
|
||||||
config["server_name"] = server_name
|
|
||||||
config["public_baseurl"] = f"https://{domain}/"
|
|
||||||
config["database"] = {
|
|
||||||
"name": "psycopg2",
|
|
||||||
"args": {
|
|
||||||
"user": os.environ.get("MATRIX_POSTGRES_USER", "synapse"),
|
|
||||||
"password": os.environ["MATRIX_POSTGRES_PASSWORD"],
|
|
||||||
"database": os.environ.get("MATRIX_POSTGRES_DB", "synapse"),
|
|
||||||
"host": "matrix-db",
|
|
||||||
"cp_min": 5,
|
|
||||||
"cp_max": 10,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
config["redis"] = {"enabled": True, "host": "matrix-redis"}
|
|
||||||
config["registration_shared_secret"] = os.environ["MATRIX_REGISTRATION_SHARED_SECRET"]
|
|
||||||
config["turn_uris"] = [
|
|
||||||
f"turn:{domain}:3478?transport=udp",
|
|
||||||
f"turn:{domain}:3478?transport=tcp",
|
|
||||||
]
|
|
||||||
config["turn_shared_secret"] = os.environ["MATRIX_TURN_SHARED_SECRET"]
|
|
||||||
config["turn_user_lifetime"] = "1h"
|
|
||||||
config["enable_registration"] = False
|
|
||||||
config["experimental_features"] = {
|
|
||||||
**(config.get("experimental_features") or {}),
|
|
||||||
"msc3266_enabled": True,
|
|
||||||
"msc4222_enabled": True,
|
|
||||||
}
|
|
||||||
config["login_via_existing_session"] = {
|
|
||||||
"enabled": True,
|
|
||||||
"require_ui_auth": False,
|
|
||||||
"token_timeout": "5m",
|
|
||||||
}
|
|
||||||
config["max_event_delay_duration"] = "24h"
|
|
||||||
config["rc_message"] = {"per_second": 0.5, "burst_count": 30}
|
|
||||||
config["rc_delayed_event_mgmt"] = {"per_second": 1, "burst_count": 20}
|
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as handle:
|
|
||||||
yaml.safe_dump(config, handle, sort_keys=False)
|
|
||||||
PY
|
|
||||||
exec /start.py
|
|
||||||
volumes:
|
|
||||||
- ./matrix/synapse:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8008/_matrix/client/versions', timeout=2)\""]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
start_period: 20s
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.routers.fedeo-matrix.rule=Host(`${DOMAIN}`) && PathPrefix(`/_matrix`)
|
|
||||||
- traefik.http.routers.fedeo-matrix.entrypoints=websecure
|
|
||||||
- traefik.http.routers.fedeo-matrix.tls.certresolver=letsencrypt
|
|
||||||
- traefik.http.services.fedeo-matrix.loadbalancer.server.port=8008
|
|
||||||
- traefik.docker.network=fedeo_web
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
- internal
|
|
||||||
|
|
||||||
matrix-well-known:
|
|
||||||
image: nginx:1.27-alpine
|
|
||||||
container_name: fedeo-matrix-well-known
|
|
||||||
restart: unless-stopped
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -ec
|
|
||||||
- |
|
|
||||||
mkdir -p /usr/share/nginx/html/.well-known/matrix
|
|
||||||
cat >/usr/share/nginx/html/.well-known/matrix/client <<EOF
|
|
||||||
{
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "https://${DOMAIN}"
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "https://${DOMAIN}/livekit/jwt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
cat >/usr/share/nginx/html/.well-known/matrix/server <<EOF
|
|
||||||
{
|
|
||||||
"m.server": "${MATRIX_SERVER_NAME:-${DOMAIN}}:443"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
exec nginx -g 'daemon off;'
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolalloworiginlist=*
|
|
||||||
- traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolallowmethods=GET,OPTIONS
|
|
||||||
- traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolallowheaders=Content-Type,Authorization
|
|
||||||
- traefik.http.routers.fedeo-matrix-well-known.rule=Host(`${DOMAIN}`) && PathPrefix(`/.well-known/matrix`)
|
|
||||||
- traefik.http.routers.fedeo-matrix-well-known.entrypoints=websecure
|
|
||||||
- traefik.http.routers.fedeo-matrix-well-known.tls.certresolver=letsencrypt
|
|
||||||
- traefik.http.routers.fedeo-matrix-well-known.middlewares=fedeo-matrix-well-known-cors
|
|
||||||
- traefik.http.services.fedeo-matrix-well-known.loadbalancer.server.port=80
|
|
||||||
- traefik.docker.network=fedeo_web
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
|
|
||||||
matrix-turn:
|
|
||||||
image: instrumentisto/coturn:4
|
|
||||||
container_name: fedeo-matrix-turn
|
|
||||||
restart: unless-stopped
|
|
||||||
command:
|
|
||||||
- --fingerprint
|
|
||||||
- --use-auth-secret
|
|
||||||
- --static-auth-secret=${MATRIX_TURN_SHARED_SECRET:-change-this-turn-secret}
|
|
||||||
- --realm=${MATRIX_SERVER_NAME:-${DOMAIN}}
|
|
||||||
- --listening-port=3478
|
|
||||||
- --tls-listening-port=5349
|
|
||||||
- --min-port=49160
|
|
||||||
- --max-port=49200
|
|
||||||
- --no-cli
|
|
||||||
- --no-tlsv1
|
|
||||||
- --no-tlsv1_1
|
|
||||||
ports:
|
|
||||||
- "3478:3478/tcp"
|
|
||||||
- "3478:3478/udp"
|
|
||||||
- "5349:5349/tcp"
|
|
||||||
- "49160-49200:49160-49200/udp"
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
matrix-livekit:
|
|
||||||
image: livekit/livekit-server:v1.9
|
|
||||||
container_name: fedeo-matrix-livekit
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- matrix-redis
|
|
||||||
entrypoint: /bin/sh
|
|
||||||
command:
|
|
||||||
- -ec
|
|
||||||
- |
|
|
||||||
cat >/tmp/livekit.yaml <<EOF
|
|
||||||
port: 7880
|
|
||||||
redis:
|
|
||||||
address: matrix-redis:6379
|
|
||||||
rtc:
|
|
||||||
tcp_port: 7881
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 50100
|
|
||||||
use_external_ip: true
|
|
||||||
keys:
|
|
||||||
${LIVEKIT_KEY:-fedeo-livekit}: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
|
||||||
room:
|
|
||||||
auto_create: true
|
|
||||||
EOF
|
|
||||||
exec /livekit-server --config /tmp/livekit.yaml
|
|
||||||
ports:
|
|
||||||
- "7881:7881/tcp"
|
|
||||||
- "50000-50100:50000-50100/udp"
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.middlewares.fedeo-matrix-livekit-strip.stripprefix.prefixes=/livekit/sfu
|
|
||||||
- traefik.http.routers.fedeo-matrix-livekit.rule=Host(`${DOMAIN}`) && PathPrefix(`/livekit/sfu`)
|
|
||||||
- traefik.http.routers.fedeo-matrix-livekit.entrypoints=websecure
|
|
||||||
- traefik.http.routers.fedeo-matrix-livekit.tls.certresolver=letsencrypt
|
|
||||||
- traefik.http.routers.fedeo-matrix-livekit.middlewares=fedeo-matrix-livekit-strip
|
|
||||||
- traefik.http.services.fedeo-matrix-livekit.loadbalancer.server.port=7880
|
|
||||||
- traefik.docker.network=fedeo_web
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
- internal
|
|
||||||
|
|
||||||
matrix-rtc-jwt:
|
|
||||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
|
||||||
container_name: fedeo-matrix-rtc-jwt
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- matrix-livekit
|
|
||||||
- matrix-synapse
|
|
||||||
environment:
|
|
||||||
LIVEKIT_URL: wss://${DOMAIN}/livekit/sfu
|
|
||||||
LIVEKIT_KEY: ${LIVEKIT_KEY:-fedeo-livekit}
|
|
||||||
LIVEKIT_SECRET: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
|
||||||
LIVEKIT_FULL_ACCESS_HOMESERVERS: ${MATRIX_SERVER_NAME:-${DOMAIN}}
|
|
||||||
LIVEKIT_JWT_BIND: :8080
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.middlewares.fedeo-matrix-rtc-jwt-strip.stripprefix.prefixes=/livekit/jwt
|
|
||||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.rule=Host(`${DOMAIN}`) && PathPrefix(`/livekit/jwt`)
|
|
||||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.entrypoints=websecure
|
|
||||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.tls.certresolver=letsencrypt
|
|
||||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.middlewares=fedeo-matrix-rtc-jwt-strip
|
|
||||||
- traefik.http.services.fedeo-matrix-rtc-jwt.loadbalancer.server.port=8080
|
|
||||||
- traefik.docker.network=fedeo_web
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
- internal
|
|
||||||
|
|
||||||
matrix-element:
|
|
||||||
image: vectorim/element-web:latest
|
|
||||||
container_name: fedeo-matrix-element
|
|
||||||
user: "0:0"
|
|
||||||
restart: unless-stopped
|
|
||||||
entrypoint: /bin/sh
|
|
||||||
command:
|
|
||||||
- -ec
|
|
||||||
- |
|
|
||||||
cat >/app/config.json <<EOF
|
|
||||||
{
|
|
||||||
"default_server_config": {
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "https://${DOMAIN}",
|
|
||||||
"server_name": "${MATRIX_SERVER_NAME:-${DOMAIN}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "https://${DOMAIN}/livekit/jwt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"disable_custom_urls": false,
|
|
||||||
"disable_guests": true,
|
|
||||||
"brand": "FEDEO Matrix",
|
|
||||||
"default_theme": "light",
|
|
||||||
"features": {
|
|
||||||
"feature_video_rooms": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
exec nginx -g 'daemon off;'
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.routers.fedeo-matrix-element.rule=Host(`${DOMAIN}`) && PathPrefix(`/element`)
|
|
||||||
- traefik.http.routers.fedeo-matrix-element.entrypoints=websecure
|
|
||||||
- traefik.http.routers.fedeo-matrix-element.tls.certresolver=letsencrypt
|
|
||||||
- traefik.http.middlewares.fedeo-matrix-element-strip.stripprefix.prefixes=/element
|
|
||||||
- traefik.http.routers.fedeo-matrix-element.middlewares=fedeo-matrix-element-strip
|
|
||||||
- traefik.http.services.fedeo-matrix-element.loadbalancer.server.port=80
|
|
||||||
- traefik.docker.network=fedeo_web
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
name: fedeo_web
|
name: fedeo_web
|
||||||
|
|||||||
@@ -11,18 +11,16 @@ services:
|
|||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik"
|
- "traefik.docker.network=traefik"
|
||||||
- "traefik.port=3000"
|
- "traefik.port=3000"
|
||||||
# Middlewares
|
|
||||||
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
|
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
|
||||||
# Web Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
|
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
|
||||||
- "traefik.http.routers.fedeo-frontend.rule=Host(`app.fedeo.de`) && PathPrefix(`/`)"
|
- "traefik.http.routers.fedeo-frontend.rule=Host(`app.fedeo.de`) && PathPrefix(`/`)"
|
||||||
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
|
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
|
||||||
- "traefik.http.routers.fedeo-frontend.priority=1"
|
- "traefik.http.routers.fedeo-frontend.priority=1"
|
||||||
# Web Secure Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`app.fedeo.de`) && PathPrefix(`/`)"
|
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`app.fedeo.de`) && PathPrefix(`/`)"
|
||||||
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" #
|
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured"
|
||||||
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
|
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
|
||||||
- "traefik.http.routers.fedeo-frontend-secure.priority=1"
|
- "traefik.http.routers.fedeo-frontend-secure.priority=1"
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
image: git.federspiel.tech/flfeders/fedeo/docs:dev
|
image: git.federspiel.tech/flfeders/fedeo/docs:dev
|
||||||
restart: always
|
restart: always
|
||||||
@@ -32,20 +30,18 @@ services:
|
|||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik"
|
- "traefik.docker.network=traefik"
|
||||||
- "traefik.port=3000"
|
- "traefik.port=3000"
|
||||||
# Middlewares
|
|
||||||
- "traefik.http.middlewares.fedeo-docs-redirect-web-secure.redirectscheme.scheme=https"
|
- "traefik.http.middlewares.fedeo-docs-redirect-web-secure.redirectscheme.scheme=https"
|
||||||
- "traefik.http.middlewares.fedeo-docs-strip.stripprefix.prefixes=/docs"
|
- "traefik.http.middlewares.fedeo-docs-strip.stripprefix.prefixes=/docs"
|
||||||
# Web Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-docs.middlewares=fedeo-docs-redirect-web-secure"
|
- "traefik.http.routers.fedeo-docs.middlewares=fedeo-docs-redirect-web-secure"
|
||||||
- "traefik.http.routers.fedeo-docs.rule=Host(`app.fedeo.de`) && PathPrefix(`/docs`)"
|
- "traefik.http.routers.fedeo-docs.rule=Host(`app.fedeo.de`) && PathPrefix(`/docs`)"
|
||||||
- "traefik.http.routers.fedeo-docs.entrypoints=web"
|
- "traefik.http.routers.fedeo-docs.entrypoints=web"
|
||||||
- "traefik.http.routers.fedeo-docs.priority=120"
|
- "traefik.http.routers.fedeo-docs.priority=120"
|
||||||
# Web Secure Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-docs-secure.rule=Host(`app.fedeo.de`) && PathPrefix(`/docs`)"
|
- "traefik.http.routers.fedeo-docs-secure.rule=Host(`app.fedeo.de`) && PathPrefix(`/docs`)"
|
||||||
- "traefik.http.routers.fedeo-docs-secure.entrypoints=web-secured"
|
- "traefik.http.routers.fedeo-docs-secure.entrypoints=web-secured"
|
||||||
- "traefik.http.routers.fedeo-docs-secure.tls.certresolver=mytlschallenge"
|
- "traefik.http.routers.fedeo-docs-secure.tls.certresolver=mytlschallenge"
|
||||||
- "traefik.http.routers.fedeo-docs-secure.middlewares=fedeo-docs-strip"
|
- "traefik.http.routers.fedeo-docs-secure.middlewares=fedeo-docs-strip"
|
||||||
- "traefik.http.routers.fedeo-docs-secure.priority=120"
|
- "traefik.http.routers.fedeo-docs-secure.priority=120"
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: git.federspiel.tech/flfeders/fedeo/backend:dev
|
image: git.federspiel.tech/flfeders/fedeo/backend:dev
|
||||||
restart: always
|
restart: always
|
||||||
@@ -78,16 +74,13 @@ services:
|
|||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik"
|
- "traefik.docker.network=traefik"
|
||||||
- "traefik.port=3100"
|
- "traefik.port=3100"
|
||||||
# Middlewares
|
|
||||||
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
|
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
|
||||||
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
|
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
|
||||||
# Web Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
|
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
|
||||||
- "traefik.http.routers.fedeo-backend.rule=Host(`app.fedeo.de`) && PathPrefix(`/backend`)"
|
- "traefik.http.routers.fedeo-backend.rule=Host(`app.fedeo.de`) && PathPrefix(`/backend`)"
|
||||||
- "traefik.http.routers.fedeo-backend.entrypoints=web"
|
- "traefik.http.routers.fedeo-backend.entrypoints=web"
|
||||||
# Web Secure Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`app.fedeo.de`) && PathPrefix(`/backend`)"
|
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`app.fedeo.de`) && PathPrefix(`/backend`)"
|
||||||
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
|
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured"
|
||||||
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
|
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
|
||||||
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
|
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
|
||||||
|
|
||||||
@@ -107,323 +100,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
|
|
||||||
matrix-db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=${MATRIX_POSTGRES_DB:-synapse}
|
|
||||||
- POSTGRES_USER=${MATRIX_POSTGRES_USER:-synapse}
|
|
||||||
- POSTGRES_PASSWORD=${MATRIX_POSTGRES_PASSWORD:-change-this-matrix-db-password}
|
|
||||||
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --lc-collate=C --lc-ctype=C
|
|
||||||
volumes:
|
|
||||||
- ./matrix/postgres:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-synapse:
|
|
||||||
image: ghcr.io/element-hq/synapse:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix
|
|
||||||
depends_on:
|
|
||||||
- matrix-db
|
|
||||||
- matrix-redis
|
|
||||||
environment:
|
|
||||||
- SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
|
|
||||||
volumes:
|
|
||||||
- ./matrix/synapse:/data
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik"
|
|
||||||
- "traefik.port=8008"
|
|
||||||
- "traefik.http.services.fedeo-matrix.loadbalancer.server.port=8008"
|
|
||||||
# Matrix Client-Server API
|
|
||||||
- "traefik.http.routers.fedeo-matrix.rule=Host(`${MATRIX_HOMESERVER_HOST:-matrix.fedeo.de}`) && PathPrefix(`/_matrix`)"
|
|
||||||
- "traefik.http.routers.fedeo-matrix.entrypoints=web"
|
|
||||||
- "traefik.http.routers.fedeo-matrix.middlewares=fedeo-matrix-redirect-web-secure"
|
|
||||||
- "traefik.http.routers.fedeo-matrix.service=fedeo-matrix"
|
|
||||||
- "traefik.http.middlewares.fedeo-matrix-redirect-web-secure.redirectscheme.scheme=https"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-secure.rule=Host(`${MATRIX_HOMESERVER_HOST:-matrix.fedeo.de}`) && PathPrefix(`/_matrix`)"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-secure.entrypoints=web-secured"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-secure.tls.certresolver=mytlschallenge"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-secure.service=fedeo-matrix"
|
|
||||||
# Matrix Federation API, nur öffnen wenn Federation gewünscht ist.
|
|
||||||
- "traefik.http.routers.fedeo-matrix-federation.rule=Host(`${MATRIX_HOMESERVER_HOST:-matrix.fedeo.de}`) && PathPrefix(`/_matrix/federation`)"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-federation.entrypoints=web-secured"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-federation.tls.certresolver=mytlschallenge"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-federation.service=fedeo-matrix"
|
|
||||||
|
|
||||||
matrix-well-known:
|
|
||||||
image: nginx:1.27-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix
|
|
||||||
volumes:
|
|
||||||
- ./matrix/well-known:/usr/share/nginx/html/.well-known/matrix:ro
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik"
|
|
||||||
- "traefik.port=80"
|
|
||||||
- "traefik.http.services.fedeo-matrix-well-known.loadbalancer.server.port=80"
|
|
||||||
- "traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolalloworiginlist=*"
|
|
||||||
- "traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolallowmethods=GET,OPTIONS"
|
|
||||||
- "traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolallowheaders=Content-Type,Authorization"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-well-known.rule=Host(`${MATRIX_SERVER_NAME:-fedeo.de}`) && PathPrefix(`/.well-known/matrix`)"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-well-known.entrypoints=web-secured"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-well-known.tls.certresolver=mytlschallenge"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-well-known.middlewares=fedeo-matrix-well-known-cors"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-well-known.service=fedeo-matrix-well-known"
|
|
||||||
|
|
||||||
matrix-turn:
|
|
||||||
image: instrumentisto/coturn:4
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix
|
|
||||||
command:
|
|
||||||
- --fingerprint
|
|
||||||
- --use-auth-secret
|
|
||||||
- --static-auth-secret=${MATRIX_TURN_SHARED_SECRET:-change-this-turn-secret}
|
|
||||||
- --realm=${MATRIX_SERVER_NAME:-fedeo.de}
|
|
||||||
- --listening-port=3478
|
|
||||||
- --tls-listening-port=5349
|
|
||||||
- --min-port=49160
|
|
||||||
- --max-port=49200
|
|
||||||
- --no-cli
|
|
||||||
- --no-tlsv1
|
|
||||||
- --no-tlsv1_1
|
|
||||||
ports:
|
|
||||||
- "3478:3478/tcp"
|
|
||||||
- "3478:3478/udp"
|
|
||||||
- "5349:5349/tcp"
|
|
||||||
- "49160-49200:49160-49200/udp"
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-livekit:
|
|
||||||
image: livekit/livekit-server:v1.9
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix
|
|
||||||
depends_on:
|
|
||||||
- matrix-redis
|
|
||||||
entrypoint: /bin/sh
|
|
||||||
command:
|
|
||||||
- -ec
|
|
||||||
- |
|
|
||||||
cat >/tmp/livekit.yaml <<EOF
|
|
||||||
port: 7880
|
|
||||||
redis:
|
|
||||||
address: matrix-redis:6379
|
|
||||||
rtc:
|
|
||||||
tcp_port: 7881
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 50100
|
|
||||||
use_external_ip: true
|
|
||||||
keys:
|
|
||||||
${LIVEKIT_KEY:-fedeo-livekit}: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
|
||||||
room:
|
|
||||||
auto_create: true
|
|
||||||
EOF
|
|
||||||
exec /livekit-server --config /tmp/livekit.yaml
|
|
||||||
ports:
|
|
||||||
- "7881:7881/tcp"
|
|
||||||
- "50000-50100:50000-50100/udp"
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik"
|
|
||||||
- "traefik.port=7880"
|
|
||||||
- "traefik.http.services.fedeo-matrix-livekit.loadbalancer.server.port=7880"
|
|
||||||
- "traefik.http.middlewares.fedeo-matrix-livekit-strip.stripprefix.prefixes=/livekit/sfu"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-livekit.rule=Host(`${MATRIX_RTC_HOST:-call.fedeo.de}`) && PathPrefix(`/livekit/sfu`)"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-livekit.entrypoints=web-secured"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-livekit.tls.certresolver=mytlschallenge"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-livekit.middlewares=fedeo-matrix-livekit-strip"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-livekit.service=fedeo-matrix-livekit"
|
|
||||||
|
|
||||||
matrix-rtc-jwt:
|
|
||||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix
|
|
||||||
depends_on:
|
|
||||||
- matrix-livekit
|
|
||||||
- matrix-synapse
|
|
||||||
environment:
|
|
||||||
- LIVEKIT_URL=wss://${MATRIX_RTC_HOST:-call.fedeo.de}/livekit/sfu
|
|
||||||
- LIVEKIT_KEY=${LIVEKIT_KEY:-fedeo-livekit}
|
|
||||||
- LIVEKIT_SECRET=${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
|
||||||
- LIVEKIT_FULL_ACCESS_HOMESERVERS=${MATRIX_SERVER_NAME:-fedeo.de}
|
|
||||||
- LIVEKIT_JWT_BIND=:8080
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik"
|
|
||||||
- "traefik.port=8080"
|
|
||||||
- "traefik.http.services.fedeo-matrix-rtc-jwt.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.http.middlewares.fedeo-matrix-rtc-jwt-strip.stripprefix.prefixes=/livekit/jwt"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-rtc-jwt.rule=Host(`${MATRIX_RTC_HOST:-call.fedeo.de}`) && PathPrefix(`/livekit/jwt`)"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-rtc-jwt.entrypoints=web-secured"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-rtc-jwt.tls.certresolver=mytlschallenge"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-rtc-jwt.middlewares=fedeo-matrix-rtc-jwt-strip"
|
|
||||||
- "traefik.http.routers.fedeo-matrix-rtc-jwt.service=fedeo-matrix-rtc-jwt"
|
|
||||||
|
|
||||||
matrix-dev-db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix-dev
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=synapse
|
|
||||||
- POSTGRES_USER=synapse
|
|
||||||
- POSTGRES_PASSWORD=synapse-dev-password
|
|
||||||
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --lc-collate=C --lc-ctype=C
|
|
||||||
volumes:
|
|
||||||
- ./matrix/dev/postgres:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-dev-redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix-dev
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-dev-synapse:
|
|
||||||
image: ghcr.io/element-hq/synapse:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix-dev
|
|
||||||
depends_on:
|
|
||||||
- matrix-dev-db
|
|
||||||
- matrix-dev-redis
|
|
||||||
environment:
|
|
||||||
- SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
|
|
||||||
volumes:
|
|
||||||
- ./matrix/dev/synapse:/data
|
|
||||||
ports:
|
|
||||||
- "${MATRIX_DEV_SYNAPSE_PORT:-8008}:8008"
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-dev-turn:
|
|
||||||
image: instrumentisto/coturn:4
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix-dev
|
|
||||||
command:
|
|
||||||
- --fingerprint
|
|
||||||
- --use-auth-secret
|
|
||||||
- --static-auth-secret=matrix-dev-turn-secret
|
|
||||||
- --realm=localhost
|
|
||||||
- --listening-port=3478
|
|
||||||
- --min-port=49160
|
|
||||||
- --max-port=49200
|
|
||||||
- --no-cli
|
|
||||||
- --no-tls
|
|
||||||
- --no-dtls
|
|
||||||
ports:
|
|
||||||
- "${MATRIX_DEV_TURN_PORT:-3478}:3478/tcp"
|
|
||||||
- "${MATRIX_DEV_TURN_PORT:-3478}:3478/udp"
|
|
||||||
- "${MATRIX_DEV_TURN_MIN_PORT:-49160}-${MATRIX_DEV_TURN_MAX_PORT:-49200}:49160-49200/udp"
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-dev-livekit:
|
|
||||||
image: livekit/livekit-server:v1.9
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix-dev
|
|
||||||
depends_on:
|
|
||||||
- matrix-dev-redis
|
|
||||||
entrypoint: /bin/sh
|
|
||||||
command:
|
|
||||||
- -ec
|
|
||||||
- |
|
|
||||||
cat >/tmp/livekit.yaml <<EOF
|
|
||||||
port: 7880
|
|
||||||
redis:
|
|
||||||
address: matrix-dev-redis:6379
|
|
||||||
rtc:
|
|
||||||
tcp_port: 7881
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 50100
|
|
||||||
node_ip: ${MATRIX_DEV_LIVEKIT_NODE_IP:-127.0.0.1}
|
|
||||||
use_external_ip: false
|
|
||||||
enable_loopback_candidate: true
|
|
||||||
keys:
|
|
||||||
devkey: devsecret-local-matrix-stack-32-chars
|
|
||||||
room:
|
|
||||||
auto_create: true
|
|
||||||
EOF
|
|
||||||
exec /livekit-server --config /tmp/livekit.yaml
|
|
||||||
ports:
|
|
||||||
- "${MATRIX_DEV_LIVEKIT_PORT:-7880}:7880"
|
|
||||||
- "${MATRIX_DEV_LIVEKIT_TCP_PORT:-7881}:7881/tcp"
|
|
||||||
- "${MATRIX_DEV_LIVEKIT_RTC_MIN_PORT:-50000}-${MATRIX_DEV_LIVEKIT_RTC_MAX_PORT:-50100}:50000-50100/udp"
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-dev-rtc-jwt:
|
|
||||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix-dev
|
|
||||||
depends_on:
|
|
||||||
- matrix-dev-livekit
|
|
||||||
- matrix-dev-synapse
|
|
||||||
environment:
|
|
||||||
- LIVEKIT_URL=ws://localhost:${MATRIX_DEV_LIVEKIT_PORT:-7880}
|
|
||||||
- LIVEKIT_KEY=devkey
|
|
||||||
- LIVEKIT_SECRET=devsecret-local-matrix-stack-32-chars
|
|
||||||
- LIVEKIT_FULL_ACCESS_HOMESERVERS=localhost
|
|
||||||
- LIVEKIT_JWT_BIND=:8080
|
|
||||||
ports:
|
|
||||||
- "${MATRIX_DEV_RTC_JWT_PORT:-8081}:8080"
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
|
|
||||||
matrix-dev-element:
|
|
||||||
image: vectorim/element-web:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles:
|
|
||||||
- matrix-dev
|
|
||||||
volumes:
|
|
||||||
- ./matrix/dev/element-config.json:/app/config.json:ro
|
|
||||||
ports:
|
|
||||||
- "${MATRIX_DEV_ELEMENT_PORT:-8080}:80"
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
# db:
|
|
||||||
# image: postgres
|
|
||||||
# restart: always
|
|
||||||
# shm_size: 128mb
|
|
||||||
# environment:
|
|
||||||
# POSTGRES_PASSWORD: abc
|
|
||||||
# POSTGRES_USER: sandelcom
|
|
||||||
# POSTGRES_DB: sensorfy
|
|
||||||
# volumes:
|
|
||||||
# - ./pg-data:/var/lib/postgresql/data
|
|
||||||
# ports:
|
|
||||||
# - "5432:5432"
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v2.11
|
image: traefik:v2.11
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -442,18 +118,19 @@ services:
|
|||||||
- "--accesslog.bufferingsize=5000"
|
- "--accesslog.bufferingsize=5000"
|
||||||
- "--accesslog.fields.defaultMode=keep"
|
- "--accesslog.fields.defaultMode=keep"
|
||||||
- "--accesslog.fields.headers.defaultMode=keep"
|
- "--accesslog.fields.headers.defaultMode=keep"
|
||||||
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" #
|
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
|
||||||
- "--certificatesresolvers.mytlschallenge.acme.email=moin@fedeo.de"
|
- "--certificatesresolvers.mytlschallenge.acme.email=moin@fedeo.de"
|
||||||
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
|
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 443:443
|
- 443:443
|
||||||
volumes:
|
volumes:
|
||||||
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS)
|
- "./traefik/letsencrypt:/letsencrypt"
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
- "./traefik/logs:/logs"
|
- "./traefik/logs:/logs"
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik:
|
traefik:
|
||||||
external: false
|
external: false
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ Diese Dokumentation unterstützt dich bei der täglichen Nutzung von FEDEO.
|
|||||||
## Einstieg
|
## Einstieg
|
||||||
|
|
||||||
- [Bedienung](./bedienung/README.md)
|
- [Bedienung](./bedienung/README.md)
|
||||||
- [Kommunikationslösung auf Basis des Matrix-Standards](./kommunikationslösung-matrix.md)
|
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
# Kommunikationslösung auf Basis des Matrix-Standards
|
|
||||||
|
|
||||||
Dieser Entwurf beschreibt eine FEDEO-Kommunikationslösung für Chat, Anrufe und Videokonferenzen auf Basis des Matrix-Standards. Ziel ist eine souverän betreibbare Lösung, die Mandantenfähigkeit, Datenschutz, Rechteverwaltung und die bestehenden FEDEO-Workflows berücksichtigt.
|
|
||||||
|
|
||||||
## Zielbild
|
|
||||||
|
|
||||||
FEDEO erhält einen integrierten Kommunikationsbereich, der interne Zusammenarbeit und externe Kommunikation abdeckt:
|
|
||||||
|
|
||||||
- Chat in Einzel-, Gruppen-, Projekt-, Vorgangs- und Kundenräumen
|
|
||||||
- Audioanrufe aus Direktchats, Gruppenräumen und Kontakten
|
|
||||||
- Videokonferenzen mit Bildschirmfreigabe und Einladungslinks
|
|
||||||
- Ende-zu-Ende-verschlüsselte private Kommunikation
|
|
||||||
- revisionsfähige Verknüpfung von relevanten Kommunikationsereignissen mit FEDEO-Objekten
|
|
||||||
- optional föderierte Kommunikation mit externen Matrix-Organisationen
|
|
||||||
|
|
||||||
Matrix wird dabei nicht als isolierter Messenger betrieben, sondern als Kommunikationsschicht neben dem bestehenden FEDEO-Backend.
|
|
||||||
|
|
||||||
## Empfohlene Architektur
|
|
||||||
|
|
||||||
```text
|
|
||||||
Nutzerinnen und Nutzer
|
|
||||||
|
|
|
||||||
| FEDEO Web, Mobile App, optional Element Desktop/Mobile
|
|
||||||
v
|
|
||||||
FEDEO Frontend
|
|
||||||
|
|
|
||||||
| FEDEO API, SSO, Rechte, Objektkontext
|
|
||||||
v
|
|
||||||
FEDEO Backend
|
|
||||||
|
|
|
||||||
| Provisionierung, Webhooks, Audit-Metadaten
|
|
||||||
v
|
|
||||||
Matrix Homeserver
|
|
||||||
|
|
|
||||||
+-- PostgreSQL für Matrix-Daten
|
|
||||||
+-- Redis für Worker und Caches
|
|
||||||
+-- Medien-Repository für Anhänge
|
|
||||||
+-- TURN/STUN für direkte Medienverbindungen
|
|
||||||
+-- MatrixRTC / LiveKit SFU für Gruppenanrufe und Videokonferenzen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kernkomponenten
|
|
||||||
|
|
||||||
| Komponente | Empfehlung | Aufgabe |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Matrix Homeserver | Synapse | Standardnaher, bewährter Homeserver mit guter Betriebsdokumentation |
|
|
||||||
| Matrix Client im FEDEO Web | Matrix JS SDK oder eingebetteter Element-Web-Ausschnitt | Chat, Raumliste, Nachrichten, Reaktionen, Anhänge |
|
|
||||||
| Mobile Integration | Matrix SDK über FEDEO Mobile oder Deep Link zu Element X | Pushfähige mobile Kommunikation |
|
|
||||||
| Identität | OIDC/SSO über FEDEO Auth, perspektivisch Matrix Authentication Service | Einheitlicher Login und zentrale Nutzerverwaltung |
|
|
||||||
| Audio/Video | MatrixRTC mit Element Call und LiveKit SFU | Moderne Anrufe und Videokonferenzen |
|
|
||||||
| NAT Traversal | coturn | STUN/TURN für stabile Medienverbindungen |
|
|
||||||
| Reverse Proxy | bestehender Traefik-Ansatz | TLS, Routing, `.well-known/matrix/*` |
|
|
||||||
| Administration | FEDEO Admin-Oberfläche plus Synapse Admin API | Nutzer, Räume, Richtlinien, Sperren |
|
|
||||||
|
|
||||||
## Betriebsmodell
|
|
||||||
|
|
||||||
Für FEDEO ist ein eigener Matrix-Homeserver pro Installation oder pro großer Betreiberinstanz sinnvoll. Der Matrix-Server sollte nicht öffentlich als offener Registrierungsserver betrieben werden. Nutzer werden ausschließlich durch FEDEO angelegt, aktualisiert und deaktiviert.
|
|
||||||
|
|
||||||
Empfohlene Domains:
|
|
||||||
|
|
||||||
- `app.example.com`: FEDEO Oberfläche
|
|
||||||
- `matrix.example.com`: Matrix Client-Server und Federation API
|
|
||||||
- `call.example.com`: Element Call / MatrixRTC
|
|
||||||
- `livekit.example.com`: LiveKit SFU
|
|
||||||
- `turn.example.com`: TURN/STUN
|
|
||||||
|
|
||||||
Die öffentliche Matrix-Serverkennung kann trotzdem `example.com` lauten. Dafür werden `.well-known/matrix/client` und `.well-known/matrix/server` über Traefik ausgeliefert.
|
|
||||||
|
|
||||||
## Mandantenmodell
|
|
||||||
|
|
||||||
Matrix selbst ist raumbasiert, FEDEO ist mandantenbasiert. Deshalb sollte FEDEO die Mandantenlogik explizit auf Matrix-Räume und Spaces abbilden.
|
|
||||||
|
|
||||||
### Räume und Spaces
|
|
||||||
|
|
||||||
- Pro FEDEO-Mandant wird ein Matrix Space angelegt.
|
|
||||||
- Projekte, Vorgänge, Helpdesk-Konversationen, interne Teams und Kundenkontakte werden als Räume im Mandanten-Space geführt.
|
|
||||||
- Direkträume werden nutzerbezogen angelegt, aber über FEDEO mandantengebunden sichtbar gemacht.
|
|
||||||
- Externe Räume erhalten einen klaren Status, zum Beispiel `intern`, `extern`, `kunde`, `lieferant`.
|
|
||||||
|
|
||||||
### Raumalias-Konvention
|
|
||||||
|
|
||||||
Beispiele:
|
|
||||||
|
|
||||||
- `#tenant-<mandant>-team:example.com`
|
|
||||||
- `#tenant-<mandant>-project-<projekt>:example.com`
|
|
||||||
- `#tenant-<mandant>-ticket-<ticket>:example.com`
|
|
||||||
- `#tenant-<mandant>-customer-<kunde>:example.com`
|
|
||||||
|
|
||||||
Interne technische IDs sollten nicht als sichtbarer Anzeigename genutzt werden. Nutzerinnen und Nutzer sehen sprechende Namen wie `Projekt: Website Relaunch` oder `Kunde: Muster GmbH`.
|
|
||||||
|
|
||||||
## Rechte und Rollen
|
|
||||||
|
|
||||||
FEDEO bleibt führend für Berechtigungen. Matrix übernimmt die technische Durchsetzung im Raum.
|
|
||||||
|
|
||||||
| FEDEO-Rolle | Matrix-Abbildung |
|
|
||||||
| --- | --- |
|
|
||||||
| Mandantenadmin | Space-Admin und Raumadmin |
|
|
||||||
| Teamleitung | Moderatorin oder Moderator in Team- und Projekträumen |
|
|
||||||
| Mitarbeitende | Mitglied mit Schreibrechten |
|
|
||||||
| Externe Kontakte | Eingeschränkte Mitgliedschaft in ausgewählten Räumen |
|
|
||||||
| Automationen | Application-Service- oder Bot-Nutzer mit minimalen Rechten |
|
|
||||||
|
|
||||||
Änderungen an Rollen, Teams oder Mandantenzugehörigkeiten lösen im FEDEO-Backend eine Synchronisation mit Matrix aus. Beim Entzug eines Zugriffs wird die Person aus den betroffenen Räumen entfernt. Bei Ende-zu-Ende-verschlüsselten Räumen muss zusätzlich berücksichtigt werden, dass bereits erhaltene Nachrichten auf Geräten verbleiben können.
|
|
||||||
|
|
||||||
## Chat
|
|
||||||
|
|
||||||
Der Chat wird als erste Ausbaustufe umgesetzt.
|
|
||||||
|
|
||||||
### Funktionen
|
|
||||||
|
|
||||||
- Direktnachrichten
|
|
||||||
- Gruppenräume
|
|
||||||
- Mandanten-, Team-, Projekt- und Vorgangsräume
|
|
||||||
- Datei- und Bildanhänge
|
|
||||||
- Erwähnungen, Reaktionen und Lesestatus
|
|
||||||
- Suche in nicht verschlüsselten Räumen über den Homeserver
|
|
||||||
- lokale Suche in verschlüsselten Räumen über Client-Indizes
|
|
||||||
- Verknüpfung von Nachrichten mit FEDEO-Objekten
|
|
||||||
|
|
||||||
### Integration in FEDEO
|
|
||||||
|
|
||||||
FEDEO sollte keine vollständige Kopie aller Nachrichten in der eigenen Datenbank speichern. Stattdessen speichert FEDEO nur Referenzen:
|
|
||||||
|
|
||||||
- Matrix Raum-ID
|
|
||||||
- Matrix Event-ID
|
|
||||||
- FEDEO Objekt-Typ und Objekt-ID
|
|
||||||
- Zeitstempel
|
|
||||||
- beteiligter FEDEO-Nutzer
|
|
||||||
- optionale Vorschau, falls Datenschutzrichtlinie dies erlaubt
|
|
||||||
|
|
||||||
So bleibt Matrix das Kommunikationssystem, während FEDEO nachvollziehen kann, welche Kommunikation zu welchem Objekt gehört.
|
|
||||||
|
|
||||||
## Audioanrufe
|
|
||||||
|
|
||||||
Einzelanrufe können direkt über Matrix-VoIP in Direktchats gestartet werden. Der FEDEO-Client zeigt dafür in Kontakt-, Kunden-, Mitarbeitenden- und Chatansichten einen Anruf-Button.
|
|
||||||
|
|
||||||
### Anforderungen
|
|
||||||
|
|
||||||
- WebRTC-Unterstützung im Browser
|
|
||||||
- STUN/TURN über coturn
|
|
||||||
- Geräteauswahl für Mikrofon und Lautsprecher
|
|
||||||
- Anrufbenachrichtigung im Web und mobil
|
|
||||||
- Statusanzeige `verfügbar`, `beschäftigt`, `im Anruf`, `abwesend`
|
|
||||||
|
|
||||||
Für klassische Telefonie kann später ein SIP-Gateway ergänzt werden. Das sollte jedoch getrennt von der ersten Matrix-Einführung betrachtet werden, damit Chat und WebRTC-Kommunikation nicht durch Telefoniekomplexität ausgebremst werden.
|
|
||||||
|
|
||||||
## Videokonferenzen
|
|
||||||
|
|
||||||
Für Gruppenanrufe und Videokonferenzen wird MatrixRTC mit Element Call und LiveKit empfohlen. Matrix übernimmt dabei Raumzustand, Identität, Berechtigungen und Signalisierung; LiveKit übernimmt als SFU die effiziente Medienverteilung.
|
|
||||||
|
|
||||||
### Funktionen
|
|
||||||
|
|
||||||
- Videokonferenzen aus Matrix-Räumen
|
|
||||||
- spontane Besprechungen aus Projekten, Vorgängen oder Kundenakten
|
|
||||||
- Bildschirmfreigabe
|
|
||||||
- Einladungslink für externe Gäste
|
|
||||||
- Wartebereich für externe Gäste
|
|
||||||
- Moderationsrechte für Stummschalten, Entfernen und Raumverwaltung
|
|
||||||
- optionale Aufzeichnung erst in einer späteren, gesondert freizugebenden Ausbaustufe
|
|
||||||
|
|
||||||
### Konfiguration
|
|
||||||
|
|
||||||
Clients finden den MatrixRTC-Dienst über `.well-known/matrix/client`. Dort wird der LiveKit-JWT-Dienst als `org.matrix.msc4143.rtc_foci` angekündigt. Diese Datei muss öffentlich lesbar sein, als JSON ausgeliefert werden und CORS für Webclients erlauben.
|
|
||||||
|
|
||||||
Beispiel:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "https://matrix.example.com"
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "https://call.example.com/livekit/jwt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Authentifizierung und Nutzerverwaltung
|
|
||||||
|
|
||||||
FEDEO sollte Identität und Lebenszyklus der Nutzer zentral steuern.
|
|
||||||
|
|
||||||
### Empfohlener Ablauf
|
|
||||||
|
|
||||||
1. Nutzer wird in FEDEO angelegt.
|
|
||||||
2. FEDEO erzeugt oder aktualisiert den Matrix-Nutzer.
|
|
||||||
3. FEDEO weist den Nutzer den passenden Spaces und Räumen zu.
|
|
||||||
4. Login erfolgt über FEDEO SSO/OIDC.
|
|
||||||
5. Deaktivierung in FEDEO deaktiviert auch den Matrix-Zugang und entfernt Raumzugriffe.
|
|
||||||
|
|
||||||
Die Matrix User-ID sollte stabil und nicht personenbezogen änderungsanfällig sein:
|
|
||||||
|
|
||||||
```text
|
|
||||||
@u_<fedeo_user_id>:example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Anzeigename kann weiterhin den echten Namen enthalten und bei Änderungen synchronisiert werden.
|
|
||||||
|
|
||||||
## Datenschutz und Compliance
|
|
||||||
|
|
||||||
Matrix erlaubt starke Datenschutzkonzepte, erfordert aber klare Betriebsregeln.
|
|
||||||
|
|
||||||
### Empfehlungen
|
|
||||||
|
|
||||||
- Ende-zu-Ende-Verschlüsselung für Direktnachrichten und vertrauliche Projekträume aktivieren.
|
|
||||||
- Nicht verschlüsselte Räume nur dort nutzen, wo serverseitige Suche, Archivierung oder Compliance-Funktionen ausdrücklich benötigt werden.
|
|
||||||
- Medienaufbewahrung mandantenweit konfigurierbar machen.
|
|
||||||
- Externe Gäste optisch klar kennzeichnen.
|
|
||||||
- Federation standardmäßig deaktivieren oder auf erlaubte Domains beschränken.
|
|
||||||
- Aufzeichnungen von Videokonferenzen nur mit expliziter Einwilligung und sichtbarem Status erlauben.
|
|
||||||
- Administrative Zugriffe protokollieren.
|
|
||||||
- Klare Löschfristen für Räume, Anhänge und Audit-Referenzen definieren.
|
|
||||||
|
|
||||||
## Federation
|
|
||||||
|
|
||||||
Matrix kann mit anderen Homeservern föderieren. Für FEDEO sollte Federation als kontrollierbare Option umgesetzt werden.
|
|
||||||
|
|
||||||
### Betriebsmodi
|
|
||||||
|
|
||||||
| Modus | Beschreibung | Empfehlung |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| geschlossen | Keine Federation, nur interne Nutzer und explizite Gäste | Standard für kleine Installationen |
|
|
||||||
| allowlist | Federation nur mit freigegebenen Domains | Empfehlung für B2B-Kommunikation |
|
|
||||||
| offen | Federation mit beliebigen Matrix-Servern | Nur für bewusst öffentliche Communities |
|
|
||||||
|
|
||||||
Für steuer-, kunden- und projektnahe Kommunikation ist `allowlist` der beste Zielmodus.
|
|
||||||
|
|
||||||
## Brücken zu anderen Systemen
|
|
||||||
|
|
||||||
Matrix unterstützt Brücken zu anderen Kommunikationsdiensten. Für FEDEO sind Brücken nützlich, sollten aber nicht zur ersten Produktstufe gehören.
|
|
||||||
|
|
||||||
Mögliche spätere Erweiterungen:
|
|
||||||
|
|
||||||
- E-Mail-Brücke für Helpdesk- oder Kundenkommunikation
|
|
||||||
- Slack- oder Teams-Brücke für externe Projektpartner
|
|
||||||
- WhatsApp- oder SMS-Brücke nur nach gesonderter Datenschutzprüfung
|
|
||||||
- SIP-Brücke für Telefonie
|
|
||||||
|
|
||||||
Brücken müssen pro Mandant aktivierbar sein und brauchen klare Hinweise, welche Daten an externe Dienste fließen.
|
|
||||||
|
|
||||||
## FEDEO-Produktoberfläche
|
|
||||||
|
|
||||||
Die Kommunikation sollte in FEDEO an zwei Stellen sichtbar sein.
|
|
||||||
|
|
||||||
### Globaler Kommunikationsbereich
|
|
||||||
|
|
||||||
- Raumliste
|
|
||||||
- Direktnachrichten
|
|
||||||
- Suche
|
|
||||||
- Anrufe
|
|
||||||
- laufende Besprechungen
|
|
||||||
- Benachrichtigungen
|
|
||||||
|
|
||||||
### Objektbezogene Kommunikation
|
|
||||||
|
|
||||||
In Projekten, Kunden, Vorgängen, Helpdesk-Tickets und Dokumenten erscheint ein Kommunikations-Tab:
|
|
||||||
|
|
||||||
- zugeordneter Raum
|
|
||||||
- relevante Nachrichtenreferenzen
|
|
||||||
- Start von Chat, Anruf oder Besprechung
|
|
||||||
- Teilnehmerverwaltung entsprechend FEDEO-Rechten
|
|
||||||
|
|
||||||
So bleibt Kommunikation dort, wo die Arbeit stattfindet.
|
|
||||||
|
|
||||||
## Backend-Integration
|
|
||||||
|
|
||||||
Das FEDEO-Backend erhält ein Kommunikationsmodul mit folgenden Aufgaben:
|
|
||||||
|
|
||||||
- Matrix-Nutzer provisionieren
|
|
||||||
- Spaces und Räume anlegen
|
|
||||||
- Raum-Mitgliedschaften synchronisieren
|
|
||||||
- Matrix-Event-Webhooks empfangen
|
|
||||||
- FEDEO-Objekte mit Matrix-Räumen verknüpfen
|
|
||||||
- Benachrichtigungseinstellungen verwalten
|
|
||||||
- Admin-Aktionen auditieren
|
|
||||||
|
|
||||||
Technisch kann dies über Matrix Admin API, Client-Server API und Application Services erfolgen. Für Automationen empfiehlt sich ein eigener Application Service, weil er reservierte Nutzer- und Raum-Namensräume sauber verwalten kann.
|
|
||||||
|
|
||||||
## Deployment-Erweiterung
|
|
||||||
|
|
||||||
Der bestehende Docker-/Traefik-Ansatz kann um folgende Dienste erweitert werden:
|
|
||||||
|
|
||||||
- `matrix-synapse`
|
|
||||||
- `matrix-db` oder gemeinsame PostgreSQL-Instanz mit getrennter Datenbank
|
|
||||||
- `redis`
|
|
||||||
- `coturn`
|
|
||||||
- `element-web` optional als Fallback-Client
|
|
||||||
- `element-call`
|
|
||||||
- `livekit`
|
|
||||||
- `matrix-rtc-jwt-service`
|
|
||||||
|
|
||||||
Für produktive Installationen sollte Matrix eine eigene PostgreSQL-Datenbank erhalten. Medien sollten in S3-kompatiblen Speicher ausgelagert werden, damit große Anhänge und Konferenzartefakte nicht den Applikationsserver füllen.
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
Wichtige Kennzahlen:
|
|
||||||
|
|
||||||
- aktive Nutzerinnen und Nutzer
|
|
||||||
- Anzahl Räume pro Mandant
|
|
||||||
- Nachrichtenrate
|
|
||||||
- Medien-Speicherverbrauch
|
|
||||||
- Zustellverzögerung
|
|
||||||
- fehlgeschlagene Anrufe
|
|
||||||
- LiveKit Paketverlust, Latenz und Teilnehmerzahl
|
|
||||||
- TURN-Nutzung
|
|
||||||
- Federation-Fehler
|
|
||||||
|
|
||||||
Logs von FEDEO, Synapse, LiveKit, coturn und Traefik sollten über eine gemeinsame Korrelation, zum Beispiel Request-ID oder Nutzer-ID, untersuchbar sein.
|
|
||||||
|
|
||||||
## Risiken und Gegenmaßnahmen
|
|
||||||
|
|
||||||
| Risiko | Gegenmaßnahme |
|
|
||||||
| --- | --- |
|
|
||||||
| Komplexität durch zwei Systeme | FEDEO bleibt führend für Nutzer, Rechte und Objektbezug |
|
|
||||||
| Datenschutz bei externen Räumen | Externe Kennzeichnung, Federation-Allowlist, Mandantenrichtlinien |
|
|
||||||
| E2EE erschwert Suche und Archivierung | Raumtyp bewusst wählen, lokale Suche, Metadatenreferenzen statt Vollkopie |
|
|
||||||
| Medienverbindungen scheitern in Firmennetzen | coturn sauber betreiben, UDP und TCP/TLS-Fallback anbieten |
|
|
||||||
| Betriebskosten durch Video | LiveKit skalierbar betreiben, Limits pro Mandant definieren |
|
|
||||||
| Gästezugriff wird unübersichtlich | Einladungslinks mit Ablaufdatum, Wartebereich, Moderationsrechte |
|
|
||||||
|
|
||||||
## Umsetzung in Phasen
|
|
||||||
|
|
||||||
### Phase 1: Fundament und Chat
|
|
||||||
|
|
||||||
- Synapse mit PostgreSQL, Redis, Traefik und `.well-known` betreiben
|
|
||||||
- FEDEO-Nutzer zu Matrix synchronisieren
|
|
||||||
- Mandanten-Spaces und erste Teamräume anlegen
|
|
||||||
- Chat im FEDEO-Frontend integrieren
|
|
||||||
- Benachrichtigungen und Raumreferenzen speichern
|
|
||||||
|
|
||||||
### Phase 2: Objektbezogene Kommunikation
|
|
||||||
|
|
||||||
- Räume automatisch für Projekte, Vorgänge und Kunden anlegen
|
|
||||||
- Kommunikations-Tab in FEDEO-Objekten ergänzen
|
|
||||||
- Rechteänderungen aus FEDEO nach Matrix synchronisieren
|
|
||||||
- externe Gäste einladen und kennzeichnen
|
|
||||||
|
|
||||||
### Phase 3: Audio und Video
|
|
||||||
|
|
||||||
- coturn bereitstellen
|
|
||||||
- MatrixRTC, Element Call und LiveKit integrieren
|
|
||||||
- Anruf- und Videobuttons in Chat, Kontakten und Projekten ergänzen
|
|
||||||
- Gäste-Links und Wartebereich umsetzen
|
|
||||||
|
|
||||||
### Phase 4: Compliance und Skalierung
|
|
||||||
|
|
||||||
- Aufbewahrungsrichtlinien pro Mandant
|
|
||||||
- Monitoring und Admin-Dashboards
|
|
||||||
- Federation-Allowlist
|
|
||||||
- optionale Brücken
|
|
||||||
- optionale Aufzeichnung mit Einwilligungsworkflow
|
|
||||||
|
|
||||||
## Offene Entscheidungen
|
|
||||||
|
|
||||||
- Soll Federation initial deaktiviert oder direkt mit Allowlist ausgeliefert werden?
|
|
||||||
- Welche Räume müssen serverseitig durchsuchbar sein und bleiben deshalb unverschlüsselt?
|
|
||||||
- Sollen externe Gäste Matrix-Konten erhalten oder nur temporäre Konferenzzugänge?
|
|
||||||
- Wird Element als sichtbarer Fallback-Client angeboten oder soll alles primär in FEDEO stattfinden?
|
|
||||||
- Welche Mandantenlimits gelten für Speicher, Teilnehmerzahl und Videodauer?
|
|
||||||
|
|
||||||
## Quellen und Standards
|
|
||||||
|
|
||||||
- Matrix Specification: https://spec.matrix.org/
|
|
||||||
- Matrix Application Services: https://matrix.org/docs/older/application-services/
|
|
||||||
- Matrix Bridges: https://www.matrix.org/docs/communities/bridging/
|
|
||||||
- Synapse Worker-Dokumentation: https://matrix-org.github.io/synapse/develop/workers.html
|
|
||||||
- Element Call Self-Hosting: https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md
|
|
||||||
- Element MatrixRTC Konfiguration: https://docs.element.io/latest/element-server-suite-pro/configuring-components/configuring-matrix-rtc/
|
|
||||||
- LiveKit Self-Hosting: https://docs.livekit.io/transport/self-hosting/
|
|
||||||
@@ -6,5 +6,4 @@ Diese Dokumentation unterstützt dich bei der täglichen Nutzung von FEDEO.
|
|||||||
|
|
||||||
- [Bedienung](./bedienung/README.md)
|
- [Bedienung](./bedienung/README.md)
|
||||||
- [Fach- und Technikkonzept für das Lagersystem](./lagersystem-konzept.md)
|
- [Fach- und Technikkonzept für das Lagersystem](./lagersystem-konzept.md)
|
||||||
- [Kommunikationslösung auf Basis des Matrix-Standards](./kommunikationslösung-matrix.md)
|
|
||||||
- [Zentraler Push-Server für Selfhost-Instanzen](./zentraler-push-server.md)
|
- [Zentraler Push-Server für Selfhost-Instanzen](./zentraler-push-server.md)
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
# Kommunikationslösung auf Basis des Matrix-Standards
|
|
||||||
|
|
||||||
Dieser Entwurf beschreibt eine FEDEO-Kommunikationslösung für Chat, Anrufe und Videokonferenzen auf Basis des Matrix-Standards. Ziel ist eine souverän betreibbare Lösung, die Mandantenfähigkeit, Datenschutz, Rechteverwaltung und die bestehenden FEDEO-Workflows berücksichtigt.
|
|
||||||
|
|
||||||
## Zielbild
|
|
||||||
|
|
||||||
FEDEO erhält einen integrierten Kommunikationsbereich, der interne Zusammenarbeit und externe Kommunikation abdeckt:
|
|
||||||
|
|
||||||
- Chat in Einzel-, Gruppen-, Projekt-, Vorgangs- und Kundenräumen
|
|
||||||
- Audioanrufe aus Direktchats, Gruppenräumen und Kontakten
|
|
||||||
- Videokonferenzen mit Bildschirmfreigabe und Einladungslinks
|
|
||||||
- Ende-zu-Ende-verschlüsselte private Kommunikation
|
|
||||||
- revisionsfähige Verknüpfung von relevanten Kommunikationsereignissen mit FEDEO-Objekten
|
|
||||||
- optional föderierte Kommunikation mit externen Matrix-Organisationen
|
|
||||||
|
|
||||||
Matrix wird dabei nicht als isolierter Messenger betrieben, sondern als Kommunikationsschicht neben dem bestehenden FEDEO-Backend.
|
|
||||||
|
|
||||||
## Empfohlene Architektur
|
|
||||||
|
|
||||||
```text
|
|
||||||
Nutzerinnen und Nutzer
|
|
||||||
|
|
|
||||||
| FEDEO Web, Mobile App, optional Element Desktop/Mobile
|
|
||||||
v
|
|
||||||
FEDEO Frontend
|
|
||||||
|
|
|
||||||
| FEDEO API, SSO, Rechte, Objektkontext
|
|
||||||
v
|
|
||||||
FEDEO Backend
|
|
||||||
|
|
|
||||||
| Provisionierung, Webhooks, Audit-Metadaten
|
|
||||||
v
|
|
||||||
Matrix Homeserver
|
|
||||||
|
|
|
||||||
+-- PostgreSQL für Matrix-Daten
|
|
||||||
+-- Redis für Worker und Caches
|
|
||||||
+-- Medien-Repository für Anhänge
|
|
||||||
+-- TURN/STUN für direkte Medienverbindungen
|
|
||||||
+-- MatrixRTC / LiveKit SFU für Gruppenanrufe und Videokonferenzen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kernkomponenten
|
|
||||||
|
|
||||||
| Komponente | Empfehlung | Aufgabe |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Matrix Homeserver | Synapse | Standardnaher, bewährter Homeserver mit guter Betriebsdokumentation |
|
|
||||||
| Matrix Client im FEDEO Web | Matrix JS SDK oder eingebetteter Element-Web-Ausschnitt | Chat, Raumliste, Nachrichten, Reaktionen, Anhänge |
|
|
||||||
| Mobile Integration | Matrix SDK über FEDEO Mobile oder Deep Link zu Element X | Pushfähige mobile Kommunikation |
|
|
||||||
| Identität | OIDC/SSO über FEDEO Auth, perspektivisch Matrix Authentication Service | Einheitlicher Login und zentrale Nutzerverwaltung |
|
|
||||||
| Audio/Video | MatrixRTC mit Element Call und LiveKit SFU | Moderne Anrufe und Videokonferenzen |
|
|
||||||
| NAT Traversal | coturn | STUN/TURN für stabile Medienverbindungen |
|
|
||||||
| Reverse Proxy | bestehender Traefik-Ansatz | TLS, Routing, `.well-known/matrix/*` |
|
|
||||||
| Administration | FEDEO Admin-Oberfläche plus Synapse Admin API | Nutzer, Räume, Richtlinien, Sperren |
|
|
||||||
|
|
||||||
## Betriebsmodell
|
|
||||||
|
|
||||||
Für FEDEO ist ein eigener Matrix-Homeserver pro Installation oder pro großer Betreiberinstanz sinnvoll. Der Matrix-Server sollte nicht öffentlich als offener Registrierungsserver betrieben werden. Nutzer werden ausschließlich durch FEDEO angelegt, aktualisiert und deaktiviert.
|
|
||||||
|
|
||||||
Empfohlene Domains:
|
|
||||||
|
|
||||||
- `app.example.com`: FEDEO Oberfläche
|
|
||||||
- `matrix.example.com`: Matrix Client-Server und Federation API
|
|
||||||
- `call.example.com`: Element Call / MatrixRTC
|
|
||||||
- `livekit.example.com`: LiveKit SFU
|
|
||||||
- `turn.example.com`: TURN/STUN
|
|
||||||
|
|
||||||
Die öffentliche Matrix-Serverkennung kann trotzdem `example.com` lauten. Dafür werden `.well-known/matrix/client` und `.well-known/matrix/server` über Traefik ausgeliefert.
|
|
||||||
|
|
||||||
## Mandantenmodell
|
|
||||||
|
|
||||||
Matrix selbst ist raumbasiert, FEDEO ist mandantenbasiert. Deshalb sollte FEDEO die Mandantenlogik explizit auf Matrix-Räume und Spaces abbilden.
|
|
||||||
|
|
||||||
### Räume und Spaces
|
|
||||||
|
|
||||||
- Pro FEDEO-Mandant wird ein Matrix Space angelegt.
|
|
||||||
- Projekte, Vorgänge, Helpdesk-Konversationen, interne Teams und Kundenkontakte werden als Räume im Mandanten-Space geführt.
|
|
||||||
- Direkträume werden nutzerbezogen angelegt, aber über FEDEO mandantengebunden sichtbar gemacht.
|
|
||||||
- Externe Räume erhalten einen klaren Status, zum Beispiel `intern`, `extern`, `kunde`, `lieferant`.
|
|
||||||
|
|
||||||
### Raumalias-Konvention
|
|
||||||
|
|
||||||
Beispiele:
|
|
||||||
|
|
||||||
- `#tenant-<mandant>-team:example.com`
|
|
||||||
- `#tenant-<mandant>-project-<projekt>:example.com`
|
|
||||||
- `#tenant-<mandant>-ticket-<ticket>:example.com`
|
|
||||||
- `#tenant-<mandant>-customer-<kunde>:example.com`
|
|
||||||
|
|
||||||
Interne technische IDs sollten nicht als sichtbarer Anzeigename genutzt werden. Nutzerinnen und Nutzer sehen sprechende Namen wie `Projekt: Website Relaunch` oder `Kunde: Muster GmbH`.
|
|
||||||
|
|
||||||
## Rechte und Rollen
|
|
||||||
|
|
||||||
FEDEO bleibt führend für Berechtigungen. Matrix übernimmt die technische Durchsetzung im Raum.
|
|
||||||
|
|
||||||
| FEDEO-Rolle | Matrix-Abbildung |
|
|
||||||
| --- | --- |
|
|
||||||
| Mandantenadmin | Space-Admin und Raumadmin |
|
|
||||||
| Teamleitung | Moderatorin oder Moderator in Team- und Projekträumen |
|
|
||||||
| Mitarbeitende | Mitglied mit Schreibrechten |
|
|
||||||
| Externe Kontakte | Eingeschränkte Mitgliedschaft in ausgewählten Räumen |
|
|
||||||
| Automationen | Application-Service- oder Bot-Nutzer mit minimalen Rechten |
|
|
||||||
|
|
||||||
Änderungen an Rollen, Teams oder Mandantenzugehörigkeiten lösen im FEDEO-Backend eine Synchronisation mit Matrix aus. Beim Entzug eines Zugriffs wird die Person aus den betroffenen Räumen entfernt. Bei Ende-zu-Ende-verschlüsselten Räumen muss zusätzlich berücksichtigt werden, dass bereits erhaltene Nachrichten auf Geräten verbleiben können.
|
|
||||||
|
|
||||||
## Chat
|
|
||||||
|
|
||||||
Der Chat wird als erste Ausbaustufe umgesetzt.
|
|
||||||
|
|
||||||
### Funktionen
|
|
||||||
|
|
||||||
- Direktnachrichten
|
|
||||||
- Gruppenräume
|
|
||||||
- Mandanten-, Team-, Projekt- und Vorgangsräume
|
|
||||||
- Datei- und Bildanhänge
|
|
||||||
- Erwähnungen, Reaktionen und Lesestatus
|
|
||||||
- Suche in nicht verschlüsselten Räumen über den Homeserver
|
|
||||||
- lokale Suche in verschlüsselten Räumen über Client-Indizes
|
|
||||||
- Verknüpfung von Nachrichten mit FEDEO-Objekten
|
|
||||||
|
|
||||||
### Integration in FEDEO
|
|
||||||
|
|
||||||
FEDEO sollte keine vollständige Kopie aller Nachrichten in der eigenen Datenbank speichern. Stattdessen speichert FEDEO nur Referenzen:
|
|
||||||
|
|
||||||
- Matrix Raum-ID
|
|
||||||
- Matrix Event-ID
|
|
||||||
- FEDEO Objekt-Typ und Objekt-ID
|
|
||||||
- Zeitstempel
|
|
||||||
- beteiligter FEDEO-Nutzer
|
|
||||||
- optionale Vorschau, falls Datenschutzrichtlinie dies erlaubt
|
|
||||||
|
|
||||||
So bleibt Matrix das Kommunikationssystem, während FEDEO nachvollziehen kann, welche Kommunikation zu welchem Objekt gehört.
|
|
||||||
|
|
||||||
## Audioanrufe
|
|
||||||
|
|
||||||
Einzelanrufe können direkt über Matrix-VoIP in Direktchats gestartet werden. Der FEDEO-Client zeigt dafür in Kontakt-, Kunden-, Mitarbeitenden- und Chatansichten einen Anruf-Button.
|
|
||||||
|
|
||||||
### Anforderungen
|
|
||||||
|
|
||||||
- WebRTC-Unterstützung im Browser
|
|
||||||
- STUN/TURN über coturn
|
|
||||||
- Geräteauswahl für Mikrofon und Lautsprecher
|
|
||||||
- Anrufbenachrichtigung im Web und mobil
|
|
||||||
- Statusanzeige `verfügbar`, `beschäftigt`, `im Anruf`, `abwesend`
|
|
||||||
|
|
||||||
Für klassische Telefonie kann später ein SIP-Gateway ergänzt werden. Das sollte jedoch getrennt von der ersten Matrix-Einführung betrachtet werden, damit Chat und WebRTC-Kommunikation nicht durch Telefoniekomplexität ausgebremst werden.
|
|
||||||
|
|
||||||
## Videokonferenzen
|
|
||||||
|
|
||||||
Für Gruppenanrufe und Videokonferenzen wird MatrixRTC mit Element Call und LiveKit empfohlen. Matrix übernimmt dabei Raumzustand, Identität, Berechtigungen und Signalisierung; LiveKit übernimmt als SFU die effiziente Medienverteilung.
|
|
||||||
|
|
||||||
### Funktionen
|
|
||||||
|
|
||||||
- Videokonferenzen aus Matrix-Räumen
|
|
||||||
- spontane Besprechungen aus Projekten, Vorgängen oder Kundenakten
|
|
||||||
- Bildschirmfreigabe
|
|
||||||
- Einladungslink für externe Gäste
|
|
||||||
- Wartebereich für externe Gäste
|
|
||||||
- Moderationsrechte für Stummschalten, Entfernen und Raumverwaltung
|
|
||||||
- optionale Aufzeichnung erst in einer späteren, gesondert freizugebenden Ausbaustufe
|
|
||||||
|
|
||||||
### Konfiguration
|
|
||||||
|
|
||||||
Clients finden den MatrixRTC-Dienst über `.well-known/matrix/client`. Dort wird der LiveKit-JWT-Dienst als `org.matrix.msc4143.rtc_foci` angekündigt. Diese Datei muss öffentlich lesbar sein, als JSON ausgeliefert werden und CORS für Webclients erlauben.
|
|
||||||
|
|
||||||
Beispiel:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "https://matrix.example.com"
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "https://call.example.com/livekit/jwt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Authentifizierung und Nutzerverwaltung
|
|
||||||
|
|
||||||
FEDEO sollte Identität und Lebenszyklus der Nutzer zentral steuern.
|
|
||||||
|
|
||||||
### Empfohlener Ablauf
|
|
||||||
|
|
||||||
1. Nutzer wird in FEDEO angelegt.
|
|
||||||
2. FEDEO erzeugt oder aktualisiert den Matrix-Nutzer.
|
|
||||||
3. FEDEO weist den Nutzer den passenden Spaces und Räumen zu.
|
|
||||||
4. Login erfolgt über FEDEO SSO/OIDC.
|
|
||||||
5. Deaktivierung in FEDEO deaktiviert auch den Matrix-Zugang und entfernt Raumzugriffe.
|
|
||||||
|
|
||||||
Die Matrix User-ID sollte stabil und nicht personenbezogen änderungsanfällig sein:
|
|
||||||
|
|
||||||
```text
|
|
||||||
@u_<fedeo_user_id>:example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Anzeigename kann weiterhin den echten Namen enthalten und bei Änderungen synchronisiert werden.
|
|
||||||
|
|
||||||
## Datenschutz und Compliance
|
|
||||||
|
|
||||||
Matrix erlaubt starke Datenschutzkonzepte, erfordert aber klare Betriebsregeln.
|
|
||||||
|
|
||||||
### Empfehlungen
|
|
||||||
|
|
||||||
- Ende-zu-Ende-Verschlüsselung für Direktnachrichten und vertrauliche Projekträume aktivieren.
|
|
||||||
- Nicht verschlüsselte Räume nur dort nutzen, wo serverseitige Suche, Archivierung oder Compliance-Funktionen ausdrücklich benötigt werden.
|
|
||||||
- Medienaufbewahrung mandantenweit konfigurierbar machen.
|
|
||||||
- Externe Gäste optisch klar kennzeichnen.
|
|
||||||
- Federation standardmäßig deaktivieren oder auf erlaubte Domains beschränken.
|
|
||||||
- Aufzeichnungen von Videokonferenzen nur mit expliziter Einwilligung und sichtbarem Status erlauben.
|
|
||||||
- Administrative Zugriffe protokollieren.
|
|
||||||
- Klare Löschfristen für Räume, Anhänge und Audit-Referenzen definieren.
|
|
||||||
|
|
||||||
## Federation
|
|
||||||
|
|
||||||
Matrix kann mit anderen Homeservern föderieren. Für FEDEO sollte Federation als kontrollierbare Option umgesetzt werden.
|
|
||||||
|
|
||||||
### Betriebsmodi
|
|
||||||
|
|
||||||
| Modus | Beschreibung | Empfehlung |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| geschlossen | Keine Federation, nur interne Nutzer und explizite Gäste | Standard für kleine Installationen |
|
|
||||||
| allowlist | Federation nur mit freigegebenen Domains | Empfehlung für B2B-Kommunikation |
|
|
||||||
| offen | Federation mit beliebigen Matrix-Servern | Nur für bewusst öffentliche Communities |
|
|
||||||
|
|
||||||
Für steuer-, kunden- und projektnahe Kommunikation ist `allowlist` der beste Zielmodus.
|
|
||||||
|
|
||||||
## Brücken zu anderen Systemen
|
|
||||||
|
|
||||||
Matrix unterstützt Brücken zu anderen Kommunikationsdiensten. Für FEDEO sind Brücken nützlich, sollten aber nicht zur ersten Produktstufe gehören.
|
|
||||||
|
|
||||||
Mögliche spätere Erweiterungen:
|
|
||||||
|
|
||||||
- E-Mail-Brücke für Helpdesk- oder Kundenkommunikation
|
|
||||||
- Slack- oder Teams-Brücke für externe Projektpartner
|
|
||||||
- WhatsApp- oder SMS-Brücke nur nach gesonderter Datenschutzprüfung
|
|
||||||
- SIP-Brücke für Telefonie
|
|
||||||
|
|
||||||
Brücken müssen pro Mandant aktivierbar sein und brauchen klare Hinweise, welche Daten an externe Dienste fließen.
|
|
||||||
|
|
||||||
## FEDEO-Produktoberfläche
|
|
||||||
|
|
||||||
Die Kommunikation sollte in FEDEO an zwei Stellen sichtbar sein.
|
|
||||||
|
|
||||||
### Globaler Kommunikationsbereich
|
|
||||||
|
|
||||||
- Raumliste
|
|
||||||
- Direktnachrichten
|
|
||||||
- Suche
|
|
||||||
- Anrufe
|
|
||||||
- laufende Besprechungen
|
|
||||||
- Benachrichtigungen
|
|
||||||
|
|
||||||
### Objektbezogene Kommunikation
|
|
||||||
|
|
||||||
In Projekten, Kunden, Vorgängen, Helpdesk-Tickets und Dokumenten erscheint ein Kommunikations-Tab:
|
|
||||||
|
|
||||||
- zugeordneter Raum
|
|
||||||
- relevante Nachrichtenreferenzen
|
|
||||||
- Start von Chat, Anruf oder Besprechung
|
|
||||||
- Teilnehmerverwaltung entsprechend FEDEO-Rechten
|
|
||||||
|
|
||||||
So bleibt Kommunikation dort, wo die Arbeit stattfindet.
|
|
||||||
|
|
||||||
## Backend-Integration
|
|
||||||
|
|
||||||
Das FEDEO-Backend erhält ein Kommunikationsmodul mit folgenden Aufgaben:
|
|
||||||
|
|
||||||
- Matrix-Nutzer provisionieren
|
|
||||||
- Spaces und Räume anlegen
|
|
||||||
- Raum-Mitgliedschaften synchronisieren
|
|
||||||
- Matrix-Event-Webhooks empfangen
|
|
||||||
- FEDEO-Objekte mit Matrix-Räumen verknüpfen
|
|
||||||
- Benachrichtigungseinstellungen verwalten
|
|
||||||
- Admin-Aktionen auditieren
|
|
||||||
|
|
||||||
Technisch kann dies über Matrix Admin API, Client-Server API und Application Services erfolgen. Für Automationen empfiehlt sich ein eigener Application Service, weil er reservierte Nutzer- und Raum-Namensräume sauber verwalten kann.
|
|
||||||
|
|
||||||
## Deployment-Erweiterung
|
|
||||||
|
|
||||||
Der bestehende Docker-/Traefik-Ansatz kann um folgende Dienste erweitert werden:
|
|
||||||
|
|
||||||
- `matrix-synapse`
|
|
||||||
- `matrix-db` oder gemeinsame PostgreSQL-Instanz mit getrennter Datenbank
|
|
||||||
- `redis`
|
|
||||||
- `coturn`
|
|
||||||
- `element-web` optional als Fallback-Client
|
|
||||||
- `element-call`
|
|
||||||
- `livekit`
|
|
||||||
- `matrix-rtc-jwt-service`
|
|
||||||
|
|
||||||
Für produktive Installationen sollte Matrix eine eigene PostgreSQL-Datenbank erhalten. Medien sollten in S3-kompatiblen Speicher ausgelagert werden, damit große Anhänge und Konferenzartefakte nicht den Applikationsserver füllen.
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
Wichtige Kennzahlen:
|
|
||||||
|
|
||||||
- aktive Nutzerinnen und Nutzer
|
|
||||||
- Anzahl Räume pro Mandant
|
|
||||||
- Nachrichtenrate
|
|
||||||
- Medien-Speicherverbrauch
|
|
||||||
- Zustellverzögerung
|
|
||||||
- fehlgeschlagene Anrufe
|
|
||||||
- LiveKit Paketverlust, Latenz und Teilnehmerzahl
|
|
||||||
- TURN-Nutzung
|
|
||||||
- Federation-Fehler
|
|
||||||
|
|
||||||
Logs von FEDEO, Synapse, LiveKit, coturn und Traefik sollten über eine gemeinsame Korrelation, zum Beispiel Request-ID oder Nutzer-ID, untersuchbar sein.
|
|
||||||
|
|
||||||
## Risiken und Gegenmaßnahmen
|
|
||||||
|
|
||||||
| Risiko | Gegenmaßnahme |
|
|
||||||
| --- | --- |
|
|
||||||
| Komplexität durch zwei Systeme | FEDEO bleibt führend für Nutzer, Rechte und Objektbezug |
|
|
||||||
| Datenschutz bei externen Räumen | Externe Kennzeichnung, Federation-Allowlist, Mandantenrichtlinien |
|
|
||||||
| E2EE erschwert Suche und Archivierung | Raumtyp bewusst wählen, lokale Suche, Metadatenreferenzen statt Vollkopie |
|
|
||||||
| Medienverbindungen scheitern in Firmennetzen | coturn sauber betreiben, UDP und TCP/TLS-Fallback anbieten |
|
|
||||||
| Betriebskosten durch Video | LiveKit skalierbar betreiben, Limits pro Mandant definieren |
|
|
||||||
| Gästezugriff wird unübersichtlich | Einladungslinks mit Ablaufdatum, Wartebereich, Moderationsrechte |
|
|
||||||
|
|
||||||
## Umsetzung in Phasen
|
|
||||||
|
|
||||||
### Phase 1: Fundament und Chat
|
|
||||||
|
|
||||||
- Synapse mit PostgreSQL, Redis, Traefik und `.well-known` betreiben
|
|
||||||
- FEDEO-Nutzer zu Matrix synchronisieren
|
|
||||||
- Mandanten-Spaces und erste Teamräume anlegen
|
|
||||||
- Chat im FEDEO-Frontend integrieren
|
|
||||||
- Benachrichtigungen und Raumreferenzen speichern
|
|
||||||
|
|
||||||
### Phase 2: Objektbezogene Kommunikation
|
|
||||||
|
|
||||||
- Räume automatisch für Projekte, Vorgänge und Kunden anlegen
|
|
||||||
- Kommunikations-Tab in FEDEO-Objekten ergänzen
|
|
||||||
- Rechteänderungen aus FEDEO nach Matrix synchronisieren
|
|
||||||
- externe Gäste einladen und kennzeichnen
|
|
||||||
|
|
||||||
### Phase 3: Audio und Video
|
|
||||||
|
|
||||||
- coturn bereitstellen
|
|
||||||
- MatrixRTC, Element Call und LiveKit integrieren
|
|
||||||
- Anruf- und Videobuttons in Chat, Kontakten und Projekten ergänzen
|
|
||||||
- Gäste-Links und Wartebereich umsetzen
|
|
||||||
|
|
||||||
### Phase 4: Compliance und Skalierung
|
|
||||||
|
|
||||||
- Aufbewahrungsrichtlinien pro Mandant
|
|
||||||
- Monitoring und Admin-Dashboards
|
|
||||||
- Federation-Allowlist
|
|
||||||
- optionale Brücken
|
|
||||||
- optionale Aufzeichnung mit Einwilligungsworkflow
|
|
||||||
|
|
||||||
## Offene Entscheidungen
|
|
||||||
|
|
||||||
- Soll Federation initial deaktiviert oder direkt mit Allowlist ausgeliefert werden?
|
|
||||||
- Welche Räume müssen serverseitig durchsuchbar sein und bleiben deshalb unverschlüsselt?
|
|
||||||
- Sollen externe Gäste Matrix-Konten erhalten oder nur temporäre Konferenzzugänge?
|
|
||||||
- Wird Element als sichtbarer Fallback-Client angeboten oder soll alles primär in FEDEO stattfinden?
|
|
||||||
- Welche Mandantenlimits gelten für Speicher, Teilnehmerzahl und Videodauer?
|
|
||||||
|
|
||||||
## Quellen und Standards
|
|
||||||
|
|
||||||
- Matrix Specification: https://spec.matrix.org/
|
|
||||||
- Matrix Application Services: https://matrix.org/docs/older/application-services/
|
|
||||||
- Matrix Bridges: https://www.matrix.org/docs/communities/bridging/
|
|
||||||
- Synapse Worker-Dokumentation: https://matrix-org.github.io/synapse/develop/workers.html
|
|
||||||
- Element Call Self-Hosting: https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md
|
|
||||||
- Element MatrixRTC Konfiguration: https://docs.element.io/latest/element-server-suite-pro/configuring-components/configuring-matrix-rtc/
|
|
||||||
- LiveKit Self-Hosting: https://docs.livekit.io/transport/self-hosting/
|
|
||||||
@@ -352,8 +352,8 @@ const links = computed(() => {
|
|||||||
icon: "i-heroicons-phone",
|
icon: "i-heroicons-phone",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Matrix-Setup",
|
label: "Chat",
|
||||||
to: "/communication",
|
to: "/communication/chat",
|
||||||
icon: "i-heroicons-chat-bubble-left-right",
|
icon: "i-heroicons-chat-bubble-left-right",
|
||||||
},
|
},
|
||||||
featureEnabled("export") ? {
|
featureEnabled("export") ? {
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ export default defineNuxtConfig({
|
|||||||
public: {
|
public: {
|
||||||
apiBase: '',
|
apiBase: '',
|
||||||
pdfLicense: '',
|
pdfLicense: '',
|
||||||
matrixElementUrl: process.env.NUXT_PUBLIC_MATRIX_ELEMENT_URL || 'http://localhost:8080'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
131
frontend/package-lock.json
generated
131
frontend/package-lock.json
generated
@@ -74,7 +74,6 @@
|
|||||||
"image-js": "^1.1.0",
|
"image-js": "^1.1.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"livekit-client": "^2.19.0",
|
|
||||||
"maplibre-gl": "^4.7.0",
|
"maplibre-gl": "^4.7.0",
|
||||||
"nuxt-editorjs": "^1.0.4",
|
"nuxt-editorjs": "^1.0.4",
|
||||||
"nuxt-viewport": "^2.0.6",
|
"nuxt-viewport": "^2.0.6",
|
||||||
@@ -1867,12 +1866,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@bufbuild/protobuf": {
|
|
||||||
"version": "1.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz",
|
|
||||||
"integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==",
|
|
||||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
|
||||||
},
|
|
||||||
"node_modules/@capacitor-community/bluetooth-le": {
|
"node_modules/@capacitor-community/bluetooth-le": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.3.0.tgz",
|
||||||
@@ -3167,21 +3160,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@livekit/mutex": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/@livekit/protocol": {
|
|
||||||
"version": "1.45.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.45.8.tgz",
|
|
||||||
"integrity": "sha512-Q+l57E7w/xxOBFVWzdX5rkAZO7ffyF+rlDzNUYq2SU114+5aTyCq+PK4unaEVDNd4952Af7wteKr3sOgasGuaA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@bufbuild/protobuf": "^1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mapbox/geojson-rewind": {
|
"node_modules/@mapbox/geojson-rewind": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||||
@@ -3763,18 +3741,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/cli/node_modules/commander": {
|
|
||||||
"version": "13.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
|
||||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nuxt/devalue": {
|
"node_modules/@nuxt/devalue": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz",
|
||||||
@@ -8719,13 +8685,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/dom-mediacapture-record": {
|
|
||||||
"version": "1.0.22",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
|
|
||||||
"integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -12221,6 +12180,7 @@
|
|||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
@@ -14238,15 +14198,6 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jose": {
|
|
||||||
"version": "6.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
|
||||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/panva"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jpeg-js": {
|
"node_modules/jpeg-js": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||||
@@ -14822,26 +14773,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/livekit-client": {
|
|
||||||
"version": "2.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.19.0.tgz",
|
|
||||||
"integrity": "sha512-aolY1XDAtx0nHKBNm29W9OhzBnSz1CP5kq3phvRhFfi1NbvMXs8tcACjAkZTnIKgihkp+BiJScZZ3tZv0Gz8sA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@livekit/mutex": "1.1.1",
|
|
||||||
"@livekit/protocol": "1.45.8",
|
|
||||||
"events": "^3.3.0",
|
|
||||||
"jose": "^6.1.0",
|
|
||||||
"loglevel": "^1.9.2",
|
|
||||||
"sdp-transform": "^2.15.0",
|
|
||||||
"tslib": "2.8.1",
|
|
||||||
"typed-emitter": "^2.1.0",
|
|
||||||
"webrtc-adapter": "9.0.5"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/dom-mediacapture-record": "^1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/local-pkg": {
|
"node_modules/local-pkg": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||||
@@ -14916,19 +14847,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/loglevel": {
|
|
||||||
"version": "1.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
|
||||||
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -18523,16 +18441,6 @@
|
|||||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/rxjs": {
|
|
||||||
"version": "7.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
|
||||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-array-concat": {
|
"node_modules/safe-array-concat": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||||
@@ -18690,21 +18598,6 @@
|
|||||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sdp": {
|
|
||||||
"version": "3.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz",
|
|
||||||
"integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/sdp-transform": {
|
|
||||||
"version": "2.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz",
|
|
||||||
"integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"sdp-verify": "checker.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/secure-json-parse": {
|
"node_modules/secure-json-parse": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||||
@@ -20269,15 +20162,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typed-emitter": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"rxjs": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -21832,19 +21716,6 @@
|
|||||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/webrtc-adapter": {
|
|
||||||
"version": "9.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz",
|
|
||||||
"integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"sdp": "^3.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0",
|
|
||||||
"npm": ">=3.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
|||||||
@@ -87,7 +87,6 @@
|
|||||||
"image-js": "^1.1.0",
|
"image-js": "^1.1.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"livekit-client": "^2.19.0",
|
|
||||||
"maplibre-gl": "^4.7.0",
|
"maplibre-gl": "^4.7.0",
|
||||||
"nuxt-editorjs": "^1.0.4",
|
"nuxt-editorjs": "^1.0.4",
|
||||||
"nuxt-viewport": "^2.0.6",
|
"nuxt-viewport": "^2.0.6",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const serviceLabels: Record<string, string> = {
|
|||||||
backend: "Backend",
|
backend: "Backend",
|
||||||
database: "Datenbank",
|
database: "Datenbank",
|
||||||
nodeExporter: "Node Exporter",
|
nodeExporter: "Node Exporter",
|
||||||
matrix: "Matrix",
|
|
||||||
minio: "Dateispeicher",
|
minio: "Dateispeicher",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,501 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const toast = useToast()
|
await navigateTo("/communication/chat", { replace: true })
|
||||||
const { $api } = useNuxtApp()
|
|
||||||
|
|
||||||
const status = ref(null)
|
|
||||||
const identity = ref(null)
|
|
||||||
const tenantSpace = ref(null)
|
|
||||||
const generalRoom = ref(null)
|
|
||||||
const provisionResult = ref(null)
|
|
||||||
const tenantSpaceProvisionResult = ref(null)
|
|
||||||
const generalRoomProvisionResult = ref(null)
|
|
||||||
const loading = ref(false)
|
|
||||||
const provisioning = ref(false)
|
|
||||||
const tenantSpaceProvisioning = ref(false)
|
|
||||||
const generalRoomProvisioning = ref(false)
|
|
||||||
const lastUpdated = ref(null)
|
|
||||||
|
|
||||||
const statusItems = computed(() => [
|
|
||||||
{
|
|
||||||
label: "Konfiguration",
|
|
||||||
value: status.value?.configured ? "Aktiv" : "Nicht aktiv",
|
|
||||||
icon: status.value?.configured ? "i-heroicons-check-circle" : "i-heroicons-x-circle",
|
|
||||||
color: status.value?.configured ? "success" : "error"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Homeserver",
|
|
||||||
value: status.value?.homeserverUrl || "-",
|
|
||||||
icon: "i-heroicons-server-stack",
|
|
||||||
color: "neutral"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Servername",
|
|
||||||
value: status.value?.serverName || "-",
|
|
||||||
icon: "i-heroicons-identification",
|
|
||||||
color: "neutral"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Provisionierung",
|
|
||||||
value: status.value?.provisioningConfigured ? "Bereit" : "Nicht eingerichtet",
|
|
||||||
icon: status.value?.provisioningConfigured ? "i-heroicons-key" : "i-heroicons-exclamation-triangle",
|
|
||||||
color: status.value?.provisioningConfigured ? "success" : "warning"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Erreichbarkeit",
|
|
||||||
value: status.value?.reachable ? "Erreichbar" : "Nicht erreichbar",
|
|
||||||
icon: status.value?.reachable ? "i-heroicons-signal" : "i-heroicons-signal-slash",
|
|
||||||
color: status.value?.reachable ? "success" : "error"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Audio/Video",
|
|
||||||
value: status.value?.calls?.configured ? "Bereit" : "Nicht eingerichtet",
|
|
||||||
icon: status.value?.calls?.configured ? "i-heroicons-video-camera" : "i-heroicons-video-camera-slash",
|
|
||||||
color: status.value?.calls?.configured ? "success" : "warning"
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const canUseMatrixChat = computed(() =>
|
|
||||||
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
|
||||||
)
|
|
||||||
|
|
||||||
const loadMatrixInfo = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const [statusRes, identityRes, tenantSpaceRes, generalRoomRes] = await Promise.all([
|
|
||||||
$api("/api/communication/matrix/status"),
|
|
||||||
$api("/api/communication/matrix/me"),
|
|
||||||
$api("/api/communication/matrix/tenant-space"),
|
|
||||||
$api("/api/communication/matrix/rooms/general")
|
|
||||||
])
|
|
||||||
|
|
||||||
status.value = statusRes
|
|
||||||
identity.value = identityRes
|
|
||||||
tenantSpace.value = tenantSpaceRes
|
|
||||||
generalRoom.value = generalRoomRes
|
|
||||||
lastUpdated.value = new Date()
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
toast.add({
|
|
||||||
title: "Matrix-Status konnte nicht geladen werden",
|
|
||||||
color: "error"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const provisionMatrixAccount = async () => {
|
|
||||||
provisioning.value = true
|
|
||||||
try {
|
|
||||||
const res = await $api("/api/communication/matrix/me/provision", {
|
|
||||||
method: "POST"
|
|
||||||
})
|
|
||||||
|
|
||||||
provisionResult.value = res
|
|
||||||
identity.value = {
|
|
||||||
...identity.value,
|
|
||||||
matrixUserId: res.matrixUserId,
|
|
||||||
displayName: res.displayName
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.add({
|
|
||||||
title: res.alreadyExisted ? "Matrix-Konto ist bereits vorhanden" : "Matrix-Konto erstellt",
|
|
||||||
color: "success"
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
toast.add({
|
|
||||||
title: "Matrix-Konto konnte nicht erstellt werden",
|
|
||||||
color: "error"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
provisioning.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const provisionTenantSpace = async () => {
|
|
||||||
tenantSpaceProvisioning.value = true
|
|
||||||
try {
|
|
||||||
const res = await $api("/api/communication/matrix/tenant-space/provision", {
|
|
||||||
method: "POST"
|
|
||||||
})
|
|
||||||
|
|
||||||
tenantSpaceProvisionResult.value = res
|
|
||||||
tenantSpace.value = {
|
|
||||||
tenantId: res.tenantId,
|
|
||||||
tenantName: res.tenantName,
|
|
||||||
alias: res.alias,
|
|
||||||
exists: true,
|
|
||||||
roomId: res.roomId,
|
|
||||||
servers: res.servers || []
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.add({
|
|
||||||
title: res.alreadyExisted ? "Mandanten-Space ist bereits vorhanden" : "Mandanten-Space erstellt",
|
|
||||||
color: "success"
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
toast.add({
|
|
||||||
title: "Mandanten-Space konnte nicht erstellt werden",
|
|
||||||
color: "error"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
tenantSpaceProvisioning.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const provisionGeneralRoom = async () => {
|
|
||||||
generalRoomProvisioning.value = true
|
|
||||||
try {
|
|
||||||
const res = await $api("/api/communication/matrix/rooms/general/provision", {
|
|
||||||
method: "POST"
|
|
||||||
})
|
|
||||||
|
|
||||||
generalRoomProvisionResult.value = res
|
|
||||||
generalRoom.value = {
|
|
||||||
tenantId: res.tenantId,
|
|
||||||
tenantName: res.tenantName,
|
|
||||||
key: res.key,
|
|
||||||
name: res.name,
|
|
||||||
alias: res.alias,
|
|
||||||
exists: true,
|
|
||||||
roomId: res.roomId,
|
|
||||||
servers: res.servers || []
|
|
||||||
}
|
|
||||||
tenantSpace.value = {
|
|
||||||
...tenantSpace.value,
|
|
||||||
exists: true,
|
|
||||||
roomId: res.parentSpaceRoomId || tenantSpace.value?.roomId
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.add({
|
|
||||||
title: res.alreadyExisted ? "Allgemeiner Chat ist bereits vorhanden" : "Allgemeiner Chat erstellt",
|
|
||||||
color: "success"
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
toast.add({
|
|
||||||
title: "Allgemeiner Chat konnte nicht erstellt werden",
|
|
||||||
color: "error"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
generalRoomProvisioning.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDateTime = (value) => {
|
|
||||||
if (!value) return "-"
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat("de-DE", {
|
|
||||||
dateStyle: "short",
|
|
||||||
timeStyle: "short"
|
|
||||||
}).format(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadMatrixInfo()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
<div />
|
||||||
<div class="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6">
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-semibold text-highlighted">
|
|
||||||
Kommunikation
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-muted">
|
|
||||||
Matrix-Verbindung und persönliche Kommunikationsidentität.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-arrow-path"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
:loading="loading"
|
|
||||||
@click="loadMatrixInfo"
|
|
||||||
>
|
|
||||||
Aktualisieren
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<UCard
|
|
||||||
v-for="item in statusItems"
|
|
||||||
:key="item.label"
|
|
||||||
:ui="{ root: 'rounded-lg', body: 'p-4 sm:p-4' }"
|
|
||||||
>
|
|
||||||
<div class="flex items-start gap-3">
|
|
||||||
<UIcon
|
|
||||||
:name="item.icon"
|
|
||||||
class="mt-0.5 size-5 shrink-0"
|
|
||||||
:class="{
|
|
||||||
'text-success': item.color === 'success',
|
|
||||||
'text-error': item.color === 'error',
|
|
||||||
'text-warning': item.color === 'warning',
|
|
||||||
'text-muted': item.color === 'neutral'
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
{{ item.label }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 break-words text-sm font-medium text-highlighted">
|
|
||||||
{{ item.value }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<UCard :ui="{ root: 'rounded-lg' }">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UIcon name="i-heroicons-user-circle" class="size-5 text-primary" />
|
|
||||||
<h2 class="text-base font-semibold text-highlighted">
|
|
||||||
Eigene Matrix-Identität
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Matrix-ID
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
||||||
{{ identity?.matrixUserId || "-" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Anzeigename
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-sm text-highlighted">
|
|
||||||
{{ identity?.displayName || "-" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="provisionResult"
|
|
||||||
icon="i-heroicons-check-circle"
|
|
||||||
color="success"
|
|
||||||
variant="soft"
|
|
||||||
:title="provisionResult.alreadyExisted ? 'Matrix-Konto vorhanden' : 'Matrix-Konto erstellt'"
|
|
||||||
:description="provisionResult.matrixUserId"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="status && !status.reachable"
|
|
||||||
icon="i-heroicons-exclamation-triangle"
|
|
||||||
color="error"
|
|
||||||
variant="soft"
|
|
||||||
title="Matrix-Homeserver nicht erreichbar"
|
|
||||||
:description="status.error || 'Bitte prüfe den lokalen Matrix-Stack und die Backend-Konfiguration.'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-else-if="status && !status.provisioningConfigured"
|
|
||||||
icon="i-heroicons-key"
|
|
||||||
color="warning"
|
|
||||||
variant="soft"
|
|
||||||
title="Matrix-Provisionierung nicht eingerichtet"
|
|
||||||
description="Bitte setze MATRIX_REGISTRATION_SHARED_SECRET in der Backend-Umgebung."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<p class="text-xs text-muted">
|
|
||||||
Aktualisiert: {{ formatDateTime(lastUpdated) }}
|
|
||||||
</p>
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-user-plus"
|
|
||||||
:loading="provisioning"
|
|
||||||
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
||||||
@click="provisionMatrixAccount"
|
|
||||||
>
|
|
||||||
Matrix-Konto erstellen
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard :ui="{ root: 'rounded-lg' }">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UIcon name="i-heroicons-building-office-2" class="size-5 text-primary" />
|
|
||||||
<h2 class="text-base font-semibold text-highlighted">
|
|
||||||
Mandanten-Space
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Alias
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
||||||
{{ tenantSpace?.alias || "-" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Status
|
|
||||||
</p>
|
|
||||||
<UBadge
|
|
||||||
class="mt-1"
|
|
||||||
:color="tenantSpace?.exists ? 'success' : 'neutral'"
|
|
||||||
variant="soft"
|
|
||||||
>
|
|
||||||
{{ tenantSpace?.exists ? "Vorhanden" : "Noch nicht erstellt" }}
|
|
||||||
</UBadge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Raum-ID
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
||||||
{{ tenantSpace?.roomId || "-" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="tenantSpaceProvisionResult"
|
|
||||||
icon="i-heroicons-check-circle"
|
|
||||||
color="success"
|
|
||||||
variant="soft"
|
|
||||||
:title="tenantSpaceProvisionResult.alreadyExisted ? 'Mandanten-Space vorhanden' : 'Mandanten-Space erstellt'"
|
|
||||||
:description="tenantSpaceProvisionResult.alias"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex flex-wrap justify-end gap-2">
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-plus"
|
|
||||||
:loading="tenantSpaceProvisioning"
|
|
||||||
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
||||||
@click="provisionTenantSpace"
|
|
||||||
>
|
|
||||||
Mandanten-Space erstellen
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard :ui="{ root: 'rounded-lg' }">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5 text-primary" />
|
|
||||||
<h2 class="text-base font-semibold text-highlighted">
|
|
||||||
Allgemeiner Chat
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Alias
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
||||||
{{ generalRoom?.alias || "-" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Status
|
|
||||||
</p>
|
|
||||||
<UBadge
|
|
||||||
class="mt-1"
|
|
||||||
:color="generalRoom?.exists ? 'success' : 'neutral'"
|
|
||||||
variant="soft"
|
|
||||||
>
|
|
||||||
{{ generalRoom?.exists ? "Vorhanden" : "Noch nicht erstellt" }}
|
|
||||||
</UBadge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
|
||||||
Raum-ID
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
||||||
{{ generalRoom?.roomId || "-" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="generalRoomProvisionResult"
|
|
||||||
icon="i-heroicons-check-circle"
|
|
||||||
color="success"
|
|
||||||
variant="soft"
|
|
||||||
:title="generalRoomProvisionResult.alreadyExisted ? 'Allgemeiner Chat vorhanden' : 'Allgemeiner Chat erstellt'"
|
|
||||||
:description="generalRoomProvisionResult.alias"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex flex-wrap justify-end gap-2">
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-plus"
|
|
||||||
:loading="generalRoomProvisioning"
|
|
||||||
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
||||||
@click="provisionGeneralRoom"
|
|
||||||
>
|
|
||||||
Allgemeinen Chat erstellen
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
v-if="generalRoom?.exists"
|
|
||||||
to="/communication/chat"
|
|
||||||
icon="i-heroicons-chat-bubble-left-right"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Zum Chat
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UCard :ui="{ root: 'rounded-lg' }">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UIcon name="i-heroicons-video-camera" class="size-5 text-primary" />
|
|
||||||
<h2 class="text-base font-semibold text-highlighted">
|
|
||||||
Nächste Ausbaustufe
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="space-y-3 text-sm text-muted">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<UIcon name="i-heroicons-building-office-2" class="mt-0.5 size-4 shrink-0" />
|
|
||||||
<span>Team- und Projekt-Räume im Mandanten-Space anlegen.</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<UIcon name="i-heroicons-users" class="mt-0.5 size-4 shrink-0" />
|
|
||||||
<span>FEDEO-Nutzer in Matrix-Räume synchronisieren.</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<UIcon name="i-heroicons-chat-bubble-left-right" class="mt-0.5 size-4 shrink-0" />
|
|
||||||
<span>Nativen FEDEO-Chat schrittweise an Matrix-Sync anbinden.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
197
matrix/README.md
197
matrix/README.md
@@ -1,197 +0,0 @@
|
|||||||
# Matrix-Stack in der FEDEO Compose
|
|
||||||
|
|
||||||
Der Matrix-Stack liegt in derselben `docker-compose.yml` wie FEDEO und ist über das Compose-Profil `matrix` aktivierbar.
|
|
||||||
|
|
||||||
## Enthaltene Dienste
|
|
||||||
|
|
||||||
- `matrix-db`: PostgreSQL für Synapse
|
|
||||||
- `matrix-redis`: Redis für Synapse und LiveKit
|
|
||||||
- `matrix-synapse`: Matrix Homeserver
|
|
||||||
- `matrix-well-known`: Auslieferung von `.well-known/matrix/client` und `.well-known/matrix/server`
|
|
||||||
- `matrix-turn`: coturn für stabile WebRTC-Verbindungen
|
|
||||||
- `matrix-livekit`: LiveKit SFU für MatrixRTC-Konferenzen
|
|
||||||
- `matrix-rtc-jwt`: MatrixRTC Authorization Service für LiveKit-JWTs
|
|
||||||
|
|
||||||
## Vorbereitung
|
|
||||||
|
|
||||||
Lege im Repo eine `.env` auf Basis von `.env.example` an und passe mindestens diese Werte an:
|
|
||||||
|
|
||||||
- `MATRIX_SERVER_NAME`
|
|
||||||
- `MATRIX_HOMESERVER_HOST`
|
|
||||||
- `MATRIX_RTC_HOST`
|
|
||||||
- `MATRIX_TURN_HOST`
|
|
||||||
- `MATRIX_POSTGRES_PASSWORD`
|
|
||||||
- `MATRIX_TURN_SHARED_SECRET`
|
|
||||||
- `LIVEKIT_KEY`
|
|
||||||
- `LIVEKIT_SECRET`
|
|
||||||
|
|
||||||
Passe außerdem die Dateien in `matrix/well-known/` an, falls die Domains nicht `fedeo.de`, `matrix.fedeo.de` und `call.fedeo.de` heißen.
|
|
||||||
|
|
||||||
## Synapse-Konfiguration erzeugen
|
|
||||||
|
|
||||||
Synapse benötigt vor dem ersten Start eine generierte `homeserver.yaml`. Der Befehl bleibt innerhalb derselben Compose:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --profile matrix run --rm \
|
|
||||||
-e SYNAPSE_SERVER_NAME="${MATRIX_SERVER_NAME}" \
|
|
||||||
-e SYNAPSE_REPORT_STATS=no \
|
|
||||||
matrix-synapse generate
|
|
||||||
```
|
|
||||||
|
|
||||||
Danach `matrix/synapse/homeserver.yaml` prüfen und mindestens diese Punkte setzen:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
public_baseurl: "https://matrix.fedeo.de/"
|
|
||||||
|
|
||||||
database:
|
|
||||||
name: psycopg2
|
|
||||||
args:
|
|
||||||
user: synapse
|
|
||||||
password: "<MATRIX_POSTGRES_PASSWORD>"
|
|
||||||
database: synapse
|
|
||||||
host: matrix-db
|
|
||||||
cp_min: 5
|
|
||||||
cp_max: 10
|
|
||||||
|
|
||||||
redis:
|
|
||||||
enabled: true
|
|
||||||
host: matrix-redis
|
|
||||||
|
|
||||||
turn_uris:
|
|
||||||
- "turn:<MATRIX_TURN_HOST>:3478?transport=udp"
|
|
||||||
- "turn:<MATRIX_TURN_HOST>:3478?transport=tcp"
|
|
||||||
turn_shared_secret: "<MATRIX_TURN_SHARED_SECRET>"
|
|
||||||
turn_user_lifetime: "1h"
|
|
||||||
|
|
||||||
experimental_features:
|
|
||||||
msc3266_enabled: true
|
|
||||||
msc4222_enabled: true
|
|
||||||
|
|
||||||
max_event_delay_duration: 24h
|
|
||||||
rc_message:
|
|
||||||
per_second: 0.5
|
|
||||||
burst_count: 30
|
|
||||||
rc_delayed_event_mgmt:
|
|
||||||
per_second: 1
|
|
||||||
burst_count: 20
|
|
||||||
```
|
|
||||||
|
|
||||||
## Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --profile matrix up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Ohne Profil startet weiterhin nur der bisherige FEDEO-Stack:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hinweise
|
|
||||||
|
|
||||||
- Die Matrix-Services sind bewusst im bestehenden Compose-Stack definiert, damit FEDEO nicht in mehrere Deployment-Dateien zerfällt.
|
|
||||||
- Die aktuellen Ports für TURN und LiveKit müssen auf der Firewall des Servers freigegeben werden.
|
|
||||||
- Federation sollte erst nach einer expliziten Entscheidung geöffnet werden. Für B2B-Kommunikation ist eine Allowlist sinnvoll.
|
|
||||||
- Die Werte in `.env.example` sind Platzhalter und nicht produktionssicher.
|
|
||||||
|
|
||||||
## Lokaler Entwicklungsstack
|
|
||||||
|
|
||||||
Für lokale Entwicklung gibt es zusätzlich das Profil `matrix-dev`. Es nutzt direkte Localhost-Ports und braucht keine öffentlichen Domains, kein ACME und keine Traefik-Router.
|
|
||||||
|
|
||||||
Lokale Dienste:
|
|
||||||
|
|
||||||
- Synapse: `http://localhost:8008`
|
|
||||||
- Element Web: `http://localhost:8080`
|
|
||||||
- MatrixRTC JWT-Service: `http://localhost:8081`
|
|
||||||
- LiveKit: `ws://localhost:7880`
|
|
||||||
- TURN: `localhost:3478`
|
|
||||||
|
|
||||||
### Lokale Synapse-Konfiguration erzeugen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --profile matrix-dev run --rm \
|
|
||||||
-e SYNAPSE_SERVER_NAME=localhost \
|
|
||||||
-e SYNAPSE_REPORT_STATS=no \
|
|
||||||
matrix-dev-synapse generate
|
|
||||||
```
|
|
||||||
|
|
||||||
Danach `matrix/dev/synapse/homeserver.yaml` für die lokale Compose anpassen:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
public_baseurl: "http://localhost:8008/"
|
|
||||||
|
|
||||||
database:
|
|
||||||
name: psycopg2
|
|
||||||
args:
|
|
||||||
user: synapse
|
|
||||||
password: "synapse-dev-password"
|
|
||||||
database: synapse
|
|
||||||
host: matrix-dev-db
|
|
||||||
cp_min: 5
|
|
||||||
cp_max: 10
|
|
||||||
|
|
||||||
redis:
|
|
||||||
enabled: true
|
|
||||||
host: matrix-dev-redis
|
|
||||||
|
|
||||||
enable_registration: true
|
|
||||||
enable_registration_without_verification: true
|
|
||||||
|
|
||||||
turn_uris:
|
|
||||||
- "turn:localhost:3478?transport=udp"
|
|
||||||
- "turn:localhost:3478?transport=tcp"
|
|
||||||
turn_shared_secret: "matrix-dev-turn-secret"
|
|
||||||
turn_user_lifetime: "1h"
|
|
||||||
|
|
||||||
experimental_features:
|
|
||||||
msc3266_enabled: true
|
|
||||||
msc4222_enabled: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lokalen Stack starten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --profile matrix-dev up -d \
|
|
||||||
matrix-dev-db \
|
|
||||||
matrix-dev-redis \
|
|
||||||
matrix-dev-synapse \
|
|
||||||
matrix-dev-turn \
|
|
||||||
matrix-dev-livekit \
|
|
||||||
matrix-dev-rtc-jwt \
|
|
||||||
matrix-dev-element
|
|
||||||
```
|
|
||||||
|
|
||||||
Einen lokalen Admin-Nutzer kannst du danach im Synapse-Container anlegen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --profile matrix-dev exec matrix-dev-synapse \
|
|
||||||
register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008
|
|
||||||
```
|
|
||||||
|
|
||||||
Anschließend Element Web unter `http://localhost:8080` öffnen und mit dem lokalen Matrix-Nutzer anmelden.
|
|
||||||
|
|
||||||
Wenn FEDEO selbst parallel lokal laufen soll, starte die FEDEO-Dienste separat wie gewohnt. Der lokale Matrix-Stack ist absichtlich über direkte Ports erreichbar, damit er unabhängig von DNS, TLS und Traefik getestet werden kann.
|
|
||||||
|
|
||||||
## Erste FEDEO-Backend-Integration
|
|
||||||
|
|
||||||
Das Backend stellt geschützte Matrix-Endpunkte unter `/api/communication/matrix/*` bereit:
|
|
||||||
|
|
||||||
- `GET /api/communication/matrix/status`: prüft Konfiguration und Erreichbarkeit des Matrix-Homeservers
|
|
||||||
- `GET /api/communication/matrix/me`: zeigt die aus dem FEDEO-Nutzer abgeleitete Matrix-ID
|
|
||||||
- `POST /api/communication/matrix/me/provision`: legt den Matrix-Account für den angemeldeten FEDEO-Nutzer per Synapse-Shared-Secret-Registrierung an
|
|
||||||
|
|
||||||
Für lokale Provisionierung muss `MATRIX_REGISTRATION_SHARED_SECRET` aus `matrix/dev/synapse/homeserver.yaml` in der Backend-Umgebung gesetzt werden. Die lokale Synapse-Konfiguration ist absichtlich nicht versioniert, weil sie Secrets enthält.
|
|
||||||
|
|
||||||
In der lokalen Entwicklung liest das Backend dieses Secret als Fallback direkt aus `matrix/dev/synapse/homeserver.yaml`, sofern `NODE_ENV` nicht `production` ist. Auf Servern muss das Secret weiterhin explizit über die Umgebung oder das Secret-Management gesetzt werden.
|
|
||||||
|
|
||||||
Für den eingebetteten Element-Login in FEDEO muss in der lokalen Synapse-Konfiguration außerdem der kurzlebige Login-Token-Flow aktiv sein:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
login_via_existing_session:
|
|
||||||
enabled: true
|
|
||||||
require_ui_auth: false
|
|
||||||
token_timeout: "5m"
|
|
||||||
```
|
|
||||||
|
|
||||||
Nach einer Änderung an `matrix/dev/synapse/homeserver.yaml` muss `matrix-dev-synapse` neu gestartet werden.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"default_server_config": {
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "http://localhost:8008",
|
|
||||||
"server_name": "localhost"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "http://localhost:8081"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"disable_custom_urls": false,
|
|
||||||
"disable_guests": true,
|
|
||||||
"brand": "FEDEO Matrix Dev",
|
|
||||||
"default_theme": "light",
|
|
||||||
"features": {
|
|
||||||
"feature_video_rooms": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "https://matrix.fedeo.de"
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "https://call.fedeo.de/livekit/jwt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"m.server": "matrix.fedeo.de:443"
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import { HapticTab } from '@/components/haptic-tab';
|
|||||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
import { IconSymbol } from '@/components/ui/icon-symbol';
|
||||||
import { Colors } from '@/constants/theme';
|
import { Colors } from '@/constants/theme';
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||||
import { fetchMatrixUnreadCounts } from '@/src/lib/api';
|
import { fetchChatUnreadCounts } from '@/src/lib/api';
|
||||||
import { useAuth } from '@/src/providers/auth-provider';
|
import { useAuth } from '@/src/providers/auth-provider';
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
@@ -22,7 +22,7 @@ export default function TabLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unread = await fetchMatrixUnreadCounts(token);
|
const unread = await fetchChatUnreadCounts(token);
|
||||||
const total = Object.values(unread).reduce((sum, room) => sum + (room.count || 0), 0);
|
const total = Object.values(unread).reduce((sum, room) => sum + (room.count || 0), 0);
|
||||||
setCommunicationUnread(total);
|
setCommunicationUnread(total);
|
||||||
await Notifications.setBadgeCountAsync(total);
|
await Notifications.setBadgeCountAsync(total);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -259,27 +259,12 @@ export function isAuthenticationError(error: unknown): boolean {
|
|||||||
return error instanceof ApiError && error.status === 401;
|
return error instanceof ApiError && error.status === 401;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MatrixStatus = {
|
export type ChatRoom = {
|
||||||
enabled?: boolean;
|
|
||||||
ready?: boolean;
|
|
||||||
configured?: boolean;
|
|
||||||
homeserverUrl?: string | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MatrixIdentity = {
|
|
||||||
matrixUserId: string;
|
|
||||||
displayName?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MatrixRoom = {
|
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
topic?: string | null;
|
topic?: string | null;
|
||||||
type?: 'room' | 'project' | 'direct' | string;
|
type?: 'room' | 'project' | 'direct' | string;
|
||||||
group?: string;
|
group?: string;
|
||||||
roomId?: string | null;
|
|
||||||
alias?: string | null;
|
|
||||||
exists?: boolean;
|
exists?: boolean;
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
projectNumber?: string | null;
|
projectNumber?: string | null;
|
||||||
@@ -294,66 +279,31 @@ export type MatrixRoom = {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MatrixAttachment = {
|
export type ChatMessage = {
|
||||||
fileName?: string | null;
|
id: number;
|
||||||
mimeType?: string | null;
|
|
||||||
size?: number | null;
|
|
||||||
mxcUri?: string | null;
|
|
||||||
previewUrl?: string | null;
|
|
||||||
downloadUrl?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MatrixReaction = {
|
|
||||||
key: string;
|
|
||||||
count?: number;
|
|
||||||
own?: boolean;
|
|
||||||
senders?: string[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MatrixMessage = {
|
|
||||||
id: string;
|
|
||||||
sender: string;
|
sender: string;
|
||||||
senderDisplayName?: string | null;
|
senderDisplayName?: string | null;
|
||||||
body?: string | null;
|
body?: string | null;
|
||||||
timestamp?: string | number | null;
|
timestamp?: string | number | null;
|
||||||
own?: boolean;
|
own?: boolean;
|
||||||
edited?: boolean;
|
|
||||||
redacted?: boolean;
|
|
||||||
msgtype?: string;
|
|
||||||
attachment?: MatrixAttachment | null;
|
|
||||||
replyToEventId?: string | null;
|
|
||||||
reactions?: MatrixReaction[];
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MatrixMember = {
|
export type ChatMember = {
|
||||||
matrixUserId: string;
|
|
||||||
displayName?: string | null;
|
|
||||||
avatarUrl?: string | null;
|
|
||||||
membership?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MatrixUser = {
|
|
||||||
userId: string;
|
userId: string;
|
||||||
matrixUserId: string;
|
|
||||||
displayName?: string | null;
|
displayName?: string | null;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
own?: boolean;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MatrixSyncResponse = {
|
export type ChatSyncResponse = {
|
||||||
nextBatch?: string;
|
nextId?: number;
|
||||||
messages?: MatrixMessage[];
|
messages?: ChatMessage[];
|
||||||
replacements?: MatrixMessage[];
|
|
||||||
reactions?: (MatrixReaction & { targetEventId?: string })[];
|
|
||||||
redactions?: { redacts?: string; eventId?: string; targetEventId?: string }[];
|
|
||||||
members?: MatrixMember[];
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MatrixUnreadCounts = Record<string, { count?: number; mentions?: number }>;
|
export type ChatUnreadCounts = Record<string, { count?: number; mentions?: number }>;
|
||||||
|
|
||||||
function buildUrl(path: string): string {
|
function buildUrl(path: string): string {
|
||||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
@@ -429,77 +379,26 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
|||||||
return payload as T;
|
return payload as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiFormRequest<T>(path: string, token: string, formData: FormData): Promise<T> {
|
function chatRoomPath(roomKey: string, suffix = ''): string {
|
||||||
const { signal, cleanup } = createTimeoutSignal();
|
return `/api/communication/chat/rooms/${encodeURIComponent(roomKey)}${suffix}`;
|
||||||
let response: Response;
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await fetch(buildUrl(path), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (isAbortError(error)) {
|
|
||||||
throw new Error(`Zeitüberschreitung beim Hochladen zum FEDEO-Server (${path}).`);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await parseJson(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const message =
|
|
||||||
(payload as { message?: string; error?: string } | null)?.message ||
|
|
||||||
(payload as { message?: string; error?: string } | null)?.error ||
|
|
||||||
`Request failed (${response.status}) for ${path}`;
|
|
||||||
throw new ApiError(message, response.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matrixRoomPath(roomKey: string, suffix = ''): string {
|
|
||||||
return `/api/communication/matrix/rooms/${encodeURIComponent(roomKey)}${suffix}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkBackendHealth(): Promise<{ status: string; [key: string]: unknown }> {
|
export async function checkBackendHealth(): Promise<{ status: string; [key: string]: unknown }> {
|
||||||
return apiRequest<{ status: string; [key: string]: unknown }>('/health');
|
return apiRequest<{ status: string; [key: string]: unknown }>('/health');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMatrixStatus(token: string): Promise<MatrixStatus> {
|
export async function fetchChatRooms(token: string): Promise<ChatRoom[]> {
|
||||||
return apiRequest<MatrixStatus>('/api/communication/matrix/status', { token });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchMatrixIdentity(token: string): Promise<MatrixIdentity> {
|
|
||||||
return apiRequest<MatrixIdentity>('/api/communication/matrix/me', { token });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function provisionMatrixUser(token: string): Promise<MatrixIdentity> {
|
|
||||||
return apiRequest<MatrixIdentity>('/api/communication/matrix/me/provision', {
|
|
||||||
method: 'POST',
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchMatrixRooms(token: string): Promise<MatrixRoom[]> {
|
|
||||||
const [rooms, projectRooms, directRooms, unread] = await Promise.all([
|
const [rooms, projectRooms, directRooms, unread] = await Promise.all([
|
||||||
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/rooms', { token }),
|
apiRequest<{ rooms?: ChatRoom[] }>('/api/communication/chat/rooms', { token }),
|
||||||
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/project-rooms', { token }),
|
apiRequest<{ rooms?: ChatRoom[] }>('/api/communication/chat/project-rooms', { token }),
|
||||||
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/direct-rooms', { token }),
|
apiRequest<{ rooms?: ChatRoom[] }>('/api/communication/chat/direct-rooms', { token }),
|
||||||
apiRequest<{ rooms?: Record<string, { count?: number; mentions?: number }> }>('/api/communication/matrix/unread', {
|
apiRequest<{ rooms?: ChatUnreadCounts }>('/api/communication/chat/unread', {
|
||||||
token,
|
token,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const unreadByRoom = unread.rooms || {};
|
const unreadByRoom = unread.rooms || {};
|
||||||
const decorate = (room: MatrixRoom, group: string): MatrixRoom => ({
|
const decorate = (room: ChatRoom, group: string): ChatRoom => ({
|
||||||
...room,
|
...room,
|
||||||
group,
|
group,
|
||||||
unread: unreadByRoom[room.key]?.count || 0,
|
unread: unreadByRoom[room.key]?.count || 0,
|
||||||
@@ -513,165 +412,70 @@ export async function fetchMatrixRooms(token: string): Promise<MatrixRoom[]> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMatrixUnreadCounts(token: string): Promise<MatrixUnreadCounts> {
|
export async function fetchChatUnreadCounts(token: string): Promise<ChatUnreadCounts> {
|
||||||
const response = await apiRequest<{ rooms?: MatrixUnreadCounts }>('/api/communication/matrix/unread', { token });
|
const response = await apiRequest<{ rooms?: ChatUnreadCounts }>('/api/communication/chat/unread', { token });
|
||||||
return response.rooms || {};
|
return response.rooms || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMatrixUsers(token: string): Promise<MatrixUser[]> {
|
export async function createChatRoom(
|
||||||
const response = await apiRequest<{ users?: MatrixUser[] }>('/api/communication/matrix/users', { token });
|
|
||||||
return response.users || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMatrixRoom(
|
|
||||||
token: string,
|
token: string,
|
||||||
payload: { key: string; name: string; topic?: string | null; type?: string }
|
payload: { key: string; name: string; topic?: string | null; type?: string }
|
||||||
): Promise<MatrixRoom> {
|
): Promise<ChatRoom> {
|
||||||
return apiRequest<MatrixRoom>('/api/communication/matrix/rooms', {
|
return apiRequest<ChatRoom>('/api/communication/chat/rooms', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
token,
|
token,
|
||||||
body: payload,
|
body: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function provisionMatrixRoom(token: string, room: MatrixRoom): Promise<MatrixRoom> {
|
export async function provisionChatRoom(token: string, room: ChatRoom): Promise<ChatRoom> {
|
||||||
if (room.provisionEndpoint) {
|
|
||||||
return apiRequest<MatrixRoom>(room.provisionEndpoint, { method: 'POST', token });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (room.type === 'project' && room.projectId) {
|
if (room.type === 'project' && room.projectId) {
|
||||||
return apiRequest<MatrixRoom>(`/api/communication/matrix/project-rooms/${room.projectId}/provision`, {
|
return apiRequest<ChatRoom>(`/api/communication/chat/project-rooms/${room.projectId}/provision`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.type === 'direct' && room.userId) {
|
if (room.type === 'direct' && room.userId) {
|
||||||
return apiRequest<MatrixRoom>(`/api/communication/matrix/direct-rooms/${encodeURIComponent(room.userId)}/provision`, {
|
return apiRequest<ChatRoom>(`/api/communication/chat/direct-rooms/${encodeURIComponent(room.userId)}/provision`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiRequest<MatrixRoom>(matrixRoomPath(room.key, '/provision'), {
|
return room;
|
||||||
method: 'POST',
|
|
||||||
token,
|
|
||||||
body: {
|
|
||||||
key: room.key,
|
|
||||||
name: room.name,
|
|
||||||
topic: room.topic,
|
|
||||||
type: room.type || 'room',
|
|
||||||
entityType: room.entityType,
|
|
||||||
entityId: room.entityId,
|
|
||||||
entityUuid: room.entityUuid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMatrixMessages(token: string, roomKey: string): Promise<MatrixMessage[]> {
|
export async function fetchChatMessages(token: string, roomKey: string): Promise<ChatMessage[]> {
|
||||||
const response = await apiRequest<{ messages?: MatrixMessage[] }>(matrixRoomPath(roomKey, '/messages'), { token });
|
const response = await apiRequest<{ messages?: ChatMessage[] }>(chatRoomPath(roomKey, '/messages'), { token });
|
||||||
return response.messages || [];
|
return response.messages || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncMatrixRoom(
|
export async function syncChatRoom(token: string, roomKey: string, afterId = 0): Promise<ChatSyncResponse> {
|
||||||
token: string,
|
return apiRequest<ChatSyncResponse>(chatRoomPath(roomKey, `/sync?afterId=${afterId}`), { token });
|
||||||
roomKey: string,
|
|
||||||
since?: string,
|
|
||||||
initial = false
|
|
||||||
): Promise<MatrixSyncResponse> {
|
|
||||||
const query = new URLSearchParams();
|
|
||||||
if (since) query.set('since', since);
|
|
||||||
if (initial) query.set('initial', '1');
|
|
||||||
const suffix = query.toString() ? `/sync?${query.toString()}` : '/sync';
|
|
||||||
return apiRequest<MatrixSyncResponse>(matrixRoomPath(roomKey, suffix), { token });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMatrixMembers(token: string, roomKey: string): Promise<MatrixMember[]> {
|
export async function fetchChatMembers(token: string, roomKey: string): Promise<ChatMember[]> {
|
||||||
const response = await apiRequest<{ members?: MatrixMember[] }>(matrixRoomPath(roomKey, '/members'), { token });
|
const response = await apiRequest<{ members?: ChatMember[] }>(chatRoomPath(roomKey, '/members'), { token });
|
||||||
return response.members || [];
|
return response.members || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendMatrixMessage(
|
export async function sendChatMessage(token: string, roomKey: string, text: string): Promise<ChatMessage> {
|
||||||
token: string,
|
return apiRequest<ChatMessage>(chatRoomPath(roomKey, '/messages'), {
|
||||||
roomKey: string,
|
|
||||||
text: string,
|
|
||||||
replyToEventId?: string | null
|
|
||||||
): Promise<MatrixMessage> {
|
|
||||||
return apiRequest<MatrixMessage>(matrixRoomPath(roomKey, '/messages'), {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
token,
|
token,
|
||||||
body: { text, replyToEventId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function editMatrixMessage(token: string, roomKey: string, eventId: string, text: string): Promise<MatrixMessage> {
|
|
||||||
return apiRequest<MatrixMessage>(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}`), {
|
|
||||||
method: 'PUT',
|
|
||||||
token,
|
|
||||||
body: { text },
|
body: { text },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMatrixMessage(token: string, roomKey: string, eventId: string): Promise<void> {
|
export async function markChatRoomRead(token: string, roomKey: string, messageId?: number): Promise<void> {
|
||||||
await apiRequest(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}`), {
|
await apiRequest(chatRoomPath(roomKey, '/read'), {
|
||||||
method: 'DELETE',
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function reactToMatrixMessage(token: string, roomKey: string, eventId: string, key: string): Promise<void> {
|
|
||||||
await apiRequest(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}/reactions`), {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
token,
|
token,
|
||||||
body: { key },
|
body: { messageId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markMatrixRoomRead(token: string, roomKey: string, eventId?: string): Promise<void> {
|
|
||||||
await apiRequest(matrixRoomPath(roomKey, '/read'), {
|
|
||||||
method: 'POST',
|
|
||||||
token,
|
|
||||||
body: { eventId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncMatrixMembers(token: string, roomKey: string): Promise<void> {
|
|
||||||
await apiRequest(matrixRoomPath(roomKey, '/members/sync'), {
|
|
||||||
method: 'POST',
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function inviteMatrixMember(token: string, roomKey: string, userId: string): Promise<void> {
|
|
||||||
await apiRequest(matrixRoomPath(roomKey, '/members/invite'), {
|
|
||||||
method: 'POST',
|
|
||||||
token,
|
|
||||||
body: { userId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeMatrixMember(token: string, roomKey: string, matrixUserId: string): Promise<void> {
|
|
||||||
await apiRequest(matrixRoomPath(roomKey, `/members/${encodeURIComponent(matrixUserId)}`), {
|
|
||||||
method: 'DELETE',
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadMatrixAttachment(
|
|
||||||
token: string,
|
|
||||||
roomKey: string,
|
|
||||||
file: { uri: string; name: string; mimeType?: string | null }
|
|
||||||
): Promise<MatrixMessage> {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', {
|
|
||||||
uri: file.uri,
|
|
||||||
name: file.name,
|
|
||||||
type: file.mimeType || 'application/octet-stream',
|
|
||||||
} as unknown as Blob);
|
|
||||||
|
|
||||||
return apiFormRequest<MatrixMessage>(matrixRoomPath(roomKey, '/attachments'), token, formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderPrintLabel(
|
export async function renderPrintLabel(
|
||||||
token: string,
|
token: string,
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ choose_mode() {
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Setup-Modus"
|
echo "Setup-Modus"
|
||||||
echo " 1) einfach - Domain, Admin, lokale Datenbank, MinIO, Matrix"
|
echo " 1) einfach - Domain, Admin, lokale Datenbank und MinIO"
|
||||||
echo " 2) advanced - zusätzlich SMTP, externe Schlüssel und optionale Dienste"
|
echo " 2) advanced - zusätzlich SMTP, externe Schlüssel und optionale Dienste"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
@@ -266,12 +266,10 @@ FEDEO Selfhost Setup
|
|||||||
Dieses Script führt dich durch die lokale Betriebsstruktur:
|
Dieses Script führt dich durch die lokale Betriebsstruktur:
|
||||||
|
|
||||||
$ROOT_DIR/
|
$ROOT_DIR/
|
||||||
$(basename "$COMPOSE_FILE") Docker Stack für FEDEO, Traefik, PostgreSQL, MinIO, Matrix und Monitoring
|
$(basename "$COMPOSE_FILE") Docker Stack für FEDEO, Traefik, PostgreSQL, MinIO und Monitoring
|
||||||
.env Zielkonfiguration, wird von diesem Script geschrieben
|
.env Zielkonfiguration, wird von diesem Script geschrieben
|
||||||
postgres/ persistente FEDEO-Datenbank
|
postgres/ persistente FEDEO-Datenbank
|
||||||
minio/ lokaler S3-kompatibler Dateispeicher
|
minio/ lokaler S3-kompatibler Dateispeicher
|
||||||
matrix/postgres/ persistente Synapse-Datenbank
|
|
||||||
matrix/synapse/ generierte Synapse-Konfiguration und Matrix-Daten
|
|
||||||
traefik/letsencrypt/ Let's-Encrypt-Zertifikate
|
traefik/letsencrypt/ Let's-Encrypt-Zertifikate
|
||||||
traefik/logs/ Traefik-Logs
|
traefik/logs/ Traefik-Logs
|
||||||
|
|
||||||
@@ -279,11 +277,6 @@ Dieses Script führt dich durch die lokale Betriebsstruktur:
|
|||||||
|
|
||||||
https://DOMAIN/ FEDEO Frontend
|
https://DOMAIN/ FEDEO Frontend
|
||||||
https://DOMAIN/backend FEDEO API
|
https://DOMAIN/backend FEDEO API
|
||||||
https://DOMAIN/_matrix Matrix Homeserver
|
|
||||||
https://DOMAIN/.well-known Matrix Discovery
|
|
||||||
https://DOMAIN/livekit/sfu LiveKit
|
|
||||||
https://DOMAIN/livekit/jwt LiveKit JWT-Service
|
|
||||||
https://DOMAIN/element Element Web
|
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -303,32 +296,28 @@ write_env() {
|
|||||||
local admin_last_name="${12}"
|
local admin_last_name="${12}"
|
||||||
local tenant_name="${13}"
|
local tenant_name="${13}"
|
||||||
local tenant_short="${14}"
|
local tenant_short="${14}"
|
||||||
local matrix_db_password="${15}"
|
local mailer_host="${15}"
|
||||||
local matrix_turn_secret="${16}"
|
local mailer_port="${16}"
|
||||||
local matrix_registration_secret="${17}"
|
local mailer_ssl="${17}"
|
||||||
local livekit_secret="${18}"
|
local mailer_user="${18}"
|
||||||
local mailer_host="${19}"
|
local mailer_pass="${19}"
|
||||||
local mailer_port="${20}"
|
local mailer_from="${20}"
|
||||||
local mailer_ssl="${21}"
|
local web_push_public="${21}"
|
||||||
local mailer_user="${22}"
|
local web_push_private="${22}"
|
||||||
local mailer_pass="${23}"
|
local pdf_license="${23}"
|
||||||
local mailer_from="${24}"
|
local openai_key="${24}"
|
||||||
local web_push_public="${25}"
|
local stirling_key="${25}"
|
||||||
local web_push_private="${26}"
|
local gocardless_secret_id="${26}"
|
||||||
local pdf_license="${27}"
|
local gocardless_secret_key="${27}"
|
||||||
local openai_key="${28}"
|
local dokubox_host="${28}"
|
||||||
local stirling_key="${29}"
|
local dokubox_port="${29}"
|
||||||
local gocardless_secret_id="${30}"
|
local dokubox_secure="${30}"
|
||||||
local gocardless_secret_key="${31}"
|
local dokubox_user="${31}"
|
||||||
local dokubox_host="${32}"
|
local dokubox_password="${32}"
|
||||||
local dokubox_port="${33}"
|
local central_services_enabled="${33}"
|
||||||
local dokubox_secure="${34}"
|
local central_services_url="${34}"
|
||||||
local dokubox_user="${35}"
|
local central_instance_id="${35}"
|
||||||
local dokubox_password="${36}"
|
local central_instance_secret="${36}"
|
||||||
local central_services_enabled="${37}"
|
|
||||||
local central_services_url="${38}"
|
|
||||||
local central_instance_id="${39}"
|
|
||||||
local central_instance_secret="${40}"
|
|
||||||
|
|
||||||
cat >"$ENV_FILE" <<EOF
|
cat >"$ENV_FILE" <<EOF
|
||||||
# FEDEO Selfhosting
|
# FEDEO Selfhosting
|
||||||
@@ -401,22 +390,6 @@ FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=$(env_quote "$admin_first_name")
|
|||||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=$(env_quote "$admin_last_name")
|
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=$(env_quote "$admin_last_name")
|
||||||
FEDEO_BOOTSTRAP_TENANT_NAME=$(env_quote "$tenant_name")
|
FEDEO_BOOTSTRAP_TENANT_NAME=$(env_quote "$tenant_name")
|
||||||
FEDEO_BOOTSTRAP_TENANT_SHORT=$(env_quote "$tenant_short")
|
FEDEO_BOOTSTRAP_TENANT_SHORT=$(env_quote "$tenant_short")
|
||||||
FEDEO_BOOTSTRAP_MATRIX=$(env_quote "true")
|
|
||||||
|
|
||||||
MATRIX_SERVER_NAME=$(env_quote "$domain")
|
|
||||||
MATRIX_POSTGRES_DB=$(env_quote "synapse")
|
|
||||||
MATRIX_POSTGRES_USER=$(env_quote "synapse")
|
|
||||||
MATRIX_POSTGRES_PASSWORD=$(env_quote "$matrix_db_password")
|
|
||||||
MATRIX_TURN_SHARED_SECRET=$(env_quote "$matrix_turn_secret")
|
|
||||||
LIVEKIT_KEY=$(env_quote "fedeo-livekit")
|
|
||||||
LIVEKIT_SECRET=$(env_quote "$livekit_secret")
|
|
||||||
MATRIX_HOMESERVER_URL=$(env_quote "http://matrix-synapse:8008")
|
|
||||||
MATRIX_RTC_HOST=$(env_quote "$domain")
|
|
||||||
MATRIX_RTC_JWT_URL=$(env_quote "https://$domain/livekit/jwt")
|
|
||||||
MATRIX_LIVEKIT_URL=$(env_quote "wss://$domain/livekit/sfu")
|
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET=$(env_quote "$matrix_registration_secret")
|
|
||||||
MATRIX_SERVICE_USER_LOCALPART=$(env_quote "fedeo_service")
|
|
||||||
NUXT_PUBLIC_MATRIX_ELEMENT_URL=$(env_quote "https://$domain/element")
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,9 +398,7 @@ prepare_directories() {
|
|||||||
"$ROOT_DIR/traefik/letsencrypt" \
|
"$ROOT_DIR/traefik/letsencrypt" \
|
||||||
"$ROOT_DIR/traefik/logs" \
|
"$ROOT_DIR/traefik/logs" \
|
||||||
"$ROOT_DIR/postgres" \
|
"$ROOT_DIR/postgres" \
|
||||||
"$ROOT_DIR/minio" \
|
"$ROOT_DIR/minio"
|
||||||
"$ROOT_DIR/matrix/postgres" \
|
|
||||||
"$ROOT_DIR/matrix/synapse"
|
|
||||||
|
|
||||||
touch "$ROOT_DIR/traefik/letsencrypt/acme.json"
|
touch "$ROOT_DIR/traefik/letsencrypt/acme.json"
|
||||||
chmod 600 "$ROOT_DIR/traefik/letsencrypt/acme.json"
|
chmod 600 "$ROOT_DIR/traefik/letsencrypt/acme.json"
|
||||||
@@ -466,13 +437,11 @@ uninstall_stack() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Achtung: --purge entfernt lokale Datenbanken, Dateien, Matrix-Daten, Zertifikate und .env."
|
echo "Achtung: --purge entfernt lokale Datenbanken, Dateien, Zertifikate und .env."
|
||||||
echo "Betroffene Pfade:"
|
echo "Betroffene Pfade:"
|
||||||
echo " $ENV_FILE"
|
echo " $ENV_FILE"
|
||||||
echo " $ROOT_DIR/postgres"
|
echo " $ROOT_DIR/postgres"
|
||||||
echo " $ROOT_DIR/minio"
|
echo " $ROOT_DIR/minio"
|
||||||
echo " $ROOT_DIR/matrix/postgres"
|
|
||||||
echo " $ROOT_DIR/matrix/synapse"
|
|
||||||
echo " $ROOT_DIR/traefik/letsencrypt"
|
echo " $ROOT_DIR/traefik/letsencrypt"
|
||||||
echo " $ROOT_DIR/traefik/logs"
|
echo " $ROOT_DIR/traefik/logs"
|
||||||
|
|
||||||
@@ -487,8 +456,6 @@ uninstall_stack() {
|
|||||||
"$ENV_FILE" \
|
"$ENV_FILE" \
|
||||||
"$ROOT_DIR/postgres" \
|
"$ROOT_DIR/postgres" \
|
||||||
"$ROOT_DIR/minio" \
|
"$ROOT_DIR/minio" \
|
||||||
"$ROOT_DIR/matrix/postgres" \
|
|
||||||
"$ROOT_DIR/matrix/synapse" \
|
|
||||||
"$ROOT_DIR/traefik/letsencrypt" \
|
"$ROOT_DIR/traefik/letsencrypt" \
|
||||||
"$ROOT_DIR/traefik/logs"
|
"$ROOT_DIR/traefik/logs"
|
||||||
|
|
||||||
@@ -539,17 +506,12 @@ main() {
|
|||||||
echo
|
echo
|
||||||
echo "Secrets werden automatisch erzeugt."
|
echo "Secrets werden automatisch erzeugt."
|
||||||
local db_password minio_password cookie_secret jwt_secret encryption_key m2m_key
|
local db_password minio_password cookie_secret jwt_secret encryption_key m2m_key
|
||||||
local matrix_db_password matrix_turn_secret matrix_registration_secret livekit_secret
|
|
||||||
db_password="$(random_hex_secret)"
|
db_password="$(random_hex_secret)"
|
||||||
minio_password="$(random_secret)"
|
minio_password="$(random_secret)"
|
||||||
cookie_secret="$(random_secret)"
|
cookie_secret="$(random_secret)"
|
||||||
jwt_secret="$(random_secret)"
|
jwt_secret="$(random_secret)"
|
||||||
encryption_key="$(random_hex_secret)"
|
encryption_key="$(random_hex_secret)"
|
||||||
m2m_key="$(random_secret)"
|
m2m_key="$(random_secret)"
|
||||||
matrix_db_password="$(random_secret)"
|
|
||||||
matrix_turn_secret="$(random_secret)"
|
|
||||||
matrix_registration_secret="$(random_secret)"
|
|
||||||
livekit_secret="$(random_secret)"
|
|
||||||
|
|
||||||
local mailer_host="smtp.example.com"
|
local mailer_host="smtp.example.com"
|
||||||
local mailer_port="587"
|
local mailer_port="587"
|
||||||
@@ -613,8 +575,7 @@ main() {
|
|||||||
"$domain" "$contact_email" "$db_password" "$minio_password" \
|
"$domain" "$contact_email" "$db_password" "$minio_password" \
|
||||||
"$cookie_secret" "$jwt_secret" "$encryption_key" "$m2m_key" \
|
"$cookie_secret" "$jwt_secret" "$encryption_key" "$m2m_key" \
|
||||||
"$admin_email" "$admin_password" "$admin_first_name" "$admin_last_name" \
|
"$admin_email" "$admin_password" "$admin_first_name" "$admin_last_name" \
|
||||||
"$tenant_name" "$tenant_short" "$matrix_db_password" "$matrix_turn_secret" \
|
"$tenant_name" "$tenant_short" "$mailer_host" "$mailer_port" \
|
||||||
"$matrix_registration_secret" "$livekit_secret" "$mailer_host" "$mailer_port" \
|
|
||||||
"$mailer_ssl" "$mailer_user" "$mailer_pass" "$mailer_from" "$web_push_public" \
|
"$mailer_ssl" "$mailer_user" "$mailer_pass" "$mailer_from" "$web_push_public" \
|
||||||
"$web_push_private" "$pdf_license" "$openai_key" "$stirling_key" \
|
"$web_push_private" "$pdf_license" "$openai_key" "$stirling_key" \
|
||||||
"$gocardless_secret_id" "$gocardless_secret_key" "$dokubox_host" \
|
"$gocardless_secret_id" "$gocardless_secret_key" "$dokubox_host" \
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ h3 {
|
|||||||
.section,
|
.section,
|
||||||
.workflow-section,
|
.workflow-section,
|
||||||
.audience-teaser,
|
.audience-teaser,
|
||||||
.matrix-section,
|
.chat-section,
|
||||||
.open-source-section,
|
.open-source-section,
|
||||||
.contact-section,
|
.contact-section,
|
||||||
footer {
|
footer {
|
||||||
@@ -394,7 +394,7 @@ footer {
|
|||||||
|
|
||||||
.workflow-section,
|
.workflow-section,
|
||||||
.audience-teaser,
|
.audience-teaser,
|
||||||
.matrix-section,
|
.chat-section,
|
||||||
.open-source-section,
|
.open-source-section,
|
||||||
.script-section,
|
.script-section,
|
||||||
.contact-section {
|
.contact-section {
|
||||||
@@ -456,32 +456,32 @@ footer {
|
|||||||
max-width: 52rem;
|
max-width: 52rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-section {
|
.chat-section {
|
||||||
border-top: 1px solid rgba(23, 33, 31, 0.12);
|
border-top: 1px solid rgba(23, 33, 31, 0.12);
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
padding-top: 5rem;
|
padding-top: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-layout {
|
.chat-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(20rem, 0.82fr);
|
grid-template-columns: minmax(0, 1fr) minmax(20rem, 0.82fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-stack {
|
.chat-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-stack article {
|
.chat-stack article {
|
||||||
background: rgba(255, 255, 255, 0.76);
|
background: rgba(255, 255, 255, 0.76);
|
||||||
border: 1px solid rgba(23, 33, 31, 0.1);
|
border: 1px solid rgba(23, 33, 31, 0.1);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 1.35rem;
|
padding: 1.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-stack span {
|
.chat-stack span {
|
||||||
color: var(--accent-dark);
|
color: var(--accent-dark);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
@@ -489,13 +489,13 @@ footer {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-stack p {
|
.chat-stack p {
|
||||||
color: #51605c;
|
color: #51605c;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-diagram {
|
.chat-diagram {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
background: #17211f;
|
background: #17211f;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
@@ -505,7 +505,7 @@ footer {
|
|||||||
padding: clamp(1.25rem, 3vw, 2rem);
|
padding: clamp(1.25rem, 3vw, 2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-diagram div {
|
.chat-diagram div {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
border-radius: 0.45rem;
|
border-radius: 0.45rem;
|
||||||
@@ -514,12 +514,12 @@ footer {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-diagram small {
|
.chat-diagram small {
|
||||||
color: rgba(255, 255, 255, 0.64);
|
color: rgba(255, 255, 255, 0.64);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-diagram span {
|
.chat-diagram span {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -948,7 +948,7 @@ footer p {
|
|||||||
.hero-section,
|
.hero-section,
|
||||||
.workflow-section,
|
.workflow-section,
|
||||||
.audience-teaser,
|
.audience-teaser,
|
||||||
.matrix-section,
|
.chat-section,
|
||||||
.open-source-section,
|
.open-source-section,
|
||||||
.script-section,
|
.script-section,
|
||||||
.contact-section {
|
.contact-section {
|
||||||
@@ -964,7 +964,7 @@ footer p {
|
|||||||
.form-grid,
|
.form-grid,
|
||||||
.audience-grid,
|
.audience-grid,
|
||||||
.audience-fit,
|
.audience-fit,
|
||||||
.matrix-layout,
|
.chat-layout,
|
||||||
.federation-panel,
|
.federation-panel,
|
||||||
.legal-grid {
|
.legal-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1028,7 +1028,7 @@ footer p {
|
|||||||
.highlights,
|
.highlights,
|
||||||
.feature-grid,
|
.feature-grid,
|
||||||
.audience-grid,
|
.audience-grid,
|
||||||
.matrix-stack,
|
.chat-stack,
|
||||||
.legal-grid {
|
.legal-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
||||||
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
<NuxtLink to="/#chat">Chat</NuxtLink>
|
||||||
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
||||||
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
<NuxtLink to="/#chat">Chat</NuxtLink>
|
||||||
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<a href="#funktionen">Funktionen</a>
|
<a href="#funktionen">Funktionen</a>
|
||||||
<a href="#open-source">Open Source</a>
|
<a href="#open-source">Open Source</a>
|
||||||
<a href="#selfhost">Selfhost</a>
|
<a href="#selfhost">Selfhost</a>
|
||||||
<a href="#matrix">Matrix</a>
|
<a href="#chat">Chat</a>
|
||||||
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -167,42 +167,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="matrix" class="matrix-section">
|
<section id="chat" class="chat-section">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<p class="eyebrow">Tief integriert</p>
|
<p class="eyebrow">Tief integriert</p>
|
||||||
<h2>Matrix als Kommunikationsschicht für FEDEO.</h2>
|
<h2>Chat direkt in FEDEO.</h2>
|
||||||
<p>
|
<p>
|
||||||
FEDEO verbindet Chat, Räume, Anrufe und Videokonferenzen mit Projekten, Vorgängen, Kontakten und Berechtigungen. Matrix läuft dabei nicht daneben, sondern wird durch FEDEO provisioniert, verknüpft und betrieben.
|
FEDEO verbindet Nachrichten und Räume direkt mit Projekten, Kontakten und Berechtigungen. Identitäten, Zugriffe und Gesprächsverläufe bleiben dabei vollständig im FEDEO-Kontext.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="matrix-layout">
|
<div class="chat-layout">
|
||||||
<div class="matrix-stack">
|
<div class="chat-stack">
|
||||||
<article v-for="item in matrixStack" :key="item.title">
|
<article v-for="item in chatStack" :key="item.title">
|
||||||
<span>{{ item.tag }}</span>
|
<span>{{ item.tag }}</span>
|
||||||
<h3>{{ item.title }}</h3>
|
<h3>{{ item.title }}</h3>
|
||||||
<p>{{ item.description }}</p>
|
<p>{{ item.description }}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="matrix-diagram" aria-label="Matrix Stack Architektur">
|
<div class="chat-diagram" aria-label="FEDEO Chat Architektur">
|
||||||
<div>FEDEO Web & App</div>
|
<div>FEDEO Web & App</div>
|
||||||
<span></span>
|
<span></span>
|
||||||
<div>FEDEO Backend<br><small>SSO, Rechte, Objektkontext</small></div>
|
<div>FEDEO Backend<br><small>SSO, Rechte, Objektkontext</small></div>
|
||||||
<span></span>
|
<span></span>
|
||||||
<div>Matrix Homeserver<br><small>Synapse, Räume, Events</small></div>
|
<div>FEDEO Chat<br><small>Räume, Nachrichten, Lesestatus</small></div>
|
||||||
<span></span>
|
<span></span>
|
||||||
<div>FEDEO Kommunikation<br><small>Räume, Gäste, Föderation</small></div>
|
<div>FEDEO Datenbank<br><small>Mandantengetrennte Speicherung</small></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="federation-panel">
|
<div class="federation-panel">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Föderation</p>
|
<p class="eyebrow">Klare Zuständigkeit</p>
|
||||||
<h3>FEDEO-Instanzen können kontrolliert miteinander sprechen.</h3>
|
<h3>Kommunikation folgt den FEDEO-Berechtigungen.</h3>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Für sensible Unternehmenskommunikation bleibt die Föderation standardmäßig steuerbar: geschlossen für interne Installationen, per Allowlist für Partner und Kunden oder bewusst geöffnet für öffentliche Matrix-Szenarien.
|
Allgemeine, projektbezogene und direkte Chats werden mandantengetrennt gespeichert. Nutzer sehen nur Räume, für die ihnen FEDEO einen Zugriff zuweist.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -257,10 +257,10 @@ const features = [
|
|||||||
{
|
{
|
||||||
tag: 'Büro',
|
tag: 'Büro',
|
||||||
title: 'Aufgaben und Kommunikation',
|
title: 'Aufgaben und Kommunikation',
|
||||||
description: 'Aufgaben, interne Nachrichten und Matrix-Räume laufen dort zusammen, wo die Arbeit entsteht.',
|
description: 'Aufgaben und interne Nachrichten laufen dort zusammen, wo die Arbeit entsteht.',
|
||||||
details: [
|
details: [
|
||||||
'Aufgaben mit Bezug zu Projekten, Vorgängen und Teams',
|
'Aufgaben mit Bezug zu Projekten, Vorgängen und Teams',
|
||||||
'Matrix-Räume für projektnahe Abstimmung',
|
'Chaträume für projektnahe Abstimmung',
|
||||||
'Benachrichtigungen und Lesestatus für laufende Arbeit'
|
'Benachrichtigungen und Lesestatus für laufende Arbeit'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -316,31 +316,31 @@ const features = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const matrixStack = [
|
const chatStack = [
|
||||||
{
|
{
|
||||||
tag: 'Chat',
|
tag: 'Chat',
|
||||||
title: 'Projekt- und Vorgangsräume',
|
title: 'Projekt- und Vorgangsräume',
|
||||||
description: 'Matrix-Räume werden aus FEDEO-Kontexten wie Projekten, Tickets, Teams und Kontakten heraus genutzt.'
|
description: 'Chaträume werden aus FEDEO-Kontexten wie Projekten, Teams und Kontakten heraus genutzt.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'Identität',
|
tag: 'Identität',
|
||||||
title: 'FEDEO bleibt führend',
|
title: 'FEDEO bleibt führend',
|
||||||
description: 'Nutzer, Rollen und Raumzugriffe werden aus FEDEO heraus provisioniert und bei Änderungen synchronisiert.'
|
description: 'Nutzer, Rollen und Raumzugriffe werden direkt über FEDEO verwaltet.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'Kommunikation',
|
tag: 'Kommunikation',
|
||||||
title: 'Chat, Gäste und Räume',
|
title: 'Nachrichten und Räume',
|
||||||
description: 'Teamräume, externe Gäste und projektnahe Abstimmung bleiben direkt mit FEDEO-Kontexten verbunden.'
|
description: 'Allgemeine, direkte und projektnahe Abstimmung bleibt direkt mit FEDEO-Kontexten verbunden.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'Betrieb',
|
tag: 'Betrieb',
|
||||||
title: 'Selfhost-ready Stack',
|
title: 'Selfhost-ready',
|
||||||
description: 'Synapse, PostgreSQL, Redis, .well-known, Element und TURN/STUN sind im Selfhost-Stack vorbereitet.'
|
description: 'Chatdaten werden mit der bestehenden FEDEO-Datenbank und ohne zusätzliche Kommunikationsdienste betrieben.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'Föderation',
|
tag: 'Sicherheit',
|
||||||
title: 'Instanzübergreifende Zusammenarbeit',
|
title: 'Mandantengetrennte Kommunikation',
|
||||||
description: 'Mehrere FEDEO- oder Matrix-Instanzen können über freigegebene Domains föderieren, ohne die lokale Kontrolle über Nutzer und Räume aufzugeben.'
|
description: 'Teilnehmerrechte und Lesestatus werden serverseitig geprüft und innerhalb der jeweiligen FEDEO-Instanz gespeichert.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
||||||
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
<NuxtLink to="/#chat">Chat</NuxtLink>
|
||||||
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
||||||
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
<NuxtLink to="/#matrix">Matrix</NuxtLink>
|
<NuxtLink to="/#chat">Chat</NuxtLink>
|
||||||
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -112,7 +112,7 @@ const audiences = [
|
|||||||
points: [
|
points: [
|
||||||
'Vorgänge mit Historie, Dokumentation und Zuständigkeit',
|
'Vorgänge mit Historie, Dokumentation und Zuständigkeit',
|
||||||
'Rollen und Berechtigungen für unterschiedliche Teams',
|
'Rollen und Berechtigungen für unterschiedliche Teams',
|
||||||
'Nachvollziehbare Kommunikation über Matrix-Räume'
|
'Nachvollziehbare Kommunikation in projektbezogenen Chats'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -122,7 +122,7 @@ const audiences = [
|
|||||||
points: [
|
points: [
|
||||||
'Offener Code und nachvollziehbare Architektur',
|
'Offener Code und nachvollziehbare Architektur',
|
||||||
'Docker-basierter Selfhost-Stack',
|
'Docker-basierter Selfhost-Stack',
|
||||||
'Matrix-Föderation und Integrationen kontrollierbar betreiben'
|
'Mandanteninterne Kommunikation kontrolliert betreiben'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user