Die App signierte LiveKit-Tokens selbst – dafür steckte das SFU-Secret im App-Code (jeder mit der APK konnte sich Tokens für beliebige Räume und Identitäten ausstellen). Jetzt holt die App das Token von der bereits vorhandenen Server-Route /api/livekit-token (whoami-Prüfung, Identität = geprüfte Matrix-ID). Secret und JWT-Signieren sind aus dem Client entfernt. - Lesbare Fehlermeldungen (offline, 401, Serverfehler) statt stiller Fehler. - Gruppenanruf aus dem Chat-Kopf übergibt jetzt den Matrix-Client. - Tests: Unit-Test mit Mock-HTTP + echter LiveKit-Beitritt (Testkonto). - Offen: Handy-Anruftest; danach Secret-Rotation durch Bernd (Doku). Co-Authored-By: Claude Opus 5.5 <[email protected]>
90 lines
3.0 KiB
Dart
90 lines
3.0 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:http/http.dart' as http;
|
||
|
||
/// LiveKit-Beitritts-Token vom Pyramid-Server holen.
|
||
///
|
||
/// Früher hat die App die Tokens selbst signiert – dafür steckte das
|
||
/// LiveKit-Secret im App-Code, und wer die App hatte, konnte sich für jeden
|
||
/// Raum und jede Identität ein Token ausstellen. Jetzt signiert nur noch der
|
||
/// Server (`/api/livekit-token` im Dashboard-Server auf dem Pi): Er prüft den
|
||
/// Matrix-Login per whoami und setzt die LiveKit-Identität selbst auf die
|
||
/// geprüfte Matrix-ID. Siehe docs/LIVEKIT_TOKEN_MIGRATION.md.
|
||
class LiveKitTokenService {
|
||
static const endpoint =
|
||
'https://dashboard.steggi-matrix.work/api/livekit-token';
|
||
static const fallbackUrl = 'wss://livekit.steggi-matrix.work';
|
||
static const _timeout = Duration(seconds: 15);
|
||
|
||
/// Liefert Token und LiveKit-Adresse für [room]. Wirft
|
||
/// [LiveKitTokenException] mit einer verständlichen Meldung.
|
||
static Future<({String token, String url})> fetch({
|
||
required String room,
|
||
required String matrixToken,
|
||
http.Client? httpClient,
|
||
}) async {
|
||
final client = httpClient ?? http.Client();
|
||
try {
|
||
final res = await client
|
||
.post(
|
||
Uri.parse(endpoint),
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: jsonEncode({'room': room, 'matrix_token': matrixToken}),
|
||
)
|
||
.timeout(_timeout);
|
||
if (res.statusCode == 401) {
|
||
throw const LiveKitTokenException(
|
||
'Anmeldung vom Anrufserver abgelehnt – bitte neu anmelden.',
|
||
);
|
||
}
|
||
if (res.statusCode != 200) {
|
||
throw LiveKitTokenException(
|
||
'Anrufserver antwortet nicht richtig (HTTP ${res.statusCode}).',
|
||
);
|
||
}
|
||
final data = jsonDecode(res.body);
|
||
final token = data is Map ? data['token'] : null;
|
||
if (token is! String || token.isEmpty) {
|
||
throw const LiveKitTokenException(
|
||
'Anrufserver hat kein Zugangstoken geliefert.',
|
||
);
|
||
}
|
||
final url = (data as Map)['url'];
|
||
return (
|
||
token: token,
|
||
url: url is String && url.startsWith('wss://') ? url : fallbackUrl,
|
||
);
|
||
} on LiveKitTokenException {
|
||
rethrow;
|
||
} on TimeoutException {
|
||
throw const LiveKitTokenException(
|
||
'Anrufserver nicht erreichbar (Zeitüberschreitung).',
|
||
);
|
||
} on SocketException {
|
||
throw const LiveKitTokenException(
|
||
'Anrufserver nicht erreichbar – Internetverbindung prüfen.',
|
||
);
|
||
} on http.ClientException {
|
||
throw const LiveKitTokenException(
|
||
'Anrufserver nicht erreichbar – Internetverbindung prüfen.',
|
||
);
|
||
} on FormatException {
|
||
throw const LiveKitTokenException(
|
||
'Anrufserver hat eine unlesbare Antwort geschickt.',
|
||
);
|
||
} finally {
|
||
if (httpClient == null) client.close();
|
||
}
|
||
}
|
||
}
|
||
|
||
class LiveKitTokenException implements Exception {
|
||
final String message;
|
||
const LiveKitTokenException(this.message);
|
||
|
||
@override
|
||
String toString() => message;
|
||
}
|