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, forwarded_to_redmine, rejected (default new)

forwarded_to_redmine je terminální stav vznikající pouze úspěšnou akcí předání. Nelze jej nastavit obecným endpointem změny stavu.

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 a odpovídající oprávnění v aplikaci auth. Čtení používá support.view, změna stavu/poznámky support.manage, smazání support.delete a předání support.redmine. Superadmin má nadále nouzový bypass. Cesta je /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)
GET/admin/support/redmine/statusBezpečný stav konfigurace bez API klíče
POST/admin/support/feedback/:id/redmineIdempotentní předání požadavku a příloh do Redmine
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

# Serverové napojení na Redmine
SUPPORT_REDMINE_URL=https://redmine.atrea.cz
SUPPORT_REDMINE_API_KEY=<secret>
SUPPORT_REDMINE_PROJECT_ID=<identifier-nebo-id>
SUPPORT_REDMINE_TRACKER_ID=<volitelné-id>

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

Redmine a CORS

Panel Redmine REST API nikdy nevolá. Po kliknutí zavolá pouze POST /api/admin/support/feedback/:id/redmine na user-api. Server nahraje přílohy do /uploads.json, vytvoří /issues.json, uloží ID a URL úkolu a nastaví stav forwarded_to_redmine. Proto není nutné povolovat CORS na Redmine a API klíč se nedostane do JavaScriptu, logů panelu ani odpovědi API.

Opakování akce po úspěchu pouze vrátí již propojený požadavek. Souběžné kliknutí chrání databázový zámek.

Notifikace správcům panelu

Seed sql/auth_panel_notifications.sql vytváří v aplikaci auth obecný typ support se šablonami new_feedback a status_changed v češtině a angličtině. E-mail i push jsou ve výchozím stavu zapnuté, userCanDisable=true, takže každý příjemce si kanály může vypnout.

Po uložení požadavku API obesílá pouze aktivní uživatele, kterým efektivní oprávnění support.view dovolí požadavek skutečně otevřít. Po změně stavu obdrží autor požadavku šablonu support/status_changed. Opakované uložení stejného stavu další notifikaci nevytvoří.


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