using SameBeat.Domain.Enumy; using SameBeat.Domain.Wyjatki; namespace SameBeat.Domain.Encje; /// /// Uczestnik oczekujacy na akceptacje hosta -- zanim host wpusci go na scene (ETAP 3), siedzi /// w poczekalni z podgladem swojej kamery. Nick jest tu tez uzywany pozniej w czacie, dlatego /// juz teraz jest sanityzowany (bez '<' i '>' -- zeby nie dalo sie wstrzyknac HTML). /// public class UczestnikOczekujacy { public const int MaksymalnaDlugoscNicku = 40; public Guid Id { get; private set; } public Guid WydarzenieId { get; private set; } public string Nick { get; private set; } = string.Empty; /// Tozsamosc LiveKit -- generowana WYLACZNIE po stronie serwera, nigdy z inputu klienta. public string LiveKitIdentity { get; private set; } = string.Empty; public DateTime DolaczylUtc { get; private set; } public StatusUczestnikaOczekujacego Status { get; private set; } /// /// Hash (PBKDF2, jak haslo) sekretu sesji -- jedyny sposob, w jaki uczestnik moze pozniej /// udowodnic "to ja" bez zakladania konta (np. przy GET /api/join/{id}/status). Sekret jawny /// nigdy tu nie trafia i nigdy nie jest zapisywany -- zwracany jest uczestnikowi raz, w odpowiedzi /// na dolaczenie do poczekalni. /// public string? SekretSesjiHash { get; private set; } // Prywatny konstruktor bez parametrow -- wymagany przez EF Core do materializacji encji z bazy. private UczestnikOczekujacy() { } private UczestnikOczekujacy(Guid id, Guid wydarzenieId, string nick, string liveKitIdentity, DateTime dolaczylUtc) { Id = id; WydarzenieId = wydarzenieId; Nick = nick; LiveKitIdentity = liveKitIdentity; DolaczylUtc = dolaczylUtc; Status = StatusUczestnikaOczekujacego.Oczekuje; } /// /// Tworzy nowego uczestnika oczekujacego. Walidacja nicku tutaj chroni przed powstaniem encji /// w niepoprawnym stanie niezaleznie od tego, ktory serwis aplikacji jej uzyje. /// public static UczestnikOczekujacy Dolacz(Guid wydarzenieId, string nick) { if (wydarzenieId == Guid.Empty) { throw new NieprawidlowyStanEncjiWyjatek("Uczestnik oczekujacy musi byc powiazany z konkretnym wydarzeniem."); } var nickOczyszczony = OczyscNick(nick); // Tozsamosc LiveKit generowana tu, na serwerze -- klient nie ma na nia zadnego wplywu, // wiec nie da sie podszyc pod cudza sesje kamery ani przewidziec jej wartosci z gory. var liveKitIdentity = "uczestnik-" + Guid.NewGuid(); return new UczestnikOczekujacy(Guid.NewGuid(), wydarzenieId, nickOczyszczony, liveKitIdentity, DateTime.UtcNow); } private static string OczyscNick(string nick) { if (string.IsNullOrWhiteSpace(nick)) { throw new NieprawidlowyStanEncjiWyjatek("Nick jest wymagany."); } var przycietyNick = nick.Trim(); if (przycietyNick.Length > MaksymalnaDlugoscNicku) { throw new NieprawidlowyStanEncjiWyjatek($"Nick moze miec maksymalnie {MaksymalnaDlugoscNicku} znakow."); } // Podstawowa sanityzacja przed wstrzyknieciem HTML -- ten sam nick trafi pozniej do czatu, // wiec odrzucamy znaki, ktorymi dalo by sie otworzyc znacznik, juz przy wejsciu do systemu. if (przycietyNick.Contains('<') || przycietyNick.Contains('>')) { throw new NieprawidlowyStanEncjiWyjatek("Nick nie moze zawierac znakow '<' ani '>'."); } return przycietyNick; } public void Odrzuc() => Status = StatusUczestnikaOczekujacego.Odrzucony; public void Zbanuj() => Status = StatusUczestnikaOczekujacego.Zbanowany; /// Host przypisal uczestnika do slotu sceny -- od teraz moze wymienic sekret sesji na token do pokoju glownego. public void WpuscNaScene() => Status = StatusUczestnikaOczekujacego.NaScenie; /// Slot zostal zwolniony (host usunal ze sceny albo zmienil uklad) -- uczestnik wraca do poczekalni. public void ZdejmijZeSceny() => Status = StatusUczestnikaOczekujacego.Oczekuje; public void UstawSekretSesji(string sekretHash) { if (string.IsNullOrWhiteSpace(sekretHash)) { throw new NieprawidlowyStanEncjiWyjatek("Hash sekretu sesji nie moze byc pusty."); } SekretSesjiHash = sekretHash; } }