Přeskočit na obsah

Vývoj API

Sencai backend je postavená na Strapi v5. Tento průvodce vás naučí, jak přidávat nové API endpointy a content-types.

sencai.space/
├── src/
│ ├── api/ # Content-types a logika
│ │ ├── [content-type]/
│ │ │ ├── controllers/ # Obslužné programy
│ │ │ ├── services/ # Business logic
│ │ │ └── routes/ # Route definice
│ │ └── middleware/ # Globální middleware
│ ├── policies/ # Globální RBAC policies (prefix global::)
│ ├── lifecycles/ # Lifecycle hooks (user-kc-sync.ts)
│ └── config/ # Konfigurace
├── database/
│ └── migrations/ # Databázové migrace
└── types/ # TypeScript typy
  1. Přihlaste se do Strapi Admin: http://localhost:1337/admin

  2. V levém menu klikněte na Content-Type Builder

  3. Klikněte na Create new collection type

  4. Vyplňte údaje:

    • Display name — název typu (např. “Organisation”)
    • API ID — identifikátor (auto-generated, např. “organisation”)
    • Visible in API — zapnuto
  5. Klikněte na Continue

  6. Přidejte pole:

    • Klikněte na Add another field pro každé pole
    • Vyberte typ (Text, Email, Rich text, Relation, atd.)
    • Nakonfigurujte (povinné, default hodnota, atd.)
  7. Klikněte na Save

  8. Restartujte Strapi (změny se uloží do database/migrations/)

Controllers obsahují obslužný kód pro API endpointy.

src/api/organisations/controllers/organisations.ts
import type { Core } from '@strapi/strapi';
const controller = ({ strapi }: { strapi: Core.Strapi }) => ({
// Standard CRUD jsou auto-generované, tady vlastní akce
async getMembers(ctx) {
const { id } = ctx.params;
// Validace
if (!id) {
return ctx.badRequest('ID je povinné');
}
try {
const org = await strapi.entityService.findOne(
'api::organisation.organisation',
id,
{
populate: ['members'],
}
);
ctx.send({
members: org.members || [],
});
} catch (error) {
ctx.internalServerError('Chyba při načítání členů');
}
},
});
export default controller;

Příklad výše používá legacy strapi.entityService API, které ve Strapi v5 stále funguje. Novější kód by měl preferovat Document Service API (strapi.documents(uid)) — nativní způsob čtení/zápisu obsahu ve Strapi v5:

const org = await strapi.documents('api::organisation.organisation').findOne({
documentId,
populate: ['members'],
});

Pozor na rozdíl v identifikátoru: entityService bere numerické id, zatímco Document Service API bere string documentId (stabilní, nerostoucí identifikátor Strapi v5). Route parametry jsou v tomto repozitáři zpravidla documentId string — viz RBAC policies níže, které řeší params.id || params.documentId právě z tohoto důvodu.

src/api/organisations/routes/organisations.ts
export default {
routes: [
{
method: 'GET',
path: '/organisations/:id/members',
handler: 'organisations.getMembers',
config: {
policies: ['global::require-auth', 'global::is-organisation-member'],
auth: false,
},
},
],
};

Skutečné RBAC policies žijí jako globální policies v src/policies/ (ne per-content-type) a odkazují se s prefixem global::. Hlavní z nich:

SouborÚčelConfig
is-organisation-member.tsUživatel má jakékoliv členství (libovolnou roli) v organizacižádný
is-organisation-role.tsRole uživatele dosahuje minimálního prahu{ minRole: 'owner' | 'admin' | 'member' | 'viewer' }
is-organisation-scoped.tsRozřeší a vynutí organization scope na požadavku
is-organisation-scoped-or-cross-tenant.tsTotéž, ale povoluje i cross-tenant přístup odvozený z relationship (MSP/agentura)

Hierarchie rolí (od nejvyšší): owner (4) > admin (3) > member (2) > viewer (1).

Aplikace policies v routách:

{
config: {
policies: [
'global::require-auth',
{ name: 'global::is-organisation-role', config: { minRole: 'admin' } },
],
},
}

Nebo pro prostou kontrolu členství:

policies: ['global::require-auth', 'global::is-organisation-member'],

Hooks se spouští při CRUD akcích.

