308 lines
14 KiB
HTML
308 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Manual API - Publiczne API v1 (webinary i kursy)</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f4f7fb;
|
|
--card: #ffffff;
|
|
--text: #122033;
|
|
--muted: #4f6277;
|
|
--border: #d7e0ea;
|
|
--accent: #0b66c3;
|
|
--ok: #1f8b4c;
|
|
--err: #b02a37;
|
|
--code-bg: #0f1b2a;
|
|
--code-text: #e5f0ff;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: "Segoe UI", Tahoma, sans-serif;
|
|
line-height: 1.55;
|
|
}
|
|
.container { max-width: 1100px; margin: 0 auto; padding: 28px 16px 48px; }
|
|
.header {
|
|
background: linear-gradient(120deg, #d9ecff 0%, #f7fbff 60%, #ffffff 100%);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 20px;
|
|
margin-bottom: 16px;
|
|
}
|
|
h1, h2, h3 { margin: 0 0 10px; }
|
|
h1 { font-size: 1.7rem; }
|
|
h2 { font-size: 1.25rem; margin-top: 20px; }
|
|
h3 { font-size: 1.05rem; margin-top: 16px; }
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 18px;
|
|
margin-top: 14px;
|
|
}
|
|
.muted { color: var(--muted); }
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 9px;
|
|
border-radius: 999px;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
border: 1px solid var(--border);
|
|
background: #fff;
|
|
margin-right: 8px;
|
|
}
|
|
.badge.get { border-color: #9ac6f2; color: #084f97; }
|
|
.badge.json { border-color: #98d7ad; color: #166838; }
|
|
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
|
th, td {
|
|
border-bottom: 1px solid var(--border);
|
|
text-align: left;
|
|
padding: 9px 7px;
|
|
vertical-align: top;
|
|
font-size: 0.95rem;
|
|
}
|
|
th { background: #f7fafe; font-weight: 600; }
|
|
code {
|
|
background: #eef4fb;
|
|
border: 1px solid #dbe8f5;
|
|
border-radius: 6px;
|
|
padding: 1px 6px;
|
|
font-family: Consolas, "Courier New", monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
pre {
|
|
margin: 10px 0 0;
|
|
background: var(--code-bg);
|
|
color: var(--code-text);
|
|
border-radius: 10px;
|
|
padding: 14px;
|
|
overflow-x: auto;
|
|
font-size: 0.9rem;
|
|
}
|
|
.ok { color: var(--ok); font-weight: 600; }
|
|
.err { color: var(--err); font-weight: 600; }
|
|
ul { margin-top: 8px; }
|
|
li { margin-bottom: 6px; }
|
|
.note {
|
|
border-left: 4px solid var(--accent);
|
|
background: #eef5fd;
|
|
padding: 10px 14px;
|
|
border-radius: 0 8px 8px 0;
|
|
margin-top: 12px;
|
|
}
|
|
@media (max-width: 700px) {
|
|
h1 { font-size: 1.35rem; }
|
|
th, td { font-size: 0.9rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<section class="header">
|
|
<h1>Manual programisty: Publiczne API v1</h1>
|
|
<p class="muted">Dokumentacja API tylko do odczytu z listą aktywnych webinarów i aktywnych kursów platformy szkoleniowej.</p>
|
|
<span class="badge get">GET</span>
|
|
<span class="badge json">application/json</span>
|
|
<span class="badge">Auth: nagłówek X-API-Key</span>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>1. Informacje ogólne</h2>
|
|
<table>
|
|
<tr><th>Adres bazowy</th><td><code>https://TWOJA-DOMENA/api/public/v1</code></td></tr>
|
|
<tr><th>Metoda HTTP</th><td><code>GET</code></td></tr>
|
|
<tr><th>Format odpowiedzi</th><td><code>application/json</code></td></tr>
|
|
<tr><th>Kodowanie</th><td>UTF-8</td></tr>
|
|
<tr><th>Wersjonowanie</th><td>Wersja jest w ścieżce (<code>/v1/</code>). Zmiany łamiące trafią do <code>/v2/</code>, a <code>/v1/</code> zostanie niezmienione.</td></tr>
|
|
</table>
|
|
<p class="muted">Adres <code>TWOJA-DOMENA</code> to domena instancji (ta sama, na której działa strona zapisu). API jest wyłącznie do odczytu: nie tworzy ani nie zmienia danych.</p>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>2. Autoryzacja</h2>
|
|
<p>Klucz API należy przesłać w nagłówku HTTP <code>X-API-Key</code>. Klucz generuje administrator w panelu (Ustawienia → Klucze API) i przekazuje go odbiorcy.</p>
|
|
<pre>X-API-Key: TWOJ_KLUCZ_API</pre>
|
|
<div class="note">
|
|
<strong>Klucz jest pokazywany jeden raz</strong> – w chwili wygenerowania. W bazie przechowywany jest wyłącznie jego skrót (SHA-256), więc klucza nie da się odczytać ponownie. Jeśli zostanie zgubiony, wygeneruj nowy i usuń stary.
|
|
</div>
|
|
<p class="muted">Można wydać wiele kluczy dla różnych odbiorców i wyłączać je niezależnie. Wyłączenie lub usunięcie klucza odbiera dostęp natychmiast.</p>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>3. Endpoint: aktywne webinary</h2>
|
|
<table>
|
|
<tr><th>Adres</th><td><code>GET /api/public/v1/webinars</code></td></tr>
|
|
<tr><th>Zwraca</th><td>Tablicę JSON aktywnych webinarów (opublikowane i jeszcze niezakończone), posortowaną według daty rozpoczęcia rosnąco.</td></tr>
|
|
</table>
|
|
|
|
<h3>Pola pozycji</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Pole</th><th>Typ</th><th>Opis</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>slug</code></td><td>string</td><td>Identyfikator w adresie strony zapisu.</td></tr>
|
|
<tr><td><code>title</code></td><td>string</td><td>Tytuł webinaru.</td></tr>
|
|
<tr><td><code>description</code></td><td>string / null</td><td>Krótki opis (zajawka). Bez znaczników HTML.</td></tr>
|
|
<tr><td><code>startsAt</code></td><td>string (ISO 8601)</td><td>Termin rozpoczęcia z przesunięciem strefy, np. <code>2026-09-17T09:00:00+02:00</code>.</td></tr>
|
|
<tr><td><code>endsAt</code></td><td>string (ISO 8601)</td><td>Termin zakończenia.</td></tr>
|
|
<tr><td><code>timezone</code></td><td>string</td><td>Strefa czasowa webinaru, np. <code>Europe/Warsaw</code>.</td></tr>
|
|
<tr><td><code>registrationOpen</code></td><td>boolean</td><td>Intencja organizatora co do otwarcia zapisów.</td></tr>
|
|
<tr><td><code>capacity</code></td><td>integer / null</td><td>Limit miejsc. <code>null</code> oznacza brak limitu.</td></tr>
|
|
<tr><td><code>registeredCount</code></td><td>integer</td><td>Liczba zajętych miejsc (bez rezygnacji).</td></tr>
|
|
<tr><td><code>registrationUrl</code></td><td>string / null</td><td>Pełny adres strony zapisu. <code>null</code>, gdy w instancji nie ustawiono publicznego adresu.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>4. Endpoint: aktywne kursy</h2>
|
|
<table>
|
|
<tr><th>Adres</th><td><code>GET /api/public/v1/courses</code></td></tr>
|
|
<tr><th>Zwraca</th><td>Tablicę JSON opublikowanych kursów, w kolejności ustalonej przez administratora.</td></tr>
|
|
</table>
|
|
|
|
<h3>Pola pozycji</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Pole</th><th>Typ</th><th>Opis</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>slug</code></td><td>string</td><td>Identyfikator w adresie strony kursu.</td></tr>
|
|
<tr><td><code>title</code></td><td>string</td><td>Tytuł kursu.</td></tr>
|
|
<tr><td><code>subtitle</code></td><td>string / null</td><td>Podtytuł.</td></tr>
|
|
<tr><td><code>description</code></td><td>string / null</td><td>Krótki opis.</td></tr>
|
|
<tr><td><code>category</code></td><td>string / null</td><td>Kategoria kursu.</td></tr>
|
|
<tr><td><code>level</code></td><td>string / null</td><td>Poziom: <code>BASIC</code>, <code>INTERMEDIATE</code> lub <code>ADVANCED</code>.</td></tr>
|
|
<tr><td><code>durationMinutes</code></td><td>integer / null</td><td>Łączny czas materiału w minutach. <code>null</code>, gdy nie podano.</td></tr>
|
|
<tr><td><code>free</code></td><td>boolean</td><td><code>true</code> dla kursu bezpłatnego.</td></tr>
|
|
<tr><td><code>priceGross</code></td><td>number / null</td><td>Cena brutto. <code>null</code> dla kursu darmowego.</td></tr>
|
|
<tr><td><code>currency</code></td><td>string</td><td>Waluta ceny, np. <code>PLN</code>.</td></tr>
|
|
<tr><td><code>enrolledCount</code></td><td>integer</td><td>Liczba kursantów (zapisy aktywne i ukończone).</td></tr>
|
|
<tr><td><code>courseUrl</code></td><td>string / null</td><td>Pełny adres strony kursu. <code>null</code>, gdy nie ustawiono publicznego adresu.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>5. Przykład zapytania (cURL)</h2>
|
|
<pre>curl -X GET "https://TWOJA-DOMENA/api/public/v1/webinars" \
|
|
-H "X-API-Key: TWOJ_KLUCZ_API"</pre>
|
|
<pre>curl -X GET "https://TWOJA-DOMENA/api/public/v1/courses" \
|
|
-H "X-API-Key: TWOJ_KLUCZ_API"</pre>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>6. Przykład zapytania (JavaScript / fetch)</h2>
|
|
<pre>const response = await fetch("https://TWOJA-DOMENA/api/public/v1/webinars", {
|
|
method: "GET",
|
|
headers: { "X-API-Key": "TWOJ_KLUCZ_API" }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("API zwrocilo status " + response.status);
|
|
}
|
|
|
|
const webinars = await response.json();
|
|
console.log(webinars.length, "aktywnych webinarow");</pre>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>7. Przykład zapytania (PHP)</h2>
|
|
<pre><?php
|
|
$ch = curl_init("https://TWOJA-DOMENA/api/public/v1/courses");
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: TWOJ_KLUCZ_API"]);
|
|
$body = curl_exec($ch);
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($status === 200) {
|
|
$courses = json_decode($body, true);
|
|
foreach ($courses as $course) {
|
|
echo $course["title"] . "\n";
|
|
}
|
|
}</pre>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>8. Odpowiedzi API</h2>
|
|
|
|
<h3 class="ok">8.1 Sukces (HTTP 200) – webinary</h3>
|
|
<pre>[
|
|
{
|
|
"slug": "audyt-sieci-20260917",
|
|
"title": "Audyt bezpieczenstwa sieci",
|
|
"description": "Jak wykryc luki, zanim zrobi to ktos inny.",
|
|
"startsAt": "2026-09-17T09:00:00+02:00",
|
|
"endsAt": "2026-09-17T10:30:00+02:00",
|
|
"timezone": "Europe/Warsaw",
|
|
"registrationOpen": true,
|
|
"capacity": 100,
|
|
"registeredCount": 42,
|
|
"registrationUrl": "https://TWOJA-DOMENA/w/audyt-sieci-20260917"
|
|
}
|
|
]</pre>
|
|
|
|
<h3 class="ok">8.2 Sukces (HTTP 200) – kursy</h3>
|
|
<pre>[
|
|
{
|
|
"slug": "podstawy-eksploatacji",
|
|
"title": "Podstawy eksploatacji sieci wodociagowej",
|
|
"subtitle": "Kurs dla poczatkujacych",
|
|
"description": "Od podstaw do samodzielnej pracy.",
|
|
"category": "Eksploatacja",
|
|
"level": "BASIC",
|
|
"durationMinutes": 180,
|
|
"free": false,
|
|
"priceGross": 490.00,
|
|
"currency": "PLN",
|
|
"enrolledCount": 128,
|
|
"courseUrl": "https://TWOJA-DOMENA/kurs/podstawy-eksploatacji"
|
|
}
|
|
]</pre>
|
|
<p class="muted">Brak pozycji zwraca pustą tablicę <code>[]</code> ze statusem 200 – to nie jest błąd.</p>
|
|
|
|
<h3 class="err">8.3 Błędy</h3>
|
|
<p class="muted">Ciało błędu ma postać <code>{ "message": "...", "fields": {} }</code>.</p>
|
|
<table>
|
|
<thead><tr><th>HTTP</th><th>Komunikat</th><th>Znaczenie</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>401</td><td><code>Nieprawidłowy lub brakujący klucz API.</code></td><td>Brak nagłówka <code>X-API-Key</code>, pusty, błędny albo wyłączony klucz. Jeden komunikat dla wszystkich tych przypadków.</td></tr>
|
|
<tr><td>404</td><td><code>Nie znaleziono zasobu pod tym adresem.</code></td><td>Zły adres endpointu.</td></tr>
|
|
<tr><td>405</td><td><code>Metoda ... nie jest obsługiwana pod tym adresem.</code></td><td>Użyto innej metody niż GET.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>9. Dobre praktyki po stronie klienta</h2>
|
|
<ul>
|
|
<li>Wysyłaj żądania wyłącznie przez HTTPS.</li>
|
|
<li>Trzymaj klucz po stronie serwera. Nie umieszczaj go w kodzie strony ani w aplikacji przeglądarkowej, gdzie każdy go odczyta.</li>
|
|
<li>Maskuj <code>X-API-Key</code> w logach aplikacyjnych.</li>
|
|
<li>Buforuj odpowiedzi po swojej stronie (np. 1–5 minut). Dane zmieniają się rzadko, a odpytywanie co sekundę jest zbędne.</li>
|
|
<li>Przy statusie 401 nie ponawiaj automatycznie – sprawdź, czy klucz nie został wyłączony lub usunięty.</li>
|
|
<li>Przy błędach 5xx zastosuj ponawianie z limitem prób i narastającym odstępem (backoff).</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>10. Checklista testów integracyjnych</h2>
|
|
<ul>
|
|
<li>Poprawne żądanie z ważnym kluczem zwraca HTTP 200 i tablicę JSON.</li>
|
|
<li>Brak nagłówka <code>X-API-Key</code> zwraca HTTP 401.</li>
|
|
<li>Błędny klucz zwraca HTTP 401.</li>
|
|
<li>Wyłączenie klucza w panelu natychmiast skutkuje HTTP 401.</li>
|
|
<li>Żądanie metodą POST pod adres endpointu zwraca HTTP 405.</li>
|
|
<li>Instancja bez aktywnych pozycji zwraca pustą tablicę <code>[]</code> ze statusem 200.</li>
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
</body>
|
|
</html>
|