Přeskočit obsah

10. Stav autentizace a autorizace vlastního backendu

Záznam stavu k 1. 9. 2026. Tento dokument popisuje backend/ — vlastní PHP API, ne Base44 export. Rizika ze zděděného exportu jsou v kapitole 7. Rizika; ta se týkají react_dev/base44/functions/, tedy kódu, který se v novém backendu teprve bude psát.

Backend je rozepsaný. Většina toho, co je níže popsáno jako chybějící, není rozbitá funkce, ale nenapsaná část. Smysl zápisu je, aby se to rozhodlo teď, dokud je to levné — ne aby se to opravovalo, až na tom bude stát dvacet controllerů.

Oddíly A–E jsou popis stavu, oddíl F je doporučení. Zápis vznikl jako poradní podklad, ne jako zadání — co se z něj udělá, je na týmu, který aplikaci dál staví.

Shrnutí

Aplikace není interní CRM. Vedle personálních obrazovek obsahuje klientský portál, terapeutický portál a několik veřejných landing stránek. Backend zatím zná jen jeden druh přihlášeného uživatele — zaměstnance — a jeho autorizační vrstva nekontroluje roli. Až se bude psát přihlášení klienta, rozhodne se, jestli klient dostane tutéž session jako personál. Když ano, dostane s ní i přístup ke kartotéce.

A. Jak autentizace dnes funguje

Ověřený faktbackend/src/Services/AuthSessionService.php, backend/src/Controllers/AuthController.php.

Existuje jeden druh sezení:

  • JWT v cookie auth_token (HttpOnly, path /api), platnost 30 minut
  • refresh token v cookie auth_refresh_token (path /api/auth), platnost 7 dní, rotuje se při každém refresh, v DB uložen jako SHA-256 hash
  • záznam v tabulce sessions s vazbou na users.id, platnost 30 dní

Session může vzniknout dvěma cestami:

  1. POST /api/login — e-mail a heslo
  2. POST /api/login/request → e-mail s odkazem → POST /api/loginTokens/{token} — magic link, token 30 minut, jednorázový

Ověřený fakt: obě cesty volají tentýž issueSessionForUser() a vydají identickou session. Backend po přihlášení nepozná, kterou cestou uživatel přišel.

Ověřený faktAuthController::request() a AuthController::verify().

POST /api/login/request přijme libovolný e-mail existujícího uživatele a pošle mu přihlašovací odkaz. Nekontroluje se přitom heslo, role ani to, jestli má uživatel přihlášení odkazem vůbec povolené. verify() pak vydá plnohodnotnou session (issueSessionForUser()), tedy tutéž, jakou dostane uživatel po přihlášení heslem.

Z toho plynou dvě věci:

  1. Není to vícefaktorové ověření. Odkaz v e-mailu heslo nahrazuje, nepřidává se k němu. Jde o jediný faktor — přístup do schránky. Označovat to navenek jako MFA je nepřesné a v dotazu na IT by to takhle stát nemělo.
  2. Heslo je obejitelné. Kdo se dostane do e-mailové schránky zaměstnance, přihlásí se do CRM, aniž by heslo kdy viděl. Ve spojení s user enumeration z oddílu E (endpoint prozradí, které adresy mají účet) je i zjistitelné, na které adresy to funguje.

Odvozeno: pokud má být přihlášení personálu opravdu dvoufaktorové, musí druhý faktor přibýt k heslu (TOTP, klíč, ověření na proxy), ne ho nahradit. Případně omezit magic link jen na klientský portál a pro personál ho vypnout.

B. Autorizace: middleware roli nekontroluje

Ověřený faktbackend/src/Middleware/AuthMiddleware.php.

AuthMiddleware ověří pouze:

  • že cookie auth_token obsahuje platný JWT,
  • že odpovídající řádek v sessions existuje a nevypršel,
  • že firma uživatele je active.

Pak nastaví user_id a session_id do requestu a pustí dál. Roli neřeší vůbec.

Kontroly role jsou ad hoc až uvnitř jednotlivých controllerů, přes UserHelper::isAdmin() / isCompanyAdmin(). Mají je:

  • SystemConfigController (get i update)
  • CompanyController::delete
  • UserController (create, update, delete, listTherapists…)
  • část FunctionsController

Nemají je vůbec:

Skupina rout Controller Co je za tím
/api/clients ClientController celý CRUD nad kartotékou klientů
/api/{invoices\|paymentLogs} EntityCrudController faktury, platební logy
/api/apps/{appId}/entities/{Invoice\|PaymentLog} EntityCrudController totéž přes Base44 route
/api/clientCategories ClientCategoryController kategorie klientů
/api/notifications NotificationController notifikace
/api/orders OrderController zakázky (list má větev na isAdmin, zbytek ne)

Ověřený fakt: tabulka users má sloupec role s hodnotami admin, admin-klientske, klientske, finance, therapist, owner, user, technician (backend/db/schema.sql). Na výše uvedených routách se ale nepoužije.

Důsledek (odvozeno): kdokoli s platnou session — bez ohledu na roli — si přes GET /api/clients stáhne kompletní seznam klientů zařízení. Dnes to znamená „kterýkoli zaměstnanec"; po doplnění klientského přihlášení to bude znamenat „kterýkoli klient", pokud dostane stejný typ session.

Výchozí stav vrstvy je tedy povoleno, a zákaz se dopisuje ručně. Mělo by to být naopak.

C. Klientský a terapeutický portál na backendu neexistují

