# Platforma webinarowa Platforma do organizacji szkolen i webinarow. Osobna instancja dla kazdego klienta, roznice miedzy instancjami wylacznie w konfiguracji. Backend obsluguje konfiguracje instancji, wysylke e-mail i SMS, webinary, zapisy uczestnikow oraz uwierzytelnianie JWT. Frontend jest na razie strona startowa sprawdzajaca polaczenie z backendem. ## Uruchomienie lokalne Wymagany Docker z wtyczka Compose. Nic poza tym nie jest potrzebne, Maven i Node dzialaja wewnatrz obrazow. ### 1. Przygotuj konfiguracje ```bash cp .env.example .env ``` Wartosci domyslne wystarczaja do uruchomienia lokalnego. Przed wdrozeniem u klienta ustaw wlasne `DB_PASSWORD`, `JWT_SECRET` i klucze MinIO. ### 2. Zbuduj i uruchom kontenery ```bash docker compose up -d --build ``` Pierwszy build pobiera zaleznosci Mavena i npm, wiec trwa kilka minut. Kolejne korzystaja z cache. ### 3. Sprawdz, czy dziala ```bash curl http://localhost:8080/api/health ``` Oczekiwana odpowiedz: ```json {"status":"UP","version":"0.1.0"} ``` Aplikacja jest dostepna pod . Strona startowa pokazuje status i wersje backendu pobrane przez `/api/health`. ## Dostepne adresy | Usluga | Adres | Uwagi | | --- | --- | --- | | Frontend | | nginx serwuje SPA i proxuje `/api` | | Backend | | bezposrednio, z pominieciem nginx | | Swagger UI | | tylko przy `SWAGGER_ENABLED=true` | | Konsola MinIO | | login z `MINIO_ACCESS_KEY` | | PostgreSQL | `localhost:5432` | dane z `.env` | Porty zmieniasz zmiennymi `*_PORT_EXPOSED` w `.env`. ## Strona publiczna Czesc dostepna bez logowania, dla uczestnikow. | Ekran | Sciezka | | --- | --- | | Strona webinaru z formularzem zapisu | `/w/{slug}` | | Wlasny zapis, kalendarz i rezygnacja | `/zapis/{token}` | Adres do rozeslania uczestnikom to `APP_PUBLIC_URL` z ustawien plus `/w/{slug}`, na przyklad . Formularz jest ukrywany, gdy zapisy nie przyjmuja zgloszen. Uczestnik widzi wtedy powod: brak miejsc, miniety termin, zapisy zamkniete recznie, webinar odwolany albo webinar juz sie odbyl. Wersje robocze nie sa dostepne publicznie. ## Adresy: korzen nalezy do goscia, panel ma wlasny prefiks Pod jest **publiczna strona glowna Akademii**, dostepna bez logowania. Panel administracyjny siedzi pod `/panel`. ### Publiczne, bez logowania | Ekran | Sciezka | | --- | --- | | Strona glowna Akademii | `/` | | Kursy, webinary, cennik | `/kursy`, `/webinary`, `/cennik` | | Strona kursu | `/kurs/{slug}` | | Strona zapisu na webinar | `/w/{slug}` | | Weryfikacja certyfikatu | `/certyfikat/{numer}` | | Logowanie (wspolne) | `/logowanie` | ### Konta kursantow Rejestracja i odzyskiwanie hasla dotycza **wylacznie kont uczestnikow**. Konta panelu zaklada administrator instancji. | Ekran | Sciezka | | --- | --- | | Rejestracja | `/rejestracja` | | Przypomnienie hasla | `/przypomnienie-hasla` | | Ustawienie nowego hasla | `/reset-hasla/{token}` | | Potwierdzenie adresu | `/potwierdzenie/{token}` | | Pulpit kursanta | `/moje-konto` | ### Panel administracyjny | Ekran | Sciezka | Dostep | | --- | --- | --- | | Pulpit | `/panel` | ADMIN, MODERATOR | | Webinary | `/panel/webinary` | ADMIN, MODERATOR | | Formularz webinaru | `/panel/webinary/nowy`, `/panel/webinary/{id}/edycja` | ADMIN, MODERATOR | | Zapisani | `/panel/webinary/{id}/zapisy` | ADMIN, MODERATOR | | Kursy | `/panel/kursy` | ADMIN, MODERATOR | | Ustawienia | `/panel/ustawienia` | tylko ADMIN | | Uzytkownicy | `/panel/uzytkownicy` | tylko ADMIN | Stare adresy panelu (`/ustawienia`, `/uzytkownicy`, `/kontakty`, `/zapytania`, `/zamowienia`, `/banery`, `/pulpit`) oraz stare adresy Akademii (`/akademia/*`) **przekierowuja na nowe**, wiec zapisane zakladki dzialaja dalej. **Wyjatek: `/kursy` i `/webinary` nie przekierowuja.** Oba adresy naleza teraz do publicznych list, bo sa krotkie i trafiaja do wyszukiwarek oraz materialow reklamowych. Kolizje rozstrzygnieto na korzysc czesci publicznej, swiadomie godzac sie na to, ze administrator z zapisana zakladka trafi na oferte. Lagodzi to pasek pokazywany na obu listach **wylacznie osobom z rola ADMIN lub MODERATOR**: mowi, ze panel jest pod `/panel`, prowadzi tam odnosnikiem i znika na stale po zamknieciu. ### Jedno logowanie dla obu swiatow Formularz pod `/logowanie` jest wspolny. Backend (`POST /api/auth/login/unified`) sprawdza haslo **osobno** w kontach panelu i w kontach kursantow, po czym odpowiada, dokad kierowac: front niczego nie zgaduje. Hasla obu swiatow pozostaja **niezalezne**. Token panelu powstaje wylacznie wtedy, gdy haslo pasowalo do konta panelu, wiec wyciek hasla kursanta nadal nie otwiera panelu. Gdy to samo haslo pasuje w obu miejscach, uzytkownik dostaje wybor, dokad wejsc. Dotychczasowe `POST /api/auth/login` i `POST /api/academy/login` **dzialaja bez zmian**. Terminy sa prezentowane w strefie czasowej webinaru, zawsze z jawnym oznaczeniem strefy. ## Logowanie Endpointy `/api/admin/**` wymagaja tokenu JWT. Pierwsze konto administratora powstaje przy pierwszym starcie z `ADMIN_EMAIL` i `ADMIN_PASSWORD` z pliku `.env`. Gdy `ADMIN_PASSWORD` jest puste, haslo jest losowane i wypisane **jeden raz** w logu: ```bash docker compose logs backend | grep -A6 "UTWORZONO PIERWSZE KONTO" ``` Logowanie: ```bash curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"admin@localhost","password":"twoje-haslo"}' ``` Token z odpowiedzi doklada sie do kolejnych zadan: ```bash TOKEN="wklej-token" curl http://localhost:8080/api/admin/webinars -H "Authorization: Bearer $TOKEN" ``` Zmiana wlasnego hasla: ```bash curl -X POST http://localhost:8080/api/auth/change-password \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"currentPassword":"stare","newPassword":"nowe-haslo-min-10-znakow"}' ``` ### Role | Sciezka | ADMIN | MODERATOR | bez tokenu | | --- | --- | --- | --- | | `/api/health`, `/api/public/**` | tak | tak | tak | | `/api/admin/webinars/**` | tak | tak | 401 | | `/api/admin/settings/**` | tak | 403 | 401 | | `/api/admin/users/**` | tak | 403 | 401 | ### Blokada po nieudanych logowaniach Po 5 nieudanych probach dla adresu e-mail (albo 20 dla adresu IP) logowanie jest odrzucane przez 15 minut, takze przy poprawnym hasle. Odpowiedz jest taka sama jak przy zlym hasle. Liczniki sa w pamieci i znikaja po restarcie kontenera. ## Konfiguracja instancji przez API Ustawienia trzymane sa w tabeli `app_settings` i maja pierwszenstwo przed zmiennymi srodowiskowymi. Zmiana dziala od razu, bez restartu aplikacji. Lista ustawien: ```bash curl http://localhost:8080/api/admin/settings -H "Authorization: Bearer $TOKEN" ``` Wszystkie ponizsze wywolania wymagaja tokenu administratora, patrz sekcja Logowanie. Wartosci kluczy typu PASSWORD nigdy nie wracaja w odpowiedzi. Zamiast wartosci jest flaga `valueSet`, ktora mowi czy haslo jest ustawione. ### Ustawienie konta SMTP ```bash curl -X PUT http://localhost:8080/api/admin/settings \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "MAIL_HOST": "pzators.nazwa.pl", "MAIL_PORT": "465", "MAIL_SSL_ENABLED": "true", "MAIL_USERNAME": "powiadomienia@twojadomena.pl", "MAIL_PASSWORD": "haslo-do-konta", "MAIL_FROM_NAME": "Nazwa nadawcy", "APP_BRAND_NAME": "Nazwa marki", "APP_PUBLIC_URL": "https://webinar.twojadomena.pl" }' ``` Puste pole przy `MAIL_PASSWORD` oznacza "nie zmieniaj" i nie kasuje zapisanego hasla. ### Test wysylki e-mail ```bash curl -X POST http://localhost:8080/api/admin/settings/test-email \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"recipient":"twoj@adres.pl"}' ``` Odpowiedz zawiera `success` oraz komunikat. Przy bledzie jest to pelna tresc bledu z serwera SMTP, na przyklad `535 5.7.0 Authentication error`. ### Bramka SMS ```bash curl -X PUT http://localhost:8080/api/admin/settings \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "SMS_ENABLED": "true", "SMS_API_KEY": "klucz-api-bramki", "SMS_CREATOR": "Webinar" }' curl -X POST http://localhost:8080/api/admin/settings/test-sms \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"recipient":"600100200"}' ``` ## Wdrozenie na serwerze klienta Kazdy klient dostaje wlasna instancje: wlasny serwer, wlasna domena, wlasna baza. Kod jest ten sam, roznice wylacznie w konfiguracji. ### Co jest po stronie klienta, a co po naszej Aplikacja nasluchuje **po HTTP, wewnatrz sieci Dockera**, na petli lokalnej serwera (domyslnie `127.0.0.1:8081`). Nie obsluguje SSL i nie jest wystawiona bezposrednio do internetu. **Certyfikat SSL i reverse proxy sa po stronie serwera klienta.** Administrator serwera musi postawic przed aplikacja nginx, Caddy, Traefik albo inny serwer, ktory zakonczy polaczenie HTTPS i przekaze ruch na port aplikacji. Bez tego aplikacja nie bedzie dostepna z zewnatrz, i tak ma byc. Przykladowa konfiguracja nginx po stronie klienta: ```nginx server { listen 443 ssl http2; server_name webinary.klient.pl; ssl_certificate /etc/letsencrypt/live/webinary.klient.pl/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/webinary.klient.pl/privkey.pem; location / { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # aplikacja czyta adres uczestnika z tego naglowka, wiec musi byc ustawiony proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # naglowek User-Agent jest potrzebny do rozpoznania robotow komunikatorow proxy_set_header User-Agent $http_user_agent; } } server { listen 80; server_name webinary.klient.pl; return 301 https://$host$request_uri; } ``` ### Krok 1: przygotowanie serwera Wymagany Docker z wtyczka Compose. Sprawdz: ```bash docker --version docker compose version ``` Sklonuj repozytorium i wejdz do katalogu projektu: ```bash git clone webinar-platform cd webinar-platform ``` ### Krok 2: konfiguracja instancji ```bash cp .env.production.example .env.production chmod 600 .env.production ``` Wygeneruj sekrety i wpisz je do `.env.production`: ```bash # sekret JWT, minimum 32 znaki. Aplikacja nie wystartuje bez niego. openssl rand -base64 48 # haslo bazy danych openssl rand -base64 24 # klucz tajny MinIO openssl rand -base64 24 ``` Uzupelnij w `.env.production`: | Zmienna | Wartosc | | --- | --- | | `JWT_SECRET` | wynik `openssl rand -base64 48` | | `DB_PASSWORD` | wynik `openssl rand -base64 24` | | `MINIO_ACCESS_KEY` | dowolny login, np. `webinar-minio` | | `MINIO_SECRET_KEY` | wynik `openssl rand -base64 24` | | `ADMIN_EMAIL` | adres pierwszego administratora | | `ADMIN_PASSWORD` | zostaw **puste**, haslo zostanie wylosowane | | `APP_PUBLIC_URL` | `https://webinary.klient.pl`, bez ukosnika na koncu | | `CORS_ALLOWED_ORIGINS` | to samo co `APP_PUBLIC_URL` | `ADMIN_PASSWORD` lepiej zostawic puste: haslo zostanie wygenerowane losowo i pokazane raz w logu, wiec nie bedzie lezalo w pliku na dysku. Konfiguracji SMTP i bramki SMS **nie ma w tym pliku**. Ustawia sie ja w panelu po uruchomieniu, w kroku 5. ### Krok 3: sprawdzenie konfiguracji Przed pierwszym uruchomieniem: ```bash ./scripts/check-config.sh --skip-db ``` Skrypt **odmawia startu** (kod wyjscia 1), gdy `JWT_SECRET` jest pusty, krotszy niz 32 znaki albo rowny wartosci przykladowej, gdy `ADMIN_PASSWORD` jest rowne przykladowej, albo gdy `APP_PUBLIC_URL` jest pusty lub wskazuje na `localhost`. Kazdy z tych przypadkow kosztuje na produkcji wiecej niz nieudany start: przykladowy sekret oznacza, ze **kazdy, kto widzial repozytorium, moze podrobic token administratora**, a adres lokalny oznacza martwe linki w kazdym mailu. Po uruchomieniu warto powtorzyc bez `--skip-db`: skrypt odpyta wtedy instancje o klucze wymagane, ktore ustawia sie w panelu (SMTP, bramka SMS, dostawca wideo). Lista wymaganych kluczy pochodzi z katalogu `SettingKey` w backendzie, wiec nie rozjedzie sie z kodem przy dodaniu nowego ustawienia. ### Krok 4: uruchomienie ```bash docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build ``` Pierwszy build pobiera zaleznosci Mavena i npm, wiec trwa kilka minut. Sprawdz, czy wszystkie kontenery dzialaja: ```bash docker compose -f docker-compose.prod.yml --env-file .env.production ps ``` ### Krok 5: haslo administratora i pierwsze logowanie Jesli `ADMIN_PASSWORD` bylo puste, odczytaj wygenerowane haslo z logu: ```bash docker compose -f docker-compose.prod.yml --env-file .env.production \ logs backend | grep -A6 "UTWORZONO PIERWSZE KONTO" ``` Haslo jest pokazane **tylko raz**. Zapisz je w menedzerze hasel, zaloguj sie do panelu pod `https://webinary.klient.pl/logowanie` i zmien je od razu: Ustawienia konta, albo `POST /api/auth/change-password`. ### Krok 6: konfiguracja poczty i bramki SMS przez panel SMTP i bramka SMS konfigurowane sa **wylacznie w panelu**, nie w pliku `.env`. Wartosci trafiaja do bazy, wiec zmiana dziala od razu, bez restartu kontenera, a klient moze poprawic je samodzielnie. 1. Zaloguj sie do panelu i wejdz w **Ustawienia**. 2. Uzupelnij sekcje **Konto e-mail (SMTP)**: - `MAIL_HOST` serwer poczty, na przyklad `smtp.domena.pl` - `MAIL_PORT` 465 dla SSL albo 587 dla STARTTLS - `MAIL_SSL_ENABLED` wlaczone dla portu 465 - `MAIL_USERNAME` pelny adres e-mail nadawcy - `MAIL_PASSWORD` haslo do konta - `MAIL_FROM_NAME` nazwa widoczna dla odbiorcy 3. Kliknij **Zapisz ustawienia**. 4. W bloku **Test konfiguracji** pod ta sekcja wpisz swoj adres i kliknij **Wyslij**. Przy powodzeniu zobaczysz potwierdzenie, przy bledzie pelna tresc odpowiedzi serwera SMTP, na przyklad `535 5.7.0 Authentication error`. 5. Sprawdz, czy wiadomosc dotarla, takze do folderu ze spamem. 6. Analogicznie uzupelnij sekcje **Bramka SMS** i przetestuj ja przyciskiem **Wyslij** w bloku testu. Haslo SMTP i klucz API bramki sa sekretami: nigdy nie wracaja w odpowiedzi API, a puste pole przy zapisie oznacza brak zmiany, nie skasowanie wartosci. ### Krok 7: weryfikacja wdrozenia Pelny test odbiorczy, punkt po punkcie, jest w osobnym pliku: **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)**. Obejmuje obie sciezki (webinarowa i Akademii) oraz to, czego nie da sie sprawdzic lokalnie: notyfikacje ITN Autopay, nagrywanie przez Egress, webhook dostawcy wideo i zajawki w komunikatorach. Skrocone sprawdzenie: ```bash # aplikacja odpowiada wewnatrz serwera curl http://127.0.0.1:8081/api/health # aplikacja odpowiada przez reverse proxy, z zewnatrz curl https://webinary.klient.pl/api/health ``` Oczekiwana odpowiedz w obu przypadkach: ```json {"status":"UP","version":"0.1.0"} ``` Sprawdz jeszcze: - panel otwiera sie pod `https://webinary.klient.pl/logowanie` i logowanie dziala, - baza i MinIO **nie** sa dostepne z zewnatrz: `nc -zv webinary.klient.pl 5432` oraz `nc -zv webinary.klient.pl 9000` powinny odmowic polaczenia, - utworz probny webinar, opublikuj go i wejdz na `/w/{slug}` z telefonu, - zapisz sie na probny webinar i sprawdz, czy przyszedl mail z zalacznikiem `invite.ics`, - usun probny webinar przed przekazaniem instancji klientowi. ### Aktualizacja instancji ```bash git pull docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build ``` Migracje Flyway wykonuja sie automatycznie przy starcie backendu. Przed aktualizacja wykonaj kopie zapasowa, patrz sekcja nizej. ### Zatrzymanie i restart ```bash # restart pojedynczej uslugi docker compose -f docker-compose.prod.yml --env-file .env.production restart backend # zatrzymanie calosci, dane w wolumenach zostaja docker compose -f docker-compose.prod.yml --env-file .env.production down ``` Nie uzywaj `down -v` na produkcji: ta komenda usuwa wolumeny wraz z danymi. ## Kopia zapasowa i odtworzenie ### Wykonanie kopii ```bash ./scripts/backup.sh ``` Kopia trafia do `./backups/RRRRMMDD-GGMMSS/` i zawiera: | Plik | Zawartosc | | --- | --- | | `baza.dump` | zrzut PostgreSQL w formacie custom | | `minio.tar.gz` | zawartosc wolumenu MinIO | | `env.production.kopia` | konfiguracja instancji, **zawiera sekrety** | | `INFO.txt` | data, nazwa serwera, instrukcja odtworzenia | Katalog docelowy mozna podac jako argument: ```bash ./scripts/backup.sh /mnt/backup/webinar ``` Skrypt zachowuje 14 ostatnich kopii i usuwa starsze. Zmien zmienna `KEEP_LAST`, gdy potrzebujesz innej retencji. ### Kopia automatyczna Wpis do crona, codziennie o 3:00: ```bash crontab -e ``` ``` 0 3 * * * cd /opt/webinar-platform && ./scripts/backup.sh /mnt/backup/webinar >> /var/log/webinar-backup.log 2>&1 ``` Kopie zawieraja sekrety, wiec katalog docelowy musi miec ograniczone uprawnienia, a jesli trafiaja poza serwer, powinny byc szyfrowane. #### Ile miejsca potrzeba Rozmiar kopii to prawie w calosci **nagrania**: baza nawet przy kilku tysiacach zapisow rzadko przekracza 100 MB, a jedno nagranie godzinnego webinaru w 720p wazy 400 MB do 1 GB. | Skladnik | Rozmiar jednej kopii | | --- | --- | | Baza (`baza.dump`, format custom) | 5-100 MB | | Materialy i obrazki w tresci | 50-500 MB | | Certyfikaty PDF | okolo 200 kB na sztuke | | **Nagrania** | **400 MB - 1 GB na godzine nagrania** | Wzor na miejsce potrzebne przy domyslnym `KEEP_LAST=14`: ``` miejsce = (rozmiar wolumenu MinIO + rozmiar bazy) x 14 ``` Przyklad: instancja z 20 godzinami nagran zajmuje okolo 12 GB, wiec 14 kopii wymaga **okolo 170 GB**. Przy takiej skali warto: - zmniejszyc `KEEP_LAST` do 7 i trzymac kopie tygodniowe osobno, - albo wylaczyc nagrania z kopii codziennej i archiwizowac je rzadziej: pliki w MinIO sa **niezmienne po wgraniu**, wiec kopia sprzed tygodnia zawiera dokladnie te same nagrania co dzisiejsza. **Sprawdzenie biezacego rozmiaru:** ```bash # wolumen MinIO, czyli nagrania, materialy, certyfikaty i obrazki docker system df -v | grep minio-data # baza docker compose -f docker-compose.prod.yml exec -T postgres \ psql -U webinar -d webinar -c "SELECT pg_size_pretty(pg_database_size('webinar'));" ``` **Kopia bez nagran**, gdy miejsce jest ograniczone: zrzut samej bazy wystarczy do odtworzenia calej konfiguracji, wszystkich zapisow, kursow i wynikow. Zginie wtedy wylacznie **tresc plikow**, a wpisy w bazie zostana i beda wskazywac na nieistniejace obiekty. ``` 0 3 * * * cd /opt/webinar-platform && SKIP_MINIO=1 ./scripts/backup.sh >> /var/log/webinar-backup.log 2>&1 0 4 * * 0 cd /opt/webinar-platform && ./scripts/backup.sh /mnt/backup/webinar-pelna >> /var/log/webinar-backup.log 2>&1 ``` Pierwszy wpis: codziennie sama baza. Drugi: raz w tygodniu calosc. ### Odtworzenie ```bash ./scripts/restore.sh ./backups/20260828-143000 ``` Skrypt pokazuje metryczke kopii i wymaga wpisania `ODTWORZ`, zeby kontynuowac. Nastepnie zatrzymuje backend, odtwarza baze i pliki MinIO, i uruchamia backend z powrotem. **Odtworzenie nadpisuje biezace dane.** Przed odtworzeniem wykonaj kopie stanu biezacego, zeby dalo sie wrocic: ```bash ./scripts/backup.sh ./scripts/restore.sh ./backups/20260828-143000 ``` Po odtworzeniu sprawdz: ```bash curl http://127.0.0.1:8081/api/health ``` oraz zaloguj sie do panelu i obejrzyj liste webinarow i zapisow. ### Odtworzenie na nowym serwerze 1. Zainstaluj Dockera, sklonuj repozytorium. 2. Skopiuj `env.production.kopia` z kopii jako `.env.production` i ustaw `chmod 600`. Zachowaj **ten sam** `JWT_SECRET`, inaczej wszyscy zostana wylogowani, oraz to samo `DB_PASSWORD`, bo odtwarzana baza go uzywa. 3. Uruchom uslugi: `docker compose -f docker-compose.prod.yml --env-file .env.production up -d`. 4. Odtworz dane: `./scripts/restore.sh `. 5. Przekieruj domene na nowy serwer i sprawdz `/api/health`. ## Wideo: wybor dostawcy Platforma nie jest przywiazana do jednego dostawcy. Wybor jest na dwoch poziomach: ustawienie instancji (`Ustawienia > Wideo`) i opcjonalny wybor przy pojedynczym webinarze, ktory ma pierwszenstwo. ### Co gwarantuje ktory dostawca | Dostawca | Uprawnienia | Nagrywanie | Wymaga | | --- | --- | --- | --- | | **LIVEKIT** | egzekwuje **serwer**, na podstawie podpisanego tokenu | automatyczne, prosto do MinIO | wlasny serwer albo LiveKit Cloud | | **JITSI** z JWT | serwer egzekwuje **tylko role moderatora** | reczne wgranie pliku | wlasny serwer Jitsi z JWT | | **JITSI** bez JWT | **tylko interfejs**, pokoj otwarty dla kazdego | reczne wgranie pliku | nic, dziala na `meet.jit.si` | | **DAILY** | egzekwuje serwer | pobierane od dostawcy | konto Daily.co | | **NONE** | brak, organizator wkleja wlasny link | reczne wgranie pliku | nic | **Roznica miedzy LiveKit a Jitsi bez JWT jest zasadnicza, nie kosmetyczna.** Przy LiveKit uczestnik bez prawa publikacji nie wlaczy kamery, bo serwer odrzuci strumien. Przy Jitsi bez JWT ukrycie przycisku kamery jest jedynym, co go powstrzymuje, a wystarczy wejsc wlasnym klientem, zeby to obejsc. ### Wlasny serwer LiveKit Kontenery sa w profilu `livekit`, wylaczonym domyslnie: instancja w trybie NONE albo na Jitsi nie uruchamia niczego zbednego. ```bash # uzupelnij w .env: LIVEKIT_API_KEY, LIVEKIT_API_SECRET, TURN_PASSWORD docker compose --profile livekit up -d ``` Ten sam klucz i sekret wpisz w panelu (`Ustawienia > Wideo`): tutaj konfiguruja serwer, w panelu sluza do podpisywania tokenow wejscia. **Porty do otwarcia na firewallu:** | Port | Protokol | Do czego | | --- | --- | --- | | 7880 | TCP | sygnalizacja WebSocket i API serwera | | 7881 | TCP | media po TCP, droga zapasowa gdy UDP jest zablokowane | | 50000-50100 | **UDP** | **glowna droga audio i wideo** | | 49160-49200 | UDP | TURN, dla uczestnikow za restrykcyjnym NAT-em | **Zakres UDP jest obowiazkowy.** Bez niego uczestnicy dolacza do pokoju i zobacza liste obecnych, ale nie beda sie slyszec ani widziec. To najczestsza przyczyna zgloszenia "wideo nie dziala" przy poprawnie skonfigurowanym serwerze. TURN (`coturn`) dziala w `network_mode: host`, bo musi widziec prawdziwe adresy IP: za NAT-em Dockera rozdawalby uczestnikom adresy wewnetrzne kontenera. ### Pulapki uruchomieniowe LiveKit Zebrane przy uruchomieniu profilu na zywo. **Kazda z nich blokuje pierwsze wdrozenie**, a zadna nie daje czytelnego komunikatu. **1. Zakres portow UDP musi byc wolny w systemie.** Windows rezerwuje zakresy dla Hyper-V i WSL (sprawdzenie: `netsh interface ipv4 show excludedportrange protocol=udp`). Domyslne 50000-50100 wpada w rezerwacje 50000-50059 i kontener nie wstaje z bledem "ports are not available". Zmiana `LIVEKIT_UDP_RANGE` w `.env` wystarcza: zakres idzie do serwera zmienna `UDP_PORT` i **musi byc taki sam po obu stronach mapowania portow**, inaczej Docker odrzuca konfiguracje. **2. `use_external_ip: true` nadpisuje `NODE_IP`.** Przy uruchomieniu lokalnym serwer wykrywa publiczny adres lacza i rozglasza go uczestnikom, mimo ustawionego `NODE_IP=127.0.0.1`. Przegladarka na tej samej maszynie probuje sie tam polaczyc i nie zestawia mediow. Objaw jest mylacy: **uczestnik wchodzi do pokoju i widzi liste obecnych, ale nikogo nie slyszy i nie widzi**. Do testow lokalnych trzeba ustawic `use_external_ip: false` w `livekit/livekit.yaml`. **3. Adres serwera to dwie rozne wartosci.** `LIVEKIT_URL` opisuje adres widziany przez **przegladarke** (`ws://localhost:7880` lokalnie). Backend w kontenerze trafilby pod nim sam w siebie, wiec do wywolan API i Egress sluzy osobne `LIVEKIT_INTERNAL_URL` (`http://livekit:7880`). Bez tego nagrywanie konczy sie bledem "Failed to connect to localhost:7880" przy poprawnie dzialajacym serwerze. **4. Wersja serwera musi nadazac za `livekit-client`.** Klient 2.22 laczy sie sciezka `/rtc/v1`, ktorej serwer 1.8 nie zna: przegladarka dostaje 404 i komunikat "v1 RTC path not found. Consider upgrading your LiveKit server version". Sala wyglada na dzialajaca, ale nie plynie ani obraz, ani dzwiek. Obraz jest przypiety do `v1.13`; **podnoszac bibliotekÄ™ we froncie, sprawdz wersje serwera**. **5. Nagrywanie wymaga Egress i Redisa.** Sam serwer LiveKit nie nagrywa. Egress dostaje zadanie **przez Redisa**, wiec bez niego serwer przyjmuje zadanie, nie ma go komu przekazac, a nagrywanie konczy sie timeoutem. Oba kontenery sa w profilu `livekit`. **6. Egress uruchamia prawdziwa przegladarke.** Stad `cap_add: SYS_ADMIN` (piaskownica Chrome) i `shm_size: 1gb` (domyslne 64 MB nie wystarcza i przegladarka wysypuje sie w polowie nagrania). **7. Nie uzywaj `network_mode: host` dla Egress na Docker Desktop.** Na Windows i macOS kontenery chodza w maszynie wirtualnej, wiec tryb host laczy kontener z siecia **tej maszyny**, a nie z hostem: kontener widzi wtedy adres `192.168.65.x`, a nie adres systemu. Skutek jest widoczny dopiero w logach serwera: kandydaci ICE Egress maja **puste adresy IP** (`udp host :41557`), serwer odrzuca je jako `[filtered]`, a polaczenie mediow przechodzi `connected -> disconnected -> failed`. Nagranie konczy sie wtedy bledem "Start signal not received", ktory nie ma z sygnalem nic wspolnego. Egress ma zostac w zwyklej sieci Compose. **`network_mode: bridge` tez jest bledem**: to domyslna siec Dockera, a nie siec Compose, wiec kontener przestaje widziec `redis` i `livekit` po nazwie (`lookup redis: no such host`). **8. Egress otwiera strone szablonu.** Domyslnie pobiera ja z `https://template.livekit.io`, czyli z internetu: **instancja w sieci zamknietej nie nagra niczego**. Dlatego wskazujemy wlasny szablon (`LIVEKIT_TEMPLATE_URL`, domyslnie `http://frontend/nagrywanie`), serwowany z naszego frontu. ### Stan weryfikacji nagrywania **Nagrywanie przez Egress nie zostalo sprawdzone end-to-end.** Zweryfikowane zostalo wszystko, co do niego prowadzi, ale nie powstal plik. Co **dziala** i jest potwierdzone na zywo: - spotkania: obraz i dzwiek plyna miedzy uczestnikami (zmierzone przyrostem bajtow w statystykach WebRTC odbiorcy), - uprawnienia egzekwuje serwer: uczestnik z rola SPEAKER opublikowal audio, a **tej samej probie dla wideo serwer nie nadal zadnego wpisu o sciezce**, - Egress laczy sie z Redisem (`service ready`), przyjmuje zadanie, dolacza do pokoju i **zestawia polaczenie mediow po UDP** z prawidlowym adresem, - szablon nagrywania jest pobierany i wykonywany: jego JavaScript laczy sie z pokojem jako uczestnik `EG_*`. Gdzie proces sie **zatrzymuje**: szablon nie wola `window.START_RECORDING()`, bo w chwili nagrywania **nie zasubskrybowal zadnej sciezki**. Egress przerywa wtedy nagranie z kodem 412 "Start signal not received". Przyczyna po stronie srodowiska, nie kodu: zeby to domknac, potrzebny jest uczestnik faktycznie publikujacy obraz **w tej samej sieci co Egress**. W srodowisku testowym na Windows serwer rozglasza jeden adres (`NODE_IP`), wiec albo dziala przegladarka na hoscie, albo kontenery w sieci Dockera, ale nie oba naraz. Na Linuksie, gdzie kontenery i przegladarka widza ten sam adres, ten podzial nie wystepuje. **Funkcjonalnie nic to nie blokuje**: reczne wgranie pliku nagrania w zakladce "Po webinarze" dziala i pozostaje droga awaryjna dla kazdego dostawcy. Przed pierwszym wdrozeniem z automatycznym nagrywaniem nalezy te sciezke przejsc na docelowym serwerze. ### Jitsi Zakladamy serwer **zewnetrzny**, wskazywany domena w `Ustawienia > Wideo`. Nie ma go w `docker-compose.yml`: wlasny serwer Jitsi to kilkanascie uslug i osobne wdrozenie, a `meet.jit.si` dziala bez konfiguracji. Nagrywanie Jitsi wymaga komponentu Jibri, ktorego nie zakladamy: to osobna usluga z wlasna przegladarka i wlasnym zuzyciem CPU na kazde nagranie. Panel podpowiada w tym przypadku reczne wgranie pliku w zakladce "Po webinarze". ## Platnosci Autopay Kursy platne rozlicza Autopay (dawniej BlueMedia). Integracja obejmuje przedtransakcje, notyfikacje ITN i nadanie dostepu do kursu. ### Czego NIE DA SIE sprawdzic na localhost **ITN wymaga publicznej domeny z waznym certyfikatem.** Autopay sam laczy sie z naszym serwerem, wiec `localhost` jest dla niego nieosiagalny. Sprawdzone lokalnie zostalo wszystko, co dzieje sie **po** odebraniu komunikatu: | Element | Jak sprawdzony | | --- | --- | | Skrot na przykladzie z dokumentacji | `AutopayHashTest`, 11 testow | | Pomijanie pustych pol wraz z separatorem | `AutopayHashTest` | | Parsowanie ITN i odpornosc na XXE | `AutopayItnTest`, 11 testow | | Weryfikacja podpisu, kwoty i waluty | test przeplywu na instancji | | Idempotencja i FAILURE po SUCCESS | test przeplywu na instancji | ### Konfiguracja przed testem Ustawienia w panelu, `Ustawienia -> Aplikacja`: | Klucz | Wartosc na czas testow | | --- | --- | | `AUTOPAY_ENABLED` | `true` | | `AUTOPAY_MODE` | `TEST` | | `AUTOPAY_SERVICE_ID` | identyfikator z panelu Autopay | | `AUTOPAY_SHARED_KEY` | klucz wspoldzielony z panelu Autopay | | `AUTOPAY_HASH_ALGORITHM` | zgodny z ustawieniem po stronie operatora | | `AUTOPAY_ITN_ALLOWED_IPS` | puste, dopoki nie znasz aktualnych adresow | **Adresy srodowisk sa zaszyte w kodzie i nie sa ustawieniem:** `https://testpay.autopay.eu` dla testow, `https://pay.autopay.eu` dla produkcji. Pomylka oznaczalaby przyjmowanie prawdziwych pieniedzy na srodowisku testowym albo transakcje testowe na produkcji, ktore nigdy sie nie rozlicza. W panelu Autopay trzeba wskazac adres notyfikacji: ``` https://TWOJA-DOMENA/api/webhooks/autopay/itn ``` ### Co przetestowac po wdrozeniu Kolejnosc ma znaczenie: kazdy kolejny punkt zaklada, ze poprzedni przeszedl. 1. **Przedtransakcja odpowiada.** Kup kurs platny jako kursant. Powinienes trafic na bramke Autopay, a nie zobaczyc bledu. W logu backendu nie moze byc ostrzezenia `brak linku kontynuacji`. Zamowienie dostaje status `PENDING`. 2. **Blad konfiguracji jest czytelny.** Wpisz zly `AUTOPAY_SERVICE_ID` i sprobuj kupic: kursant ma zobaczyc komunikat o niedostepnosci platnosci, a zamowienie zostac w `NEW`, gotowe do ponowienia na tym samym numerze. W logu pojawi sie `SERVICE_NOT_FOUND`. 3. **Platnosc udana nadaje dostep.** Zaplac w srodowisku testowym operatora. Po powrocie strona pokazuje oczekiwanie na potwierdzenie, a dostep pojawia sie dopiero **po komunikacie ITN**. W panelu, na karcie zamowienia, ma byc widoczny komunikat `SUCCESS` z oznaczeniem `podpis poprawny` i `przetworzony`. 4. **Powrot z bramki niczego nie nadaje.** Wejdz recznie na adres powrotu bez platnosci. Dostepu ma nie byc: adres powrotu to zwykle przekierowanie przegladarki, ktore da sie wywolac samemu. 5. **Platnosc nieudana nie nadaje dostepu.** Powtorz zakup i wybierz w bramce scenariusz niepowodzenia. Zamowienie ma dostac `FAILED`, dostepu ma nie byc. 6. **FAILURE po SUCCESS nie odbiera dostepu.** To najwazniejszy przypadek: zaplac skutecznie, a potem doprowadz do komunikatu `FAILURE` z **innym** `remoteID`. Dostep ma **zostac**. Dokumentacja operatora opisuje ten przypadek wprost, a jego zlamanie zabiera kurs osobie, ktora zaplacila. 7. **Powtorzona notyfikacja nie dubluje niczego.** Autopay ponawia komunikat az do potwierdzenia. Po kilku powtorkach ma byc **jeden** zapis na kurs i jeden mail, a w dzienniku kilka wpisow, z ktorych tylko pierwszy ma `przetworzony`. 8. **Kwota niezgodna jest odrzucana.** Jesli operator udostepnia narzedzie do recznego wyslania ITN, wyslij komunikat z inna kwota niz zamowiona: ma dostac `NOTCONFIRMED`, a dostepu ma nie byc. 9. **Odpowiedz jest poprawnie podpisana.** Operator odrzuci potwierdzenie z bledny skrotem i bedzie ponawial notyfikacje. Objawem jest rosnaca liczba wpisow w dzienniku dla tego samego zamowienia. ### Scenariusze testowe operatora Dane kart i przelewow testowych sa w dokumentacji Autopay: - Dokumentacja techniczna: - Panel srodowiska testowego: Scenariusze udanej i nieudanej platnosci, wraz z danymi testowymi, operator udostepnia w panelu po zalozeniu konta testowego. **Nie zgadywac ich**: numery kart testowych zmieniaja sie miedzy wersjami uslugi. ### Gdzie szukac przy problemie - **Karta zamowienia w panelu** (`Akademia -> Zamowienia`) pokazuje pelny dziennik komunikatow ITN wraz z surowa trescia i informacja, czy podpis sie zgadzal. Przy sporze o platnosc to jedyny dowod. - **Komunikat odrzucony tez jest zapisany**, z oznaczeniem `podpis niepoprawny`. Brak wpisu oznacza, ze notyfikacja w ogole nie dotarla: sprawdz adres w panelu operatora i czy domena ma wazny certyfikat. - `docker compose logs -f backend | grep -i autopay` ### Reczne nadanie dostepu Przy platnosci przelewem tradycyjnym: karta zamowienia, przycisk **Nadaj dostep recznie**. Notatka jest wymagana, bo bez niej po miesiacu nie da sie ustalic, czy pieniadze wplynely i kto to sprawdzil. ## Testy Testy integracyjne wymagaja dzialajacego kontenera postgres, bo sprawdzaja blokade wiersza w prawdziwej bazie. ```bash docker compose up -d postgres docker run --rm --network webinar-platform_default \ -e TEST_DB_HOST=postgres \ -v "$(pwd)/backend:/b" -v webinar-m2:/root/.m2 -w /b \ maven:3.9-eclipse-temurin-21 mvn -B test ``` Najwazniejszy z nich, `RegistrationConcurrencyTest`, uruchamia 40 rownoczesnych zapisow na webinar z limitem 10 miejsc i sprawdza, ze w bazie powstalo dokladnie 10 rekordow. ## Zatrzymanie ```bash docker compose down ``` Dane w wolumenach zostaja zachowane. Aby usunac takze dane: ```bash docker compose down -v ``` ## Praca nad frontendem bez Dockera Serwer deweloperski Vite z hot reloadem, przy backendzie dzialajacym w Dockerze: ```bash cd frontend npm install npm run dev ``` Frontend startuje na . Aby odpytywal backend bezposrednio, utworz `frontend/.env.local`: ``` VITE_API_URL=http://localhost:8080/api ``` Origin `http://localhost:5173` jest juz na liscie `CORS_ALLOWED_ORIGINS`. ## Struktura ``` backend/ Spring Boot, pakiet com.softspm.webinar frontend/ React + TypeScript + Vite, nginx w obrazie produkcyjnym docker-compose.yml .env.example ``` Migracje bazy trafiaja do `backend/src/main/resources/db/migration` jako `Vn__opis.sql`. Schemat tworzy wylacznie Flyway, Hibernate go tylko weryfikuje (`ddl-auto: validate`). Test automatycznego deployu.