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);
}
}