using System.IdentityModel.Tokens.Jwt; using System.Text; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using SameBeat.Application.Interfejsy; using SameBeat.Domain.Enumy; namespace SameBeat.Infrastructure.LiveKit; /// /// Generuje access token LiveKit zgodny ze specyfikacja LiveKit: JWT podpisany HMAC-SHA256 kluczem /// ApiSecret, z claimem "video" niosacym granty. LiveKit SDK po stronie klienta odczytuje ten /// claim wprost -- format musi byc dokladnie taki, jak oczekuje LiveKit, nie ASP.NET. /// public class SerwisTokenowLiveKit : ISerwisTokenowLiveKit { private readonly UstawieniaLiveKit _ustawienia; public SerwisTokenowLiveKit(IOptions ustawienia) { _ustawienia = ustawienia.Value; if (string.IsNullOrWhiteSpace(_ustawienia.ApiKey) || string.IsNullOrWhiteSpace(_ustawienia.ApiSecret)) { throw new InvalidOperationException( "Brak ustawien 'LiveKit:ApiKey' / 'LiveKit:ApiSecret' w konfiguracji -- uzupelnij appsettings przed startem API."); } // HMAC-SHA256 wymaga klucza >= 256 bitow (32 bajty). Za krotki sekret rzucalby dopiero przy // pierwszym podpisywaniu tokenu kryptycznym IDX10720 -- sprawdzamy to od razu, z czytelnym komunikatem. if (Encoding.UTF8.GetByteCount(_ustawienia.ApiSecret) < 32) { throw new InvalidOperationException( "Ustawienie 'LiveKit:ApiSecret' jest za krotkie -- HMAC-SHA256 wymaga co najmniej 32 znakow (256 bitow)."); } } public string WygenerujToken(string identity, string nazwaWyswietlana, RolaUzytkownika rola, string nazwaPokoju) { if (string.IsNullOrWhiteSpace(identity)) { throw new ArgumentException("Identity jest wymagane do wygenerowania tokenu LiveKit.", nameof(identity)); } if (string.IsNullOrWhiteSpace(nazwaWyswietlana)) { throw new ArgumentException("Nazwa wyswietlana (nick) jest wymagana do wygenerowania tokenu LiveKit.", nameof(nazwaWyswietlana)); } if (string.IsNullOrWhiteSpace(nazwaPokoju)) { throw new ArgumentException("Nazwa pokoju jest wymagana do wygenerowania tokenu LiveKit.", nameof(nazwaPokoju)); } // Uprawnienia wyliczane wylacznie z roli po stronie serwera -- caller nie moze ich zmienic, // np. widz nigdy nie dostanie canPublish=true, nawet gdyby podal to w requescie. // Uczestnik publikuje TYLKO kamere -- mikrofon nigdy nie jest domyslnie publikowany, dlatego // oprocz canPublish niesiemy tez canPublishSources ograniczone do "camera": to blokuje // publikacje audio na poziomie samego tokenu (fizycznie, nie tylko przez UI klienta). // Host na tym etapie steruje wylacznie przez REST/SignalR -- nie publikuje wlasnej kamery/mikrofonu, // wiec dostaje tylko prawo subskrypcji (podglad kamer z poczekalni). var (mozePublikowac, mozeSubskrybowac, zrodlaPublikacji) = rola switch { RolaUzytkownika.Widz => (false, true, Array.Empty()), RolaUzytkownika.Uczestnik => (true, true, new[] { "camera" }), RolaUzytkownika.Host => (false, true, Array.Empty()), RolaUzytkownika.Moderator => (true, true, Array.Empty()), _ => (false, true, Array.Empty()) }; var teraz = DateTime.UtcNow; var wygasa = teraz.AddSeconds(_ustawienia.CzasZyciaSekundy); var grantVideo = new Dictionary { ["roomJoin"] = true, ["room"] = nazwaPokoju, ["canPublish"] = mozePublikowac, ["canSubscribe"] = mozeSubskrybowac, ["canPublishData"] = true }; if (zrodlaPublikacji.Length > 0) { // Pole z dokladnie taka nazwa i formatem (tablica stringow: "camera"/"microphone"/ // "screen_share"/"screen_share_audio") oczekuje LiveKit VideoGrant -- SDK klienta odrzuci // publikacje zrodla spoza tej listy, nawet jesli canPublish=true. grantVideo["canPublishSources"] = zrodlaPublikacji; } var payload = new JwtPayload { ["iss"] = _ustawienia.ApiKey, ["sub"] = identity, ["name"] = nazwaWyswietlana, ["nbf"] = EpochTime.GetIntDate(teraz), ["exp"] = EpochTime.GetIntDate(wygasa), ["jti"] = Guid.NewGuid().ToString(), ["video"] = grantVideo }; var kluczPodpisujacy = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_ustawienia.ApiSecret)); var poswiadczenia = new SigningCredentials(kluczPodpisujacy, SecurityAlgorithms.HmacSha256); var naglowek = new JwtHeader(poswiadczenia); var token = new JwtSecurityToken(naglowek, payload); return new JwtSecurityTokenHandler().WriteToken(token); } }