Přeskočit obsah

14. TODO: striktní API perimetr

Stav k 24. 9. 2026, commit 83be60c. Úkoly pro backend, které je potřeba dokončit před spuštěním. Navazuje na 11. Roadmap autentizace a 12. Technologický dluh.

Oddíl A jsou konkrétní díry, které jdou opravit hned a samostatně. Oddíly B–D jsou změna struktury routes.php, po které už podobná díra nevznikne omylem.

Proč

Práva jsou v backend/src/routes.php vyjmenovaná u každé skupiny, to je v pořádku. Výchozí zákaz to ale není: Slim sám nic nezakazuje a routa bez ->add($auth(...)) je otevřená. Zákaz dnes drží jen tím, že na ->add nikdo nezapomene. Oddíl A ukazuje, že se to už stalo.

Cíl: routa pod /api je chráněná tím, že je pod /api, ne tím, že má správný řádek navíc. Veřejná routa musí být veřejná výslovně a dohledatelně.

A. Otevřené díry — opravit hned

A1. Zaměstnanec si sám nastaví roli admin (kritické)

Ověřený fakt — PUT /api/user/{id} → UserController::update(), route má jen $auth($authenticated).

Uživatel, který není admin, smí upravit vlastní účet (authUser.id === id z URL). Z těla požadavku se ale bere i role a is_active a kontroluje se jen, že je role ze seznamu povolených — ne, kdo ji mění. Terapeut nebo recepce si tak jedním požadavkem {"role": "admin"} nastaví admina. AuthMiddleware::getAppRole() pak z role = admin udělá plného admina se všemi oprávněními.

  • Pro ne-admina ignorovat nebo odmítnout role a is_active (a zvážit i email, podle kterého se páruje passkey login).
  • Test: terapeut → PUT /api/user/{vlastní id} s role=admin → role se nezmění.
  • Zjistit, jestli frontend tuhle routu vůbec volá. Ve react_dev/src jsem volání nenašel; pokud se nepoužívá, smazat.

A2. POST /api/user bez přihlášení založí firmu a admina (střední)

Ověřený fakt — skupina /api/user, $group->post('', UserController::create) nemá žádný middleware. create() založí novou firmu a v ní uživatele s rolí admin / company_admin. Passkey callback páruje účet jen podle e-mailu (WHERE u.email = ? AND is_active = 1) a kontroluje iss i aud tokenu.

Ověřený fakt — NEO má vlastní passkey server: .env na mswork má AUTH_URL=https://auth.neocentrum.cz, AUTH_JWT_ISSUER=auth.neocentrum.cz, AUTH_JWT_AUDIENCE=neo-cal-app. Identita ze sdíleného auth.mscms.cz (Petrovo CMS) v NEO neprojde. Pozvánky na auth.neocentrum.cz posílá jen admin (/api/auth/passkey/invite) nebo CLI.

Odvozeno — heslo je prázdné a přihlášení heslem je vypnuté (STAFF_PASSWORD_LOGIN_ENABLED), takže založeným účtem se nikdo nepřihlásí. Zůstává zakládání firem a adminů komukoli a obsazení e-mailu budoucího zaměstnance předem (email je unikátní) — pak mu nepůjde vytvořit účet.

  • Smazat. Je to zbytek z Fachmana; zakládání uživatelů dělá POST /api/users (createCompanyUser, za $admins).

A3. POST /api/register bez přihlášení založí firmu a admina (vysoká)

Ověřený fakt — AuthController::register(), bez middleware a bez přepínače. Totéž co A2, jen s heslem. Přihlášení heslem je dnes vypnuté, ale stačí ho zapnout v .env (třeba kvůli ladění) a registrace je otevřená komukoli.

  • Smazat, nebo dát za stejný přepínač jako login heslem. NEO je jedna firma, samoobslužná registrace firem nedává smysl.

A4. Testovací a zbytkové routy (střední)

Ověřený fakt — bez middleware:

Routa Co dělá
POST /api/sendmail pošle mail na natvrdo zadanou gmailovou adresu; opakovaným voláním se dá zahltit schránka a vyčerpat kvóta mailového poskytovatele
GET /api/test-ecomail vypíše seznamy z Ecomailu přes print_r
GET /api/hello testovací
POST /api/analytics/track/batch prázdný stub z Base44
  • Všechny čtyři smazat (případně stub analytics nechat, pokud ho frontend volá, ale pod /api/public).
  • MailController má natvrdo SECRET_JWT_KEY_CHANGE_ME — s routou smazat i jeho.

A5. Zbytky z Fachmana v perimetru (nízká, ale k rozhodnutí)

