Search by

liquiddesign / eshop-doryo-admin

info@lqd.cz

Zápis do administrace e-shopu pro Doryo/Kolego — headless spouštění admin presenterů: gridy ke čtení, formuláře a akce s náhledem a tokenem, hromadné úpravy

Package info

github.com/liquiddesign/eshop-doryo-admin

pkg:composer/liquiddesign/eshop-doryo-admin

Statistics

Installs: 22

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-10-12 12:32 UTC

This package is auto-updated.

Last update: 2026-10-12 13:09:36 UTC


README

Zápis do administrace e-shopu pro Doryo / Kolego. Rozšiřuje balík liquiddesign/eshop-doryo-api (čtecí API) o cesty /v1/admin/…, přes které agent čte seznamy administrace, vyplňuje její formuláře, spouští její akce a dělá hromadné úpravy — se stejnými kontrolami a stejnými následky, jako kdyby to člověk naklikal v /admin.

Nic se nekopíruje. Brána spouští presentery administrace normální cestou Nette (IPresenterFactory::createPresenter() a Presenter::run()), jen bez prohlížeče. Platí tedy skutečná oprávnění (checkRequirements()), skutečná validace a skutečná onSuccess logika — i ta ve vendoru, kterou projekt nemá jak duplikovat. Když někdo formulář změní, brána ho popíše a vyplní podle nové podoby.

Zápis je vždycky dvoufázový: náhled ("dryRun": true) vrátí, co se změní, a podepsaný previewToken; ostrý zápis bez tokenu neprojde. Agent tak nemůže zapsat nic jiného, než co předtím ukázal uživateli.

Cílí na novou generaci knihoven: PHP ≥ 8.2, nette/application 3.2, nette/forms 3.2, nette/http 3.3–3.4, liquiddesign/admin 2.1, grid 1.0, forms 3.0. Vznikl z „brány administrace" projektu Levior (tam běží nad starší generací); první konzument je For Baby.

Instalace

composer require liquiddesign/eshop-doryo-admin
extensions:
    doryoApi: DoryoApi\Bridges\DoryoApiDI
    doryoAdmin: DoryoAdmin\Bridges\DoryoAdminDI

doryoAdmin:
    administratorLogin: doryo

Služby si balík zaregistruje sám a cesty /v1/admin/… sám přidá do API balíku eshop-doryo-api: v beforeCompile() připíše službu @doryoAdmin.endpoint do endpointů routeru a openapi.json, jako by ji projekt uvedl v doryoApi: endpoints:. Rozšíření eshop-doryo-api se hledá podle třídy, na jeho klíči v extensions: nezáleží. Kdo to chce mít v configu výslovně, nastaví registerEndpoint: false a přidá ji sám:

doryoApi:
    endpoints:
        - @doryoAdmin.endpoint

(Když ji projekt uvede sám a registerEndpoint nechá zapnutý, podruhé se nepřidá.)

Předpoklady, které už splňuje každý shop s čtecím API: doryoApi má token a požadavek na DoryoApi:Api projde bránou na front (frontAccess.exclude). Brána potřebuje rozšíření administrace Admin\Bridges\AdminDI — uživatele administrace (Admin\Administrator) si najde podle typu.

Konfigurace

klíč výchozí k čemu
administratorLogin doryo login administrátorského účtu, pod kterým brána jede (servisní účet)
agentName Kolego jméno agenta do auditu: <administrátor> via Kolego (<actor>)
previewTokenLifetime 600 platnost náhledového tokenu v sekundách
bulkConfirmFrom 50 od kolika cílů chce hromadná úprava "confirm": true; dá se přebít u gridu
merchantAdministrators false požadavek za obchodníka jede pod jeho administrátorským účtem, viz Identita
merchantColumn fk_merchant sloupec tabulky administrátorů s vazbou na obchodníka
identityResolver null vlastní DoryoAdmin\Gateway\IdentityResolver projektu (přebíjí merchantAdministrators)
administratorService null jméno služby Admin\Administrator, když jich je víc
registerEndpoint true cesty se samy přidají do doryoApi: endpoints:
describers [] služby DoryoAdmin\Gateway\PageDescriber, viz Rozšíření
forms, actions, grids, bulk [] whitelist — co brána umí popsat a spustit