// Automaticky se spustí v sencai.space/src/lifecycles/user-kc-sync.ts
// při jakékoliv změně User entity
export default {
definition: {
kind: 'collectionType',
collectionName: 'users',
},
async afterCreate(event) {
const { result } = event;
// Sync do Keycloaku
await syncUserToKeycloak(result);
},
};

Strapi webhooky jsou integrovány s RabbitMQ přes Webhook Publisher.

// Když vytvoříte nový Instance, Webhook Publisher jej zachytí
// a pošle do RabbitMQ jako cloud.events.instance.create
// auth-service-consumer jej poslouchá
// a může reagovat asynchronně

Validujte vstupy v controlleru:

async createOrganisation(ctx) {
const { name, description } = ctx.request.body;
// Validace
if (!name || name.trim().length === 0) {
return ctx.badRequest('Název je povinný');
}
if (name.length > 100) {
return ctx.badRequest('Název je příliš dlouhý (max 100 znaků)');
}
const org = await strapi.entityService.create(
'api::organisation.organisation',
{
data: { name, description },
}
);
ctx.created(org);
}

Všechny request obsahují JWT token z Keycloaku:

// Kontrola uživatele v controlleru
async getOrganisations(ctx) {
const user = ctx.state.user;
if (!user) {
return ctx.unauthorized('Přihlášení je povinné');
}
// `user` obsahuje KC subjekt, email, atd.
console.log(user.email, user.sub); // KC UUID
}

Při prvním přihlášení se uživatel sync z Keycloaku:

src/lifecycles/user-kc-sync.ts
// Loop guard: runWithoutKcSync() zabraňuje cyklům
export async function syncKcUserToStrapi(kcUser) {
return runWithoutKcSync(async () => {
// Strapi User update se NEPOSÍLÁ zpět do Keycloaku
await strapi.entityService.update('plugin::users-permissions.user', userId, {
data: { email: kcUser.email, name: kcUser.firstName },
});
});
}

Každý mutující endpoint na platformě má produkovat audit stopu. Před psaním mutace si projděte .claude/context/audit-patterns.md, kde je kompletní referenční vzor.

HTTP mutace jsou zachyceny automaticky. Globální middleware global::audit-logger (src/middlewares/audit-logger.ts) zachytává každý POST/PUT/PATCH/DELETE response se statusem < 400 a asynchronně publikuje audit event — ve standardním Strapi controlleru není potřeba volat nic explicitně.

Explicitní volání jsou potřeba v non-HTTP cestách — cron joby, background workery a lifecycle hooky, které mutují data mimo HTTP request. V takových případech volejte publishAuditEvent() (nebo ekvivalentní audit.log() helper v non-Strapi službách) přímo:

import { publishAuditEvent, AuditAction } from '@sencai/audit';
await publishAuditEvent({
actor_user_id: `system:${serviceName}`,
organisation_id: orgId,
action: AuditAction.CLOUD_INSTANCE_PROVISIONED,
resource_type: 'cloud-instance',
resource_id: String(entity.id),
changes: { before: null, after: sanitizeForAudit(entity) },
risk_level: 'high',
});

Vynucení lintem. ESLint pravidlo sencai/require-audit-log (v eslint-plugin-sencai) tvrdě shodí npm run lint na jakékoliv mutující funkci (matchované podle jména: create, update, delete, revoke, …), která nemá audit volání. Pokud je funkce jen tenký wrapper nad již-auditovaným voláním o úroveň níž (domain boundary), místo přidání redundantního audit volání ji anotujte:

/** @no-audit { reason: "low-level HTTP wrapper; audit emitted at domain boundary" } */

sencai.space už má plně nastavené testování — Jest v29 + ts-jest, s 89 existujícími testovacími soubory. Není potřeba instalovat Jest znovu od nuly.

Terminál
cd sencai.space
npm test # spustí všechny testy jednou
npm run test:watch # watch mode
npm run test:coverage # vygeneruje coverage report

Znovu použijte existující helpery místo psaní mocků od nuly:

HelperÚčel
tests/helpers/auth.helper.tsVytvoření JWT, mock KC odpovědi
tests/helpers/strapi.helper.tsMock Strapi kontext, db.query, lifecycle mocky
tests/helpers/fixtures.helper.tsFixture factories pro User, organisation, member

Konfigurace je v jest.config.ts (ts-jest preset, SQLite-style in-memory setup přes tests/setup.ts, sekvenční běh s maxWorkers: 1 kvůli stabilitě DB).

