Initial commit
This commit is contained in:
@@ -0,0 +1,928 @@
|
||||
# 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 <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:
|
||||
|
||||
```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 <adres-repozytorium> 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 <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.
|
||||
|
||||
```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: <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.
|
||||
|
||||
```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 <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`).
|
||||
Reference in New Issue
Block a user