Whitelist je konfigurace, ne kód — další stránka se přidá řádkem v NEONu. Oprávnění se jím neřeší (to drží role účtu v Administrace → Role), whitelist jen říká „tohle brána umí popsat". Položky mají pevné klíče, překlep spadne už při kompilaci kontejneru. Popisy jsou texty pro model; procento se v nich píše %% (Nette jinak %…% bere jako parametr) a text nesmí začínat @.

položka klíče (výchozí)
forms presenter, parameter (parametr akce se záznamem), form (form), action (edit), label, description, create (false), createAction (new), entity (kvůli zakládání), createNote
actions presenter, signal (bez handle), action (default), label, params (jména parametrů signálu), destructive (false), entity, idParam
grids presenter*, grid (grid), action (default), label, description, filters (ruční popis filtrů bez popisku), parameter (seznam patří k záznamu), tabParameter (persistentní parametr se záložkou)
bulk presenter, entity, grid (grid), form (form, detailový formulář, ze kterého si hromadná úprava půjčuje vstupy), action (bulkEdit), label, bulkConfirmFrom, filters

* povinné

Příklad: For Baby

# config/doryo-admin.neon
extensions:
    doryoAdmin: DoryoAdmin\Bridges\DoryoAdminDI

doryoAdmin:
    administratorLogin: doryo
    # obchodník z Kolega (X-Doryo-Za / merchantId) jede pod svým administrátorem (admin_administrator.fk_merchant),
    # takže platí role „Obchodník" a MerchantAdminContext — vidí a mění jen své zákazníky a jejich objednávky
    merchantAdministrators: true
    forms:
        -
            presenter: 'Eshop:Admin:Customer'
            form: form
            action: edit
            parameter: customer
            entity: Eshop\DB\Customer
            label: 'Zákazník'
            description: 'Detail zákazníka: firma, kontakty, ceníky, obchodníci, platební podmínky.'
    grids:
        -
            presenter: 'Eshop:Admin:Customer'
            grid: customers
            label: 'Zákazníci'
            description: 'Fakturační organizace tak, jak je ukazuje administrace (pobočky jsou pod nimi). Údaje jednoho zákazníka čti radši z formuláře zákazníka nebo z /v1/customers.'
    actions:
        -
            presenter: 'Eshop:Admin:Order'
            signal: cancelOrder
            params: [orderId]
            idParam: orderId
            entity: Eshop\DB\Order
            label: 'Stornovat objednávku'
            destructive: true

Účet a role

Brána jede pod administrátorským účtem (administratorLogin). Bez něj vrací 503 s návodem. Zakládá se klikáním, žádná migrace:

  1. Administrace → Role: nová role, třeba Kolego (API).
  2. V gridu role „Přístup" (555) u stránek, které má agent číst, a „Plný přístup" (777) u stránek, kde má měnit. Začni s 555 všude a 777 přidávej.
  3. Administrace → Administrátoři: administrátor s účtem doryo, dlouhé náhodné heslo (nikdo se jím nepřihlašuje) a touhle rolí.

Bez „Plného přístupu" se agentovi pole chovají stejně jako člověku: co administrace bez 777 vypíná (setDisabled(!$this->isManager)), je ve schématu readOnly: true a pokus o změnu je 403. Akce a hromadné úpravy chtějí 777 vždycky — checkRequirements() u signálů a u akce bulkEdit kontroluje jen přístup na stránku (v administraci je ochranou, že tlačítko vidí jen oprávněný), přes API je to málo.

Pozor: PermissionRepository::isAllowed() porovnává resource jako prefix, takže :Eshop:Admin:Customer:* pustí i :Eshop:Admin:CustomerLead:*. Je to chování administrace.

Identita (kdo zapisuje)