API Manager má vestavené rate-limiting:

# 100 requests per 15 minutes per IP
GET /api/organisations
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1234567890
try {
const org = await strapi.entityService.findOne(...);
ctx.send(org);
} catch (error) {
if (error.message.includes('not found')) {
ctx.notFound('Organizace nenalezena');
} else {
ctx.internalServerError('Interní chyba serveru');
}
}
  • Validujte vstup — nikdy nedůvěřujte user datům
  • Logujte důležité akce — přes audit pattern výše
  • Používejte policies — pro RBAC
  • Vyhnete se N+1 queryům — populujte relace v entityService.findOne()
  • Testujte — minimálně CRUD operace, s použitím existujících helperů
  • Dokumentujte — přidejte JSDoc pro custom akce

Tato stránka není vyčerpávající index — příklady níže pokrývají nejčastěji integrované endpointy. Kompletní, generovanou referenci všech operací /api/v1/* (všech ~213 Strapi content-types plus ručně zdokumentovaných custom routes, včetně request/response schémat) najdete v plné API Reference (zatím jen v angličtině — generátor zatím nepodporuje lokalizaci), generované z sencai.space/openapi/sencai-platform.public.v1.yaml (F4.DEVPORTAL.01/.03). Nejdřív si přečtěte Verzování API — pokrývá jmenný prostor /api/v1/ a deprecation politiku, se kterou reference počítá.

Stavíte externí integraci v TypeScriptu, Pythonu nebo Go místo přímého volání REST API? Podívejte se na generované návody SDK — Začínáme (F4.DEVPORTAL.02/.04) — instalace, inicializace klienta, kompletní příklad list + create a konvence zpracování chyb per jazyk.

Všechny API požadavky vyžadují JWT token v hlavičce Authorization:

Authorization: Bearer YOUR_JWT_TOKEN
  • GET /api/users — Seznam všech uživatelů
  • GET /api/users/{id} — Podrobnosti o uživateli
  • PUT /api/users/{id} — Aktualizace uživatele (standardní users-permissions update)
  • PUT /api/users/me/profile — Aktualizace vlastního profilu přihlášeného uživatele (whitelisted pole: name, surname, jobTitle, street, city, zip_code, country) — preferovaný endpoint pro self-service editaci profilu, obsluhovaný api::auth.auth.updateMyProfile
  • GET /api/organisations — Seznam organizací
  • POST /api/organisations — Vytvoření organizace
  • PUT /api/organisations/{id} — Aktualizace organizace (vyžaduje roli admin+)
  • POST /api/organisations/:id/invite — Pozvání uživatele emailem (jen owner/admin)

Všechny endpointy výše jsou dostupné také pod verzovaným jmenným prostorem /api/v1/ (např. /api/v1/organisations) — viz Verzování API pro deprecation politiku a migrační průvodce.

Strapi backend není jediná API plocha na platformě — a generovaná API Reference výše pokrývá jen Strapi. Ostatní služby vystavují vlastní REST API, které jinde v této dokumentaci nejsou indexované:

  • API Manager (port 3200) — vydává a rotuje Strapi API tokeny ostatním microservices; GET /api/token/:service (viz Verzování API pro jeho verzovaný alias /v1/token/:service).
  • GraphQL Gateway (port 4000) — sjednocené GraphQL schéma nad core Strapi entitami, Apollo Server 5 na Fastify; resolvery delegují na podkladové Strapi REST endpointy. Vyžaduje GRAPHQL_ENABLED=true a Keycloak Bearer JWT. Kompletní schéma, ukázkové query, auth/org-scoping a limity depth/complexity/alias najdete na dedikované stránce GraphQL Gateway.
  • LLM Service (port 4430) — provider-agnostic LLM fasáda, vystavuje /chat a /complete s Keycloak JWT auth a per-org budget enforcement.
  • Billing Adapter (port 3600) — integrace Lago/Stripe billing; kromě požadavků běží i sync/webhook joby.
  • Agent Gateway (port 4400) — HTTPS/WebSocket fleet C2 brána pro on-host Sencai agenty (enrollment, heartbeat, dispatch runbooků).
  • Root CLAUDE.md — Keycloak sync architektura
  • Strapi v5 docshttps://docs.strapi.io
  • Webhook Publisherwebhook-publisher/src/routes/