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ý fakt — backend/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
sessionss vazbou nausers.id, platnost 30 dní
Session může vzniknout dvěma cestami:
POST /api/login— e-mail a hesloPOST /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.
A2. Magic link není druhý faktor a obchází heslo¶
Ověřený fakt — AuthController::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:
- 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.
- 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ý fakt — backend/src/Middleware/AuthMiddleware.php.
AuthMiddleware ověří pouze:
- že cookie
auth_tokenobsahuje platný JWT, - že odpovídající řádek v
sessionsexistuje 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::deleteUserController(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.jsxvolábase44.functions.invoke('clientAuth', { action: 'request_otp' | 'verify_otp' }).base44Client.jssměrujefunctions.invokenaappAuth.call(), ta voláPOST /api/functions/{name}(react_dev/src/lib/appAuth.js:168).- Skupina
/api/functionsje vbackend/src/routes.phpzaAuthMiddleware. FunctionsController::invoke()zná jen zděděné funkce z Fachmana (registerCompany,inviteTeamMember,sendToIdoklad,manageSubscription…);clientAuthmezi 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ý fakt — react_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ý fakt — AuthController::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:
- Oddělená klientská session. Klient dostane vlastní cookie a vlastní subjekt
(
client_idnad tabulkouclients), ne session nadusers. Middleware personálních rout klientskou session nepřijme, a naopak. - Middleware, který bez explicitní role nic nepustí.
AuthMiddlewareby 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.