Na začátku každé obsluhy se určí, za koho brána jedná (DoryoAdmin\Gateway\IdentityResolver, vrací Nette\Security\IIdentity). Pod touhle identitou se presentery pouštějí, platí její role a do auditu jde ona. Mapa stránek ji vrací v identity.

  • Servisní účet (výchozí): všechno pod administratorLogin. Požadavek, který nese obchodníka, je tady 403 — servisní účet má širší práva a obchodník by s nimi potichu zapisoval.
  • Administrátor obchodníka (merchantAdministrators: true): obchodníka nese hlavička X-Doryo-Za (Kolego ji posílá jen s platným tokenem; když přijde víckrát, platí první hodnota), parametr ?merchantId= nebo "merchantId" v těle zápisu. Brána se přihlásí jako administrátor, kterému je obchodník v administraci přiřazený (admin_administrator.<merchantColumn>, existence sloupce se ověřuje). Platí jeho role i filtry, které projekt staví na přihlášeném (For Baby MerchantAdminContext: jen zákazníci obchodníka a jeho podřízených, cizí detail objednávky odmítne). Bez obchodníka jede požadavek pod servisním účtem.
    • obchodník bez administrátorského účtu → 403 s návodem (založit mu ho v Administrace → Administrátoři s přiřazeným obchodníkem a rolí),
    • obchodník s víc účty → 403 (brána neví, čí role platí),
    • vypnutý účet → 403, neznámý obchodník → 404,
    • obchodník poslaný na víc místech a pokaždé jiný → 400.
  • Vlastní resolver: identityResolver: App\Doryo\MyResolver — třeba až Kolego začne posílat identitu člověka.

Audit (admin_changelog): user = <login administrátora> via Kolego (<actor>), entity = presenter, objectId = id záznamu, type = form:<akce>, signal:<signál> nebo bulk:<grid>:<počet>. actor z těla vyplňuje model — je to jen popisek, oprávnění na něm nestojí.

Náhledový token je vázaný i na identitu: náhled, který viděl jeden obchodník, nezapíše jiný ani servisní účet.

Cesty

