Tmavý režim
Support feedback
Vestavěný systém pro sběr zpětné vazby z koncových aplikací (bug reporty, nápady, dotazy). Uživatel vyplní formulář v aplikaci, optionálně přiloží screenshoty, a backend je centrálně doručí super-adminům do administrace.
Jak to funguje
┌──────────────┐ POST /support/feedback ┌─────────────┐
│ End-user app │ ───── multipart + JWT ─────────▶ │ user-api │
└──────────────┘ │ │
│ ┌───────┐ │
┌──────────────┐ GET /admin/support/feedback │ │ PG │ │
│ Admin panel │ ◀──── JSON + signed URLs ─────── │ └───────┘ │
└──────────────┘ └─────────────┘
│ ▲
│ <img src="...attachments/12?exp=...&sig=..."> │
└────────────────────────────────────────────────┘
bez Authorization headeruDatový model
Dvě tabulky:
| Tabulka | Účel |
|---|---|
support_feedback | Hlavička ticketu (text, status, autor, app metadata) |
support_attachment | Přílohy jako BYTEA v PostgreSQL (TOAST) — žádné soubory na disku |
ON DELETE CASCADE zajistí, že smazání ticketu odstraní i jeho přílohy.
Stavy a typy
| Pole | Hodnoty |
|---|---|
type | bug, idea, question, other |
status | new, in_progress, resolved, rejected (default new) |
Signed URL pro přílohy
Endpoint /admin/support/attachments/:id je namountovaný před JWT middleware. Autorizace probíhá přes HMAC-SHA256 podpis v query stringu (?exp=...&sig=...), aby admin panel mohl vkládat obrázky přímo do <img src> bez bearer headeru. TTL je 10 minut, sekret je SUPPORT_SIGNED_URL_SECRET.
Auto-cleanup (rolling cap)
Aby tabulky nerostly do nekonečna a BYTEA přílohy nezatěžovaly server, drží se maximálně 500 ticketů na jeden appCode. Po každém vložení nového ticketu se přebytečné nejstarší řádky odstraní (CASCADE smaže i jejich přílohy). Limit je v src/services/supportService.ts jako konstanta MAX_FEEDBACKS_PER_APP.
Cleanup je per aplikace, ne globální — každý
appCode(např.kgb) má svůj nezávislý limit 500.
Integrace z nové aplikace
Požadavky
- Aplikace má vlastní
appCoderegistrovaný v tabulceapplications(řeší admin panel). - Uživatel je přihlášený přes Zitadel (frontend posílá platný
Beareraccess token). - Frontend posílá
multipart/form-data— žádný JSON.
Endpoint
http
POST /api/support/feedback
Authorization: Bearer <jwt>
Content-Type: multipart/form-data| Pole | Typ | Povinné | Validace |
|---|---|---|---|
appCode | string | ✓ | Musí existovat v applications |
appVersion | string | ✓ | 1..64 znaků (verze frontend buildu) |
type | string | ✓ | bug | idea | question | other |
message | string | ✓ | 1..5000 znaků (po trim) |
attachments | file[] | ✗ | max 5 souborů, 10 MB každý, 60 MB total |
Povolené MIME typy příloh: image/png, image/jpeg, image/gif, image/webp, application/pdf.
E-mail a jméno autora se berou automaticky z JWT — nikdy je neposílej v body, server je ignoruje.
Odpověď
http
201 Created
{
"code": 201,
"response": {
"id": 42,
"createdAt": "2026-06-09T12:34:56.789Z"
}
}Chybové stavy
| Status | Příčina |
|---|---|
| 400 | Neznámý appCode, vadný type/status, příliš dlouhý text, nepovolený MIME |
| 401 | Chybí / vadný JWT |
| 413 | Multipart payload přes 60 MB |
Příklad — React/TypeScript (fetch)
typescript
async function reportBug(opts: {
message: string;
files: File[];
token: string;
}) {
const fd = new FormData();
fd.append('appCode', 'kgb');
fd.append('appVersion', __APP_VERSION__); // z build configu
fd.append('type', 'bug');
fd.append('message', opts.message);
for (const f of opts.files) fd.append('attachments', f);
const res = await fetch('https://api.user.kagb.cloud/api/support/feedback', {
method: 'POST',
headers: { Authorization: `Bearer ${opts.token}` },
body: fd,
});
if (!res.ok) throw new Error(`Support submit failed: ${res.status}`);
return res.json() as Promise<{ code: number; response: { id: number; createdAt: string } }>;
}Příklad — curl
bash
curl -X POST https://api.user.kagb.cloud/api/support/feedback \
-H "Authorization: Bearer $TOKEN" \
-F "appCode=kgb" \
-F "appVersion=2.3.1" \
-F "type=bug" \
-F "message=Aplikace se zasekne při otevření detailu jednotky." \
-F "attachments=@screenshot.png"Admin endpointy
Všechny vyžadují JWT super-admina (requireSuperAdmin). Cesta /api/admin/support/*.
| Metoda | Cesta | Účel |
|---|---|---|
GET | /admin/support/feedback | List s filtry (appCode, version, userEmail, status, type, limit, offset) |
GET | /admin/support/feedback/:id | Detail jednoho ticketu |
PATCH | /admin/support/feedback/:id/status | Změna stavu ({ "status": "in_progress" }) |
PATCH | /admin/support/feedback/:id/note | Interní poznámka ({ "internalNote": "..." }, "" = vyprázdnit) |
DELETE | /admin/support/feedback/:id | Smazání ticketu i příloh (204) |
GET | /admin/support/attachments/:id?exp=...&sig=... | Streamuje přílohu (signed URL, bez JWT) |
URL příloh dostává admin panel už podepsané v každé odpovědi (attachments[].url), takže není potřeba je generovat ručně.
Konfigurace
V .env.production:
bash
# HMAC sekret pro signed URL. Vygeneruj: openssl rand -hex 32
SUPPORT_SIGNED_URL_SECRET=<32+ chars hex>
# Veřejná URL backendu, jak ji vidí browser admin panelu (+ /api)
SUPPORT_PUBLIC_BASE_URL=https://api.user.kagb.cloud/apiV YAML konfiguraci jsou hodnoty prázdné — env mapping v config/custom-environment-variables.yaml je přepíše za běhu.
Limity a tuning
| Co | Default | Kde |
|---|---|---|
| Max příloh na ticket | 5 | MAX_ATTACHMENTS v supportService.ts |
| Max velikost jednoho souboru | 10 MB | MAX_FILE_BYTES |
| Max velikost multipart payloadu | 60 MB | MAX_TOTAL_BYTES v supportRouter.ts |
Max délka message | 5000 znaků | MESSAGE_MAX |
Max délka internalNote | 5000 znaků | INTERNAL_NOTE_MAX |
| Rolling cap ticketů per app | 500 | MAX_FEEDBACKS_PER_APP |
| Signed URL TTL | 600 s | TTL_SECONDS v supportStorage.ts |
Pokud bys chtěl jiný cap (např. 200 pro mobilní apky, 1000 pro web), uprav konstantu a deployni — žádná migrace.