Ověřený fakt — veřejné bez middleware: /api/inquiry/submitInquiry, /api/fnct/idokladCallback, /api/fnct/idokladAuthorize, /api/images/*, /api/billing/webhook (Stripe). Za $admins: /api/companies, /api/referral, /api/subscription, /api/billing/*.

  • U každé rozhodnout: používá ji NEO? Když ne, smazat. Když ano, přesunout podle oddílu B.

B. Cílová struktura: tři skupiny

Skupina Kdo volá Middleware skupiny Dostupnost
/api/auth nepřihlášený personál žádný, jen přihlašovací routy intranet/VPN
/api personál staff session vždy ($auth($authenticated) na skupině), podskupiny přidávají oprávnění intranet/VPN
/api/public internet: klient, terapeut, token, webhook povinný režim přístupu u každé routy (oddíl C) internet

Ve Slimu to znamená jeden $app->group('/api', ...)->add($auth($authenticated)), do kterého se přesunou všechny dnešní staff skupiny. Oprávnění ($clientsAccess, $admins …) zůstávají na podskupinách jako dnes. Zapomenuté ->add pak znamená nanejvýš chybějící oprávnění, ne otevřenou routu.

Pozor: skupina /api/auth a /api/public se musí registrovat mimo skupinu /api, i když mají stejný prefix URL. Ve Slimu platí middleware skupiny jen pro routy registrované uvnitř ní, ne podle prefixu URL.

Co se musí přesunout

Do /api/auth (bez session): /api/login, /api/login/request, /api/loginTokens/{token}, /api/auth/refresh, /api/auth/logout, /api/auth/passkey/login, /api/auth/passkey/callback, /api/invitations/{token}, /api/invitations/{token}/complete. Část z nich už prefix /api/auth má, ostatní změní URL — upravit i frontend.

Do /api/public (volá je internet): vše z A5, co zůstane — webhooky, callbacky, obrázky, submitInquiry. URL webhooků a callbacků je nastavená u poskytovatele (Stripe, iDoklad, Comgate) — po přesunu ji tam změnit, jinak přestanou chodit platby.

Do /api (staff): /api/getUser, /api/updateMe, /api/user-info, /api/me, /api/auth/passkey/manage, /api/auth/passkey/invite, /api/upload, /api/functions/* a všechny $app->group(...) od /api/users níž.

K rozhodnutí: /api/cron/functions/reminderDispatcher. Od commitu 83be60c existuje backend/bin/run-reminders.php. Odvozeno: pokud cron běží přes CLI, HTTP routa je zbytečná a má zmizet.

C. /api/public: režim přístupu povinně u každé routy

/api/public neznamená „bez přihlášení“, ale „dostupné z internetu“. Podle roadmapy 11 tam chodí:

  • public — bez identity (veřejný rezervační portál, číselníky),
  • client — klientská session (ClientAuthMiddleware),
  • staff — passkey session personálu (terapeutský portál),
  • token — jednorázový token v URL (onboarding, platba, storno, doklad),
  • webhook — volá poskytovatel, ověřuje se podpisem.

Jeden middleware na celou skupinu tu nestačí. Přísnost znamená, že každá routa musí režim výslovně deklarovat a routa bez deklarace neprojde. Dvě cesty, stačí jedna:

  1. Middleware skupiny /api/public, který čte deklaraci routy (např. argument ->setArgument('access', 'client')) a routu bez ní odmítne 500 + log.
  2. Test z oddílu D.

Ověřený fakt — dnešní stav /api/public bez middleware, kde kontrola musí být uvnitř controlleru (a kde jsou otevřené nálezy auditu REZ/SET):

  • POST /api/public/{clientBooking|comgatePayment}
  • PUT /api/public/{calendarEvents|bookings}/{id} (publicUpdate)
  • GET /api/public/{18 entit} (publicList)
  • GET /api/public/therapists, GET /api/public/clientCategories
  • POST /api/public/client_auth, /onboarding, /refresh, /logout

U každé z nich doplnit do tabulky níže, jaký má režim a čím se ověřuje.

Routa Režim Čím se ověřuje
…

D. Pojistka: test perimetru

Aktuální stav: existuje backend/tests/Feature/ApiPerimeterTest.php. Ten ověřuje vybrané chování perimetru integračně (např. interní /api/clients bez session vrací 401, client session neplatí na interních routách, /api/test/tokens je za admin auth, několik terapeutických/public rout funguje pro přihlášeného terapeuta). Je to užitečná sada, ale není to plošná pojistka proti nově přidané špatně zařazené routě.

Ještě chybí samostatný route-collector test v backend/tests/Feature/Audit/, který projde všechny registrované routy Slim aplikace ($app->getRouteCollector()->getRoutes()) a spadne, když:

  • routa pod /api (mimo /api/auth a /api/public) nemá staff AuthMiddleware,
  • routa pod /api/public nemá deklarovaný režim přístupu,
  • routa není pod žádným z prefixů /api/auth, /api, /api/public,
  • routa pod /api/auth není na explicitním seznamu přihlašovacích rout.

Tenhle test hlídá, aby nová routa nevznikla omylem otevřená. Jestli daná role smí na daný endpoint, nehlídá — to je audit po zdrojích (Přehled auditu).

  • Částečný behaviorální test perimetru: backend/tests/Feature/ApiPerimeterTest.php.
  • Dopsat plošný route-collector test. Nejdřív ho pustit proti dnešním routám — musí najít všechno z oddílu A.

Checklist

  • A1 — ne-admin nemění role / is_active, test
  • A2 — smazat POST /api/user
  • A3 — smazat nebo zamknout POST /api/register
  • A4 — smazat sendmail, test-ecomail, hello, analytics/track/batch, MailController
  • A5 — rozhodnout o zbytcích z Fachmana
  • B — skupina /api se staff session na skupině
  • B — skupina /api/auth, přesun přihlašovacích rout, úprava frontendu
  • B — přesun webhooků/callbacků pod /api/public, změna URL u poskytovatelů
  • B — rozhodnout o /api/cron/functions
  • C — deklarace režimu u každé /api/public routy, vyplněná tabulka
  • D — plošný route-collector test perimetru, zelený
  • Aktualizovat pravidla proxy (Deployment a routing) na tři prefixy

Definice hotovo

  • Žádná routa pod /api (mimo /api/auth) neprojde bez staff session, a to díky skupině, ne díky řádku u routy.
  • Každá routa pod /api/public má deklarovaný režim a tabulka v oddílu C je vyplněná.
  • Test perimetru je zelený a běží v CI / před nasazením.
  • Na produkci nejsou testovací ani nepoužívané Fachman routy.