Ověřený fakt:

  • react_dev/src/pages/ClientLogin.jsx volá base44.functions.invoke('clientAuth', { action: 'request_otp' | 'verify_otp' }).
  • base44Client.js směruje functions.invoke na appAuth.call(), ta volá POST /api/functions/{name} (react_dev/src/lib/appAuth.js:168).
  • Skupina /api/functions je v backend/src/routes.php za AuthMiddleware.
  • FunctionsController::invoke() zná jen zděděné funkce z Fachmana (registerCompany, inviteTeamMember, sendToIdoklad, manageSubscription…); clientAuth mezi nimi není.

Nepřihlášený klient se tedy k přihlášení nedostane — endpoint vyžaduje přihlášení. Klientské přihlášení není zranitelnost, prostě zatím není napsané. Totéž platí pro terapeutický portál (components/therapist/TherapistOtpLogin.jsx).

Ověřený fakt: klienti nejsou v tabulce users. Jsou v samostatné tabulce clients, která nemá heslo ani žádný autentizační sloupec (backend/db/schema.sql).

Odvozeno: až se clientAuth bude psát, nejmenší cesta odporu je zavolat existující issueSessionForUser(). Tím by klient dostal session nad tabulkou users a s ní i všechny routy z oddílu B.

D. Jedna SPA, jeden origin

Ověřený faktreact_dev/src/App.jsx. Veřejné i personální obrazovky jsou v jednom Reactovém buildu na jedné doméně. Mimo StaffGate jsou tyto routy:

/login, /token, /client, /invoice, /redeem, /storno, /TherapistPortal

Všechno ostatní je za StaffGate.

Že si klient stáhne JS s administrací, samo o sobě data neodhalí — autorizace patří na backend. Leakne to strukturu aplikace a interní názvy, což je nepříjemné, ne kritické.

Problém je jiný: jeden origin znemožňuje oddělit obě části na perimetru. Nelze zároveň omezit administraci na firemní síť nebo VPN a nechat klientský portál dostupný z internetu, když obojí sdílí doménu a cookie prostor. Původní znění dotazu na IT (intranet + omezení na české IP) proto nešlo splnit; korigovaná verze mailu už s dělením počítá, ale to dělení musí umět aplikace, ne poskytovatel infrastruktury.

E. Drobnosti k opravě při té příležitosti

Ověřený faktAuthController::request():

if (!$user) {
    // neprezradzuj, že email neexistuje
    //return $this->json($response, ["success" => true]);
    return $this->json($response, ["success" => false, "msg" => "Účet nenalezen"]);
}

Endpoint prozrazuje, které e-maily mají účet (user enumeration). Správná varianta je o řádek výš zakomentovaná.

Ověřený fakt: na /api/login ani /api/login/request není žádný rate limit.

Ověřený fakt: backend/db/schema.sql drží krok s backendem, ale ne s frontendem. Obsahuje ještě zděděné tabulky z Fachmana (orders, inquiries, subscription_plans, referal) a naopak nemá rezervace, kalendář ani vouchery. Odpovídá to ale tomu, co backend dnes umí — controllery pokrývají klienty, faktury, payment logy, zakázky, notifikace, firmy a uživatele, nic víc. Napřed je frontend, ne schéma; jako podklad pro lokální nasazení je schema.sql použitelné.

Ověřený fakt: v routes.php je registrovaný GET /api/test/tokens (TestController::tokens) bez autentizace. Před nasazením prověřit a odstranit.

Ověřený fakt: routes.php obsahuje natvrdo zadané Base44 app ID 691ca694da5245fd49ef4be7 u tří rout, z toho POST .../functions/registerCompany je registrace firmy bez autentizace.

F. Doporučení k dalšímu postupu

Tento oddíl je doporučení, ne zadání. Rozhodnutí patří kolegovi, který kód píše, a tomu, kdo bude aplikaci vlastnit; níže je jen podklad, ať se rozhoduje s otevřenýma očima.

Dvě věci bych neotvíral jako otázku — nevidím u nich druhou rozumnou variantu:

  1. Oddělená klientská session. Klient dostane vlastní cookie a vlastní subjekt (client_id nad tabulkou clients), ne session nad users. Middleware personálních rout klientskou session nepřijme, a naopak.
  2. Middleware, který bez explicitní role nic nepustí. AuthMiddleware by měl přebírat požadovanou roli nebo oprávnění a být výchozím stavem zákaz. Dopsat to do dvaceti controllerů zpětně je práce navíc; zabudovat to do prvního je zadarmo.

Jedna věc je skutečná volba: jak oddělit administraci od klientského portálu.

  • Dvě domény, dva buildy — čistší oddělení, ale znamená rozdělit sdílené komponenty, dva deploye a dvojí konfiguraci.
  • Jeden build dělený na reverzní proxy podle cesty — na perimetru stejný efekt (administrace jen z VPN nebo firemní sítě, portál veřejně) za cenu jednoho konfiguračního souboru.

Na velikost tohohle týmu bych volil druhou variantu. Bez jedné z nich ale nelze splnit, co je slíbené v dotazu na IT.

Rozhodnuto (1. 9. 2026): SSO přes Entra ID se dělat nebude. Aplikace s ním nepočítá a vlastní přihlášení personálu už existuje (/api/login plus magic link). Korigovaná verze dotazu na IT ale SSO zmiňuje jako záměr — je potřeba to z mailu vyškrtnout, ať se Aricoma neptá na napojení, které nikdo nechce. Vícefaktorové ověření pro administraci, pokud se bude chtít, se dá řešit nezávisle na Entra ID — dnešní magic link jím ale není, viz oddíl A2.