Všechny pod prefixem a tokenem čtecího API (https://<shop>/doryo-api/v1/admin/…, Authorization: Bearer <token>). Chyby jsou česky v application/problem+json. Popis pro model je v openapi.json (schémata těla inline, Kolego $ref v těle neresolvuje).

metoda a cesta k čemu
GET /v1/admin/pages mapa administrace z menu (admin: menu:) omezená na to, kam identita smí; u stránek formuláře, akce, gridy a hromadné úpravy z whitelistu
GET /v1/admin/forms/{presenter}/{form}?id= schéma polí (typ, popisek, nápověda, povinnost, číselníky, readOnly) a hodnoty záznamu; bez id schéma prázdného formuláře pro založení
POST /v1/admin/forms/{presenter}/{form} náhled (diff + token) nebo ostrý zápis; tělo bez id = náhled založení
POST /v1/admin/actions/{presenter}/{signal} akce (signál handle*); destruktivní chtějí "confirm": true
GET /v1/admin/grids/{presenter}/{grid} stránka seznamu: text buněk pod hlavičkami, filter (JSON), page, limit (≤ 100), order, id, tab
GET /v1/admin/bulk/{presenter}/{grid} co umí hromadná úprava: vstupy s režimy, filtry, práh potvrzení
POST /v1/admin/bulk/{presenter}/{grid} hromadná úprava nad ids nebo filter; náhled a pak ostrý zápis

Všechny cesty berou merchantId (viz Identita).

Formulář: náhled → token → zápis

POST /doryo-api/v1/admin/forms/Eshop:Admin:Customer/form
{"id": "a1b2c3", "values": {"phone": "+420 601 000 111"}, "dryRun": true, "actor": "Jan Novák"}
{
  "dryRun": true, "create": false, "id": "a1b2c3", "valid": true,
  "previewToken": "eyJ2Ijoi….9f3c", "expiresAt": "2026-10-12T14:32:00+02:00",
  "diff": [{"field": "phone", "label": "Telefon", "from": "+420 602 111 222", "to": "+420 601 000 111"}],
  "errors": [],
  "warnings": ["Pole „Ceníky\" jsou v tomhle formuláři jen ke čtení; pokus o jejich změnu skončí chybou 403."]
}

Diff agent ukáže uživateli a zapíše totéž s "dryRun": false a tokenem:

POST /doryo-api/v1/admin/forms/Eshop:Admin:Customer/form
{"id": "a1b2c3", "values": {"phone": "+420 601 000 111"}, "dryRun": false, "previewToken": "eyJ2Ijoi….9f3c"}
{"dryRun": false, "written": true, "id": "a1b2c3", "diff": [...], "flashes": [{"type": "success", "message": "Uloženo"}]}
  • Posílají se jen pole, která se mění; brána formulář pošle celý, jak by ho poslal prohlížeč (včetně názvů nahraných obrázků, ty zůstávají).
  • Náhled s neplatnou hodnotou je 200 s "valid": false a errors po polích, bez tokenu.
  • Jiné hodnoty než v náhledu, změněný záznam, vypršelý token nebo token jiné identity = 409, zopakovat náhled.
  • Zakládání (create: true ve whitelistu): náhled bez id vygeneruje id a vrátí ho; ostrý zápis pošle to id, "create": true a token. Neznámé id bez "create": true je chyba, ne pokyn k založení. createNote z konfigurace se vrací ve schématu i náhledu a model ho má uživateli přetlumočit.

Akce

POST /doryo-api/v1/admin/actions/Eshop:Admin:Order/cancelOrder
{"params": {"orderId": "…"}, "dryRun": true}

Náhled vrátí record (id, kód, stav), requiresConfirm a token. Ostrý běh: totéž s "dryRun": false, tokenem a u destruktivní akce "confirm": true; odpověď nese before, after a flashes. Náhled otevře stránku akce s jejími parametry (bez signálu), takže záznam, na který identita nesmí, odmítne administrace už tady.

Seznamy (gridy)

GET /doryo-api/v1/admin/grids/Eshop:Admin:Customer/customers?filter={"search":"Novák"}&page=1&limit=20

Vrací total, pageCount, hasMore, columns, rows (id, cells — text buněk bez HTML, nejvýš 300 znaků), filters a orderable. Neznámý filtr je 400 — nefiltrovaný seznam by vypadal jako odfiltrovaný. Stačí „Přístup" (555).

Hromadná úprava

GET vrátí vstupy a u každého modes — přesně to, co u něj nabízí hromadná úprava v administraci (liquiddesign/admin 2.1):

režim kde co udělá
add vztah N:N (ceníky, seznamy viditelnosti) přidá k tomu, co záznam má
remove vztah N:N odebere poslané položky
replace výběr více položek původní vazby zahodí a nastaví poslané ([] = smazat všechny)
set ostatní pole nastaví hodnotu všem cílům (lze poslat i holou hodnotu)
POST /doryo-api/v1/admin/bulk/Eshop:Admin:Customer/customers
{"ids": ["a1", "b2"], "values": {"pricelists": {"add": ["<id ceníku>"]}}, "dryRun": true}

Cíle jsou ids, nebo filter (filtry gridu) — jedno z toho. Náhled vrátí targets (počet, ukázka 20 záznamů), changes, requiresConfirm a token. Token je vázaný na otisk cílové množiny, změny i stav cílů v měněných polích — když filtr mezitím vrací jiné záznamy nebo cílům někdo změnil ceníky, je zápis 409. Nad bulkConfirmFrom cílů chce ostrý zápis "confirm": true. Ostrý zápis posílá vždycky výčet id (bulkType = selected), i když byl zadaný filtr — admin by filtr vyhodnotil znovu a zapsal do množiny, kterou nikdo neschválil. Výčet ids se ověřuje proti zdroji gridu, tedy proti tomu, co identita vidí; cizí nebo neexistující id je 404.

Chybové stavy

kód kdy
400 vadný vstup: neznámé pole, filtr nebo režim, chybějící confirm, add/remove s prázdným seznamem, obchodník poslaný dvakrát jinak
403 role nesmí (stránka, 777, pole jen ke čtení), administrace stránku neotevřela (záznam identita nevidí), obchodník bez účtu, požadavek za obchodníka bez merchantAdministrators
404 neznámý formulář/akce/grid ve whitelistu, neexistující záznam nebo obchodník
409 chybí / vypršel / nesedí previewToken, záznam nebo cíle se od náhledu změnily
422 formulář neprošel validací, hodnota mimo číselník, administrace zápis nebo akci odmítla (flash zpráva typu chyba / varování)
503 chybí účet agenta, API nemá token, shop nemá sloupec obchodníka

Bezpečnost

  • Oprávnění drží administrace, ne brána: role identity v Administrace → Role, pole vypínaná podle role, filtry dat podle přihlášeného. Brána nic z toho neobchází ani nedomýšlí.
  • 777 pro akce a hromadné úpravy navíc nad tím, co kontroluje administrace.
  • Token místo potvrzení v klientovi. Kolego potvrzuje zápis jednou za sezení a tělo neukazuje, takže souhlas s nástrojem je bianko šek. Bezpečnost proto stojí v API: bez tokenu z náhledu se nezapíše nic a s tokenem jen přesně to, co náhled ukázal, nad stejným stavem a za stejnou identitu. Token je bezstavový HMAC podepsaný tokenem API.
  • Odmítnutí není úspěch. Administrace odmítnutí většinou hlásí flash zprávou a přesměrováním („Objednávku nelze stornovat"), ne výjimkou. Brána každou flash zprávu typu error / danger / warning po zápisu bere jako 422 a do auditu nic nepíše; při otevírání stránky jako 403.
  • CSRF: Nette 3.2 bere formulář i signál bez same-site původu jako útok a tiše přesměruje. Požadavek nechodí z prohlížeče a chrání ho Bearer token, takže brána po dobu průchodu presenterem vymění v DI kontejneru HTTP požadavek za same-site kopii (hlavička Sec-Fetch-Site: same-origin i cookie _nss) s tělem formuláře a parametry v adrese; po průchodu vrací původní. Kdyby presenter kopii nedostal, je to 500, ne tichý neúspěch.
  • Audit nese skutečného administrátora, viz Identita.
  • Log API (eshop-doryo-api) nese u zápisu jen velikost těla, nikdy obsah.

Rozšíření z projektu

  • Vlastní cesty (importy, kontroly, vlastní domény) do brány nepatří — projekt je přidá standardně přes doryoApi: endpoints: (viz README eshop-doryo-api).

  • PageDescriber — aby o nich model věděl, projekt je připíše k řádku stránky v mapě:

    final class ImportsDescriber implements DoryoAdmin\Gateway\PageDescriber
    {
        public function describe(string $presenter, array $row): array
        {
            if ($presenter === 'Eshop:Admin:Import') {
                $row['imports'] = ['status' => 'GET /v1/fb/imports — poslední běh každého importu'];
            }
    
            return $row;
        }
    }
    doryoAdmin:
        describers: [App\DoryoApi\ImportsDescriber]

    Volá se jen u stránek, na které identita smí; vlastní klíče nesmí kolidovat s forms, actions, bulk, grids.

  • Vlastní IdentityResolver přes identityResolver:.

  • Gateways jdou volat i z projektového kódu: PresenterRunner::useIdentity() místo identify().

Omezení

  • Upload souborů brána neumí (pole typu file jsou readOnly); u uloženého obrázku nebo souboru se při zápisu pošle jeho současný název, takže zůstává.
  • Mazání záznamů a inline editace v gridech (addColumnInput*, „Uložit vše") cesty nemají.
  • Pole s datem: posílá se jako Y-m-d H:i:s; formát je potřeba ověřit, až takový formulář přibude.
  • Selecty plněné AJAXem mají v číselníku jen počet položek — posílá se id.
  • Signály se do whitelistu píšou ručně: handle* nenese popis parametrů ani významu.
  • Přihlášení identity nastartuje session, odpověď tak nese Set-Cookie. Po požadavku se odhlašuje.
  • Sloupce gridu se čtou reflexí Grid\Datagrid::$columns (getter chybí i v liquiddesign/grid 1.0) — při aktualizaci gridu zkontrolovat.
  • Stav cílů hromadné úpravy se hlídá jen u polí, která jsou sloupcem nebo vztahem N:N tabulky entity; vstupy z kontejnerů a jazykové mutace v otisku nejsou.
  • Akce bez kontroly přístupu v presenteru (signál, který si záznam neověří) projde s rolí, která má na stránce 777 — stejně jako v administraci, kde jde signál zavolat odkazem.

Vývoj

composer install
composer check-code   # phpcs (liquiddesign/codestyle) + phpstan level 6