debian 7b4d367a06
Deploy PROD / deploy (push) Successful in 25s
regulamin i polityka na stronie głównej
2026-09-02 10:56:41 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 08:21:27 +02:00
2026-09-02 09:39:34 +02:00
2026-09-02 08:21:27 +02:00

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.

  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. 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/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

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_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:

# 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

  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 <katalog-kopii>.
  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.

# 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:

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).

Test automatycznego deployu.

S
Description
Aplikacja do prowadzenia webainarów oraz szkoleń - akademiawodociagowca.pl
Readme 6.2 MiB
Languages
Java 60.5%
TypeScript 36.8%
HTML 1.9%
Shell 0.6%
JavaScript 0.1%