Strapi backend
Strapi v5 Backend
Section titled “Strapi v5 Backend”Strapi je headless CMS a REST API backend pro Sencai. Spravuje veškerá platformní data: uživatele, organizace, instance, registrace a další.
Klíčové údaje
Section titled “Klíčové údaje”- Verze: v5
- Port: 1337 (dev), routováno přes Traefik
- Admin: http://localhost:1337/admin
- Databáze: MySQL (backend-db:3306)
- Tech: Node.js 20+, TypeScript, Knex (migrace)
Content-types
Section titled “Content-types”Upozornění: Strapi aktuálně definuje 212 content-types (stav Fáze 3, viz
src/api/vsencai.space). Seznam níže je malý ilustrativní výběr pokrývající hlavní domény — není vyčerpávající. Před spoléháním na konkrétní název nebo tvar content-type vždy zkontrolujtesrc/api/jako autoritativní aktuální zdroj.
Jádro platformy
Section titled “Jádro platformy”- User — Synchronizováno s Keycloakem, profilová pole (name, surname, email, jobTitle, adresa)
- Registration — Registrační požadavky, čekající na ověření emailu
- Organisation — Workspace pro týmy
- Organisation-Member (pivot) — Propojuje uživatele s organizacemi přes RBAC role (owner, admin, member, viewer)
Cloudová infrastruktura
Section titled “Cloudová infrastruktura”- CloudInstance — Provisionované VM/kontejnery u cloud providerů
- CloudProvider — Přihlašovací údaje a konfigurace cloud providera
- CustomRegistry — Docker registry (privátní nebo veřejné)
Tool Center
Section titled “Tool Center”- DockerHubCache — Cache oblíbených Docker images
- GitTemplate — Předpřipravené Git šablony pro rychlý setup
Integrace
Section titled “Integrace”- Invitation — Pozvánky do organizace s tokenovým přijetím
- ApiToken — Uživatelské API tokeny pro programový přístup
Billing (Lago/Stripe, ADR-002)
Section titled “Billing (Lago/Stripe, ADR-002)”- Billing, Billing-Event — Záznamy usage/subscription billingu a event log
- Billing-Portal — Stav zákaznického billing portálu
- Organisation-Billing — Billing konfigurace/tarif per organizace
Fleet / Agent
Section titled “Fleet / Agent”- Agent-Config, Agent-Policy, Agent-Release — Konfigurace, politika a release tracking on-host fleet agenta (
sencai-agent) - Agent-Metric-Snapshot — Snímky fleet telemetrie
- Fleet-Cohort — Seskupení fleet agentů pro rollout/policy targeting
- K8s-Agent — Registrace/stav Kubernetes fleet agenta (
k8s-agent)
- Scim, Scim-Token — SCIM provisioning záznamy a bearer tokeny
LLM Usage
Section titled “LLM Usage”- Llm-Token-Usage, Llm-Usage — Usage/token metering záznamy LLM fasády (
llm-service)
Onboarding / Feedback
Section titled “Onboarding / Feedback”- Onboarding-State — Průběh onboarding wizardu per organizace/uživatel
- Feedback, Nps-Response — In-app feedback a odpovědi na NPS průzkum
Každý content-type má:
- REST endpointy:
GET /api/<plural>,POST /api/<plural>,PUT /api/<plural>/:id,DELETE /api/<plural>/:id - Filtrování a stránkování:
?filters[field][$eq]=value&populate=*&sort=-updatedAt&pagination[pageSize]=10 - Politiky: RBAC přes
src/policies/(např.is-organisation-member) — celkový přehled viz Policies (RBAC) níže - Webhooky: Spouštěné při create/update/delete pro event streaming
Lifecycle hooky
Section titled “Lifecycle hooky”Strapi spouští hooky v klíčových fázích životního cyklu. Sencai je používá pro cross-service synchronizaci:
Synchronizace uživatelů (KC ↔ Strapi)
Section titled “Synchronizace uživatelů (KC ↔ Strapi)”Soubor: src/lifecycles/user-kc-sync.ts
Při vytvoření nebo aktualizaci Strapi Usera:
plugin::users-permissions.user afterCreate/afterUpdate → runWithKcSync() ověří AsyncLocalStorage flag → Pokud není potlačeno: POST webhook-publisher/webhook-publisher → Event: { eventName: 'user.update', entity: user, ... } → RabbitMQ user.events exchange → auth-service-consumer zpracuje → KC admin API aktualizuje atributy uživateleOchrana proti smyčce: Používá AsyncLocalStorage k prevenci nekonečných smyček:
runWithoutKcSync()— Deaktivuje KC sync pro daný async kontext- Používá se při KC → Strapi synchronizaci, aby se zabránilo zpětnému promítnutí do KC
Synchronizovaná pole
Section titled “Synchronizovaná pole”Při aktualizaci Strapi Usera se tato pole synchronizují do Keycloaku:
email↔ KC emailusername↔ KC sub (UUID, read-only)confirmed/emailVerified↔ KC emailVerifiedblocked/enabled↔ KC enabled- Profil:
name,surname,jobTitle,street,city,zip_code,country
JWT & Keycloak integrace
Section titled “JWT & Keycloak integrace”Strapi validuje JWT tokeny z Keycloaku pomocí jwks-rsa:
Soubor: src/middlewares/keycloak-jwt.ts
// Validuje JWT podpis přes Keycloak JWKS endpoint// Dekóduje uživatelské údaje (sub, email, name, atd.)// Pokud uživatel v Strapi neexistuje nebo je profil neúplný:// → Synchronizovat z KC před povolením přístupu// → Zavolat runWithoutKcSync, aby se předešlo smyčceObsah JWT: Keycloak vydává RS256-signované tokeny s:
sub— UUID uživatele (stává se Strapiusername)email,name,family_name— Profilová poleexp— 12hodinová expiraceiat,iss,aud— Standardní claims
Příklady REST API
Section titled “Příklady REST API”Získat profil uživatele
Section titled “Získat profil uživatele”curl -H "Authorization: Bearer <JWT>" http://localhost:1337/api/users/meVytvořit organizaci
Section titled “Vytvořit organizaci”curl -X POST http://localhost:1337/api/organisations \ -H "Authorization: Bearer <JWT>" \ -H "Content-Type: application/json" \ -d '{"name":"Acme Corp","description":"AI team"}'Výpis cloud instancí (s filtrem)
Section titled “Výpis cloud instancí (s filtrem)”curl http://localhost:1337/api/cloud-instances?populate=* \ -H "Authorization: Bearer <JWT>"Stránkování a řazení
Section titled “Stránkování a řazení”curl "http://localhost:1337/api/organisations?pagination[page]=2&pagination[pageSize]=10&sort=-createdAt"Webhooky
Section titled “Webhooky”Strapi umí posílat webhooky externím službám. Sencai používá Webhook Publisher jako příjemce:
Konfigurace webhooků
Section titled “Konfigurace webhooků”-
Jít do Strapi Admin → Settings → Webhooks
-
Vytvořit nový webhook:
- URL:
http://webhook-publisher:1347/webhook-publisher - Events: Zaškrtnout
Entry Create,Entry Update,Entry Deletepro cílové content-types - Headers: Volitelné auth hlavičky
- URL:
-
Uložit
Payload webhooku
Section titled “Payload webhooku”{ "event": "entry.create", "createdAt": "2024-01-01T12:00:00Z", "model": { "uid": "plugin::users-permissions.user" }, "entry": { "id": 1, "email": "user@example.com", "username": "user", "name": "John", ... }}Policies (RBAC)
Section titled “Policies (RBAC)”Sencai vynucuje politiky přes Strapi policy systém. src/policies/ obsahuje sadu znovupoužitelných, globálních politik — is-organisation-member (ukázka níže) je jen jedna z nich, ne jediný mechanismus. Celá sada pro org/tenant scoping zahrnuje:
is-organisation-member— Základní kontrola členství (jakákoliv role) pro danou organizaciis-organisation-role— Kontrola členství s minimální rolí v hierarchii (owner (4) > admin (3) > member (2) > viewer (1))is-organisation-scoped— Parametrizovaná, znovupoužitelná izolační politika (config.minRole, výchozíviewer) s bypassem pro platform-admin; organizaci pro routy s record-id (:id/:documentId) odvozuje ze skutečné relaceorganisationdaného záznamu, ne z klientem dodaných query filtrůis-organisation-scoped-or-cross-tenant— Varianta, která navíc povoluje přístup přes schválený cross-tenantorganisation-relationship(MSP/agenturní model)is-org-auditor/is-org-or-admin-auditor— Politiky s read-scope pro přístup k audit-logu/auditor roli v rámci organizace (nebo platform admin)
Soubor: src/policies/is-organisation-member.ts
module.exports = async (policyContext, config, { strapi }) => { const { auth } = policyContext.state; const { organisationId } = policyContext.params;
// Ověřit, že uživatel je členem organizace const member = await strapi.entityService.findMany( 'api::organisation-member.organisation-member', { filters: { user: auth.user.id, organisation: organisationId } } );
if (!member) throw new Error('Not a member of this organisation');};Aplikace na routy:
{ method: 'GET', path: '/cloud-instances/:id', handler: 'cloud-instance.findOne', config: { policies: ['is-organisation-member'] }}Org-scoping není vyřešený problém — berte to jako defense-in-depth
Section titled “Org-scoping není vyřešený problém — berte to jako defense-in-depth”is-organisation-scoped dříve mělo bypass IDOR třídy: útočník mohl odvodit organizaci z jím kontrolované hodnoty query.filters[organisation], zatímco operoval nad záznamem identifikovaným jiným, obětí vlastněným :id/:documentId v cestě. Šlo o jeden z nálezů remediovaných během bezpečnostních prací Fáze 3 (F3.SECREM, evidováno jako S0-1 v SECURITY-AUDIT.md).
Opravená politika nyní odvozuje organizaci pro routy s record-id výhradně z vlastní relace organisation cílového záznamu (nikdy z klientem dodaných query filtrů) a při nemožnosti organizaci odvodit selhává uzavřeně (přístup zamítne). Nicméně:
- Pro collection routy (
find/createbez path:id) není politika sama autoritativním zdrojem pravdy pro to, které řádky se vrátí — samotný controller musí query stále scopovat na straně serveru (načíst ID organizací volajícího a aplikovat explicitníwherefiltr). - Některé controllery content-types implementují vlastní autoritativní izolaci nad rámec (nebo místo) sdílené politiky; v takových případech je
is-organisation-scopedjen druhou vrstvou obrany, ne jediným strážcem.
Nepředpokládejte, že org-scoping je plně a obecně vyřešen pouhým připojením politiky k routě — vždy ověřte konkrétní chování scopování query v daném controlleru, zejména u collection endpointů.
Databázové migrace
Section titled “Databázové migrace”Strapi v5 používá Knex pro migrace.
Soubory: database/migrations/
Vytvoření migrace
Section titled “Vytvoření migrace”cd sencai.spacenpm run knex -- migrate:make add_new_fieldStruktura migrace
Section titled “Struktura migrace”exports.up = async (knex) => { await knex.schema.createTable('users_permissions_users', (table) => { table.increments('id'); table.string('email').notNullable().unique(); table.timestamps(); });};
exports.down = async (knex) => { await knex.schema.dropTable('users_permissions_users');};Důležité: Nikdy neupravujte ani nemažte existující migrace. Vždy vytvořte nové.
Spuštění migrací
Section titled “Spuštění migrací”Migrace se spouští automaticky při startu služby. Ruční spuštění:
npm run knex -- migrate:latestControllery & Services
Section titled “Controllery & Services”Vzor controlleru
Section titled “Vzor controlleru”Soubor: src/api/organisation/controllers/organisation.ts
import { factories } from '@strapi/strapi';
export default factories.createCoreController( 'api::organisation.organisation', ({ strapi }) => ({ // Přepsat nebo rozšířit výchozí akce async findOne(ctx) { // Vlastní logika zde const { id } = ctx.params; const entity = await strapi.entityService.findOne('api::organisation.organisation', id); return entity; } }));Vzor service
Section titled “Vzor service”Soubor: src/api/organisation/services/organisation.ts
import { factories } from '@strapi/strapi';
export default factories.createCoreService( 'api::organisation.organisation', ({ strapi }) => ({ // Znovupoužitelná business logika async createWithDefaults(data) { return strapi.entityService.create('api::organisation.organisation', { data: { ...data, status: 'active' } }); } }));Admin encryption
Section titled “Admin encryption”Citlivá pole (hesla, tokeny) jsou šifrována pomocí ADMIN_ENCRYPTION_KEY:
// Ve schématu content-type{ type: 'password', encrypted: true // Používá ADMIN_ENCRYPTION_KEY pro uložení}Běžné operace
Section titled “Běžné operace”Kontrola stavu služby
Section titled “Kontrola stavu služby”curl http://localhost:1337/api/healthPřístup do admin panelu
Section titled “Přístup do admin panelu”- http://localhost:1337/admin
- Vytvořit admin uživatele při prvním přihlášení
- Definovat content-types, spravovat data, konfigurovat webhooky
Zobrazení logů
Section titled “Zobrazení logů”docker logs -f backend# Nebo při lokálním běhu:npm run developPřístup do databáze
Section titled “Přístup do databáze”# MySQL (Strapi databáze)mysql -h 127.0.0.1 -u strapi -p sencai_db
# Výpis uživatelůSELECT id, email, username, confirmed FROM up_users;
# Kontrola organizacíSELECT id, name, description FROM organisations;Troubleshooting
Section titled “Troubleshooting”Q: Chyba “Model webhook not found”
A: Strapi v5 odstranilo model webhook. Webhooky musí být vytvořeny přes admin UI (Settings → Webhooks), ne přes kód.
Q: Migrace se nespouští
A: Ujistěte se, že je databáze dostupná. Zkontrolujte logy MySQL kontejneru: docker logs backend-db
Q: CORS chyby při requestech z frontendu
A: Strapi CORS je nakonfigurován v config/middlewares.ts. Přidejte URL frontendu do allowedOrigins.
Q: JWT validace selhává
A: Ověřte Keycloak URL v .env: KEYCLOAK_URL=http://keycloak:8080/kc (interní, s prefixem /kc).
Q: Uživatel se nesynchronizuje do Keycloaku
A: Zkontrolujte, že běží webhook-publisher a je dostupné RabbitMQ. Chyby zpracování eventů zjistíte v logách RabbitMQ.