34 KiB
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
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
docker compose up -d --build
Pierwszy build pobiera zaleznosci Mavena i npm, wiec trwa kilka minut. Kolejne korzystaja z cache.
3. Sprawdz, czy dziala
curl http://localhost:8080/api/health
Oczekiwana odpowiedz:
{"status":"UP","version":"0.1.0"}
Aplikacja jest dostepna pod http://localhost:8081. Strona startowa pokazuje status
i wersje backendu pobrane przez /api/health.
Dostepne adresy
| Usluga | Adres | Uwagi |
|---|---|---|
| Frontend | http://localhost:8081 | nginx serwuje SPA i proxuje /api |
| Backend | http://localhost:8080/api | bezposrednio, z pominieciem nginx |
| Swagger UI | http://localhost:8080/api/swagger-ui.html | tylko przy SWAGGER_ENABLED=true |
| Konsola MinIO | http://localhost:9001 | 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 http://localhost:8081/w/zamowienia-publiczne-2026.
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 http://localhost:8081 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:
docker compose logs backend | grep -A6 "UTWORZONO PIERWSZE KONTO"
Logowanie:
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:
TOKEN="wklej-token"
curl http://localhost:8080/api/admin/webinars -H "Authorization: Bearer $TOKEN"
Zmiana wlasnego hasla:
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:
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
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
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
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:
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:
docker --version
docker compose version
Sklonuj repozytorium i wejdz do katalogu projektu:
git clone <adres-repozytorium> webinar-platform
cd webinar-platform
Krok 2: konfiguracja instancji
cp .env.production.example .env.production
chmod 600 .env.production
Wygeneruj sekrety i wpisz je do .env.production:
# 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:
./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
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:
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:
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.
- Zaloguj sie do panelu i wejdz w Ustawienia.
- Uzupelnij sekcje Konto e-mail (SMTP):
MAIL_HOSTserwer poczty, na przykladsmtp.domena.plMAIL_PORT465 dla SSL albo 587 dla STARTTLSMAIL_SSL_ENABLEDwlaczone dla portu 465MAIL_USERNAMEpelny adres e-mail nadawcyMAIL_PASSWORDhaslo do kontaMAIL_FROM_NAMEnazwa widoczna dla odbiorcy
- Kliknij Zapisz ustawienia.
- 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. - Sprawdz, czy wiadomosc dotarla, takze do folderu ze spamem.
- 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. 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:
# 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:
{"status":"UP","version":"0.1.0"}
Sprawdz jeszcze:
- panel otwiera sie pod
https://webinary.klient.pl/logowaniei logowanie dziala, - baza i MinIO nie sa dostepne z zewnatrz:
nc -zv webinary.klient.pl 5432oraznc -zv webinary.klient.pl 9000powinny 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
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
# 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
./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:
./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:
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_LASTdo 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:
# 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
./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:
./scripts/backup.sh
./scripts/restore.sh ./backups/20260828-143000
Po odtworzeniu sprawdz:
curl http://127.0.0.1:8081/api/health
oraz zaloguj sie do panelu i obejrzyj liste webinarow i zapisow.
Odtworzenie na nowym serwerze
- Zainstaluj Dockera, sklonuj repozytorium.
- Skopiuj
env.production.kopiaz kopii jako.env.productioni ustawchmod 600. Zachowaj ten samJWT_SECRET, inaczej wszyscy zostana wylogowani, oraz to samoDB_PASSWORD, bo odtwarzana baza go uzywa. - Uruchom uslugi:
docker compose -f docker-compose.prod.yml --env-file .env.production up -d. - Odtworz dane:
./scripts/restore.sh <katalog-kopii>. - 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.
# 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.
- 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 statusPENDING. - Blad konfiguracji jest czytelny. Wpisz zly
AUTOPAY_SERVICE_IDi sprobuj kupic: kursant ma zobaczyc komunikat o niedostepnosci platnosci, a zamowienie zostac wNEW, gotowe do ponowienia na tym samym numerze. W logu pojawi sieSERVICE_NOT_FOUND. - 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
SUCCESSz oznaczeniempodpis poprawnyiprzetworzony. - 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.
- Platnosc nieudana nie nadaje dostepu. Powtorz zakup i wybierz w bramce
scenariusz niepowodzenia. Zamowienie ma dostac
FAILED, dostepu ma nie byc. - FAILURE po SUCCESS nie odbiera dostepu. To najwazniejszy przypadek:
zaplac skutecznie, a potem doprowadz do komunikatu
FAILUREz innymremoteID. Dostep ma zostac. Dokumentacja operatora opisuje ten przypadek wprost, a jego zlamanie zabiera kurs osobie, ktora zaplacila. - 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. - 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. - 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: https://developers.autopay.pl/
- Panel srodowiska testowego: https://testpay.autopay.eu/
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.
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
docker compose down
Dane w wolumenach zostaja zachowane. Aby usunac takze dane:
docker compose down -v
Praca nad frontendem bez Dockera
Serwer deweloperski Vite z hot reloadem, przy backendzie dzialajacym w Dockerze:
cd frontend
npm install
npm run dev
Frontend startuje na http://localhost:5173. 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).