Skip to content

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 headeru

Datový model

Dvě tabulky:

TabulkaÚčel
support_feedbackHlavička ticketu (text, status, autor, app metadata)
support_attachmentPří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

PoleHodnoty
typebug, idea, question, other
statusnew, 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

  1. Aplikace má vlastní appCode registrovaný v tabulce applications (řeší admin panel).
  2. Uživatel je přihlášený přes Zitadel (frontend posílá platný Bearer access token).
  3. Frontend posílá multipart/form-data — žádný JSON.

Endpoint

http
POST /api/support/feedback
Authorization: Bearer <jwt>
Content-Type: multipart/form-data
PoleTypPovinnéValidace
appCodestringMusí existovat v applications
appVersionstring1..64 znaků (verze frontend buildu)
typestringbug | idea | question | other
messagestring1..5000 znaků (po trim)
attachmentsfile[]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

StatusPříčina
400Neznámý appCode, vadný type/status, příliš dlouhý text, nepovolený MIME
401Chybí / vadný JWT
413Multipart 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/*.

MetodaCestaÚčel
GET/admin/support/feedbackList s filtry (appCode, version, userEmail, status, type, limit, offset)
GET/admin/support/feedback/:idDetail jednoho ticketu
PATCH/admin/support/feedback/:id/statusZměna stavu ({ "status": "in_progress" })
PATCH/admin/support/feedback/:id/noteInterní poznámka ({ "internalNote": "..." }, "" = vyprázdnit)
DELETE/admin/support/feedback/:idSmazá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/api

V YAML konfiguraci jsou hodnoty prázdné — env mapping v config/custom-environment-variables.yaml je přepíše za běhu.


Limity a tuning

CoDefaultKde
Max příloh na ticket5MAX_ATTACHMENTS v supportService.ts
Max velikost jednoho souboru10 MBMAX_FILE_BYTES
Max velikost multipart payloadu60 MBMAX_TOTAL_BYTES v supportRouter.ts
Max délka message5000 znakůMESSAGE_MAX
Max délka internalNote5000 znakůINTERNAL_NOTE_MAX
Rolling cap ticketů per app500MAX_FEEDBACKS_PER_APP
Signed URL TTL600 sTTL_SECONDS v supportStorage.ts

Pokud bys chtěl jiný cap (např. 200 pro mobilní apky, 1000 pro web), uprav konstantu a deployni — žádná migrace.

Atrea User API — interní dokumentace