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
appCodestring✓Musí existovat v applications
appVersionstring✓1..64 znaků (verze frontend buildu)
typestring✓bug | idea | question | other
messagestring✓1..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 samostatné typy support_new_feedback a support se šablonami new_feedback a status_changed v češtině a angličtině. Před seedem na existující databázi spusťte sql/migrations/2026-10-07_support_new_feedback_opt_in.sql, která zachová upravené překlady dosavadní šablony.

Nový požadavek se ve výchozím stavu nikomu neoznamuje. E-mail nebo push si může zapnout jen aktivní uživatel s rolí a přístupem do administračního panelu. Po uložení požadavku API obesílá pouze takto přihlášené uživatele. E-mail obsahuje odkazy do zdrojové aplikace a přímo na požadavek v panelu. URL aplikace se vybírá z registrovaných webových instancí podle HTTP Origin; pokud hlavička chybí nebo neodpovídá, použije se první registrovaná webová instance. URL panelu určuje support.panelBaseUrl (SUPPORT_PANEL_BASE_URL). Bez registrované webové instance se nové upozornění neodešle a API zapíše varování do logu. 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