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: 93

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.10.1 2026-11-07 16:39 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 (i se soubory), spouští její akce, dělá hromadné úpravy a maže z gridů — 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
maxUploadBytes 0 strop nahrávaného souboru v bajtech; 0 = brána soubory nenahrává (pole souborů jsou jen ke čtení)
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
emailAdministrators false požadavek s hlavičkou X-Doryo-Email jede pod administrátorem s tímtéž loginem, viz Identita
administratorEmails [] párování e-mail → login účtu administrace, kde login není e-mail
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:
latte true makra n:kolego a n:kolego-pole se přidají do Latte, viz Značky na webu
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í a značkám), createNote, hidden (pole, která brána nevydá ani nezmění), customerEmail (false), znacka (druh značky na webu), bublina (pole do bubliny u prvku), createParameter (parametr zakládací akce s nadřazeným záznamem), item (formulář jedné položky záznamu, viz níž), popisky (popisky polí bez vlastního popisku, viz níž)
actions presenter, signal (bez handle), action (default), label, params (jména parametrů signálu), destructive (false), entity, idParam, customerEmail (false), file (HTML jméno souboru, který signál čte z požadavku), fileImage (false), item (akce nad položkou záznamu), cache (false — akce jen maže cache webu a data nemění; bez params, entity, idParam, file, item a destructive, v mapě "cache": true)
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
delete presenter, entity (entita řádků — podle ní brána popíše a po smazání ověří, co zmizelo), grid (grid), action (default), label, description (co smazání znamená), parameter (seznam patří k záznamu), tabParameter (persistentní parametr se záložkou)

* povinné

customerEmail: true u formuláře nebo akce říká, že uložení či spuštění pošle e-mail zákazníkovi (změna stavu objednávky, storno, odkaz na nové heslo). Brána ho vrací v náhledu i v mapě stránek a Kolego to na kartě řekne nahoře.

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
        -
            # cache webu: po zápisu přes bránu se změna na webu ukáže až po smazání (Kolego to nabídne)
            presenter: 'Eshop:Admin:Scripts'
            signal: clearCache
            label: 'Smazat cache webu'
            cache: true

Značky na webu (Kolego nad e-shopem)

Rozšíření Kolego v prohlížeči umí e-shop upravovat přímo na webu: co jde v administraci změnit, dostane rámeček a klik otevře bublinu s poli formuláře (Kolego nad e-shopem, doryo docs/156). Aby vědělo, který záznam prvek je, označí ho šablona makrem:

<article n:kolego="$product">                       {* data-kolego="produkt:<pk>" — druh podle entity *}
<div n:kolego="'slide', $slide->getPK()">           {* data-kolego="slide:<pk>"   — druh výslovně *}
<h3 n:kolego-pole="name">{$product->name}</h3>      {* data-kolego-pole="name"    — které pole prvek ukazuje *}

Druh je značka formuláře ve whitelistu a bublina říká, která pole rozšíření ukáže u prvku (ostatní jsou za „Celý formulář"):

doryoAdmin:
    forms:
        -
            presenter: 'Eshop:Admin:Product'
            parameter: product
            entity: App\Eshop\DB\Product
            znacka: produkt
            bublina: [name, hidden, ribbons]

Druh smí mít jen jeden formulář (jinak start spadne). Mapa administrace (GET /v1/admin/pages) vrací znacky: druh → formulář, parametr a pole bubliny — jen formuláře, na které identita smí. Atribut nic neodemyká; zákazník vidí jen data atribut. Makro se do Latte přidá samo (latte: true); projekt, který chce Latte zapojit sám, nastaví latte: false a přidá DoryoAdmin\Latte\KolegoExtension.

Úč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.
  • Člověk podle e-mailu (emailAdministrators: true): hlavičku X-Doryo-Email posílá Kolego jen za člověka, jehož e-mail potvrdil Google, a jen s platným tokenem. Brána se přihlásí jako administrátor, jehož účet má login rovný tomu e-mailu (nebo login z administratorEmails). Platí jeho role a do auditu jde on. Bez hlavičky (agent, automatizace) jede požadavek dál výchozí cestou — servisní účet, případně administrátor obchodníka.
    • člověk bez účtu v administraci → 403 s návodem (servisní účet nepoužije — má širší práva),
    • vypnutý účet → 403.
  • Vlastní resolver: identityResolver: App\Doryo\MyResolver.

Audit (admin_changelog): user = <login administrátora> via Kolego (<actor>), entity = presenter, objectId = id záznamu, type = form:<akce>, signal:<signál>, bulk:<grid>:<počet> nebo delete:<grid>:<počet smazaných>. 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, hromadné úpravy a mazání z whitelistu; stránky z whitelistu, které v menu nejsou (fotky produktů), ve skupině „Mimo menu". Od 0.8 bez parametrů krátký přehled (jen jména), ?presenter= celá jedna stránka, ?q= celé stránky, kde se najdou všechna slova (bez diakritiky), ?detail=1 celá mapa jako do 0.7
POST /v1/admin/forms/{presenter}/{form} s records od 0.9 víc záznamů týmž formulářem: [{id, values}] (do 100), jeden náhled a token, ostrý zápis záznam po záznamu skutečným formulářem; co administrace neuloží, vrátí failed (není atomické)
kontrola po uložení od 0.9 formulář po ostrém zápisu otevře záznam znovu a pole, která uložení tiše přeskočilo (MC Profi: „Doporučený" v seznamu viditelnosti, kde produkt není aktivní), vrátí v notSaved — u jednoho zápisu i u každého záznamu z records
soubor z e-shopu od 0.10 jde místo {name, content} poslat {"eshop": "<adresa nebo /userfiles/…>"} — soubor, který už v e-shopu je (fotka produktu), brána vezme z disku (userFilesDir, jinak parametr userDir, jinak %wwwDir%/userfiles); jen uvnitř té složky, limity jako u nahrávání
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
POST /v1/admin/delete/{presenter}/{grid} smazání ids tlačítkem „Smazat" gridu; náhled, pak ostrý běh s tokenem a "confirm": true

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": []
}

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.

  • Zakládání pod nadřazeným záznamem (createParameter, kupón akce: couponsCreate s discount): schéma GET …?parent=<id>, náhled i zápis nesou "parent" a token je na něj vázaný.

  • Formulář jedné položky záznamu (item): administrace některé položky edituje komponentou s id položky ve jménu (detailOrderItemForm-<id> je Multiplier). Ve whitelistu je jméno se zástupkou form: 'detailOrderItemForm-{item}' a item: { param: item, entity: Eshop\DB\PackageItem, parent: package.fk_order, label: cartItem.productName }. Schéma s id záznamu bez item vrátí seznam položek, s item jejich formulář; zápis nese id i item. Brána ověří, že položka k záznamu patří (parent = sloupec s id záznamu, i přes vztah) — komponenta ani signál to samy nehlídají.

  • Select bez prázdné volby a bez hodnoty dostane první možnost, jak by ji odeslal prohlížeč (jinak „Vyberte prosím platnou možnost" u podmínek kupónu).

  • Jen vybraná pole (0.6): GET …?id=<id>&fields=prices-*-price,name-cs — jména oddělená čárkou, * = cokoli. Formulář produktu má přes tisíc polí; odpověď nese fieldsFilter: { vzor, vybrano, celkem }.

  • Popisky polí bez popisku (0.6, popisky ve whitelistu): pole pod ceníkem, seznamem nebo skladem formulář skládá z id záznamu (prices-<ceník>-price) a popisek nemají. Vzor se zástupkou {id} → předpona a entita, jméno se vezme z name (jinak code) záznamu — jednou za entitu a požadavek. Platí ve schématu i v diff; popisek z formuláře vyhrává.

    popisky:
        'prices-{id}-price': ['Cena bez DPH', Eshop\DB\Pricelist]          # „Cena bez DPH · KZ – Denbraven"
        'visibility-list_{id}-recommended': ['Doporučený', Eshop\DB\VisibilityList]
  • Záložka presenteru (0.6, tabParameter, tabColumn, tabFromField): některé presentery berou při uložení údaj z persistentního parametru v adrese — kategorie strom z tab ($values['type'] = $presenter->tab). Bez něj ostrý zápis spadl, nebo by záznam přestěhoval do jiného stromu, a náhled to nepoznal (obsluhu uložení nespouští). Brána záložku pošle jako prohlížeč: u úpravy ze sloupce záznamu, u založení z tab v těle (schéma ?tab=), jinak z nadřazeného záznamu.

    tabParameter: tab        # persistentní parametr presenteru
    tabColumn: type          # sloupec záznamu s hodnotou záložky
    tabFromField: ancestor   # u založení: záložka nadřazeného záznamu z tohoto pole
  • Spadlé uložení (0.6): výjimka z obsluhy formuláře jde do logu (Tracy, doryo-admin) a volající dostane 500 s tím, co spadlo („Uložení formuláře „Kategorie" v administraci spadlo (DriverException): …"), ne jen „Požadavek se nepodařilo zpracovat".

  • Varování náhledu už nevypisují všechna pole jen ke čtení (0.6): zápis, který na ně sáhne, skončí 403 dřív a schéma je má označená readOnly — v náhledu byla jen šum (u produktu ~640 cen s DPH).

Akce

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

Akce nad položkou záznamu (item jako u formulářů, param = parametr signálu s id položky — odebrání položky objednávky toggleDeleteOrderItem s itemId) ověří, že položka patří k záznamu z idParam; náhled i výsledek ji vrátí v item.

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.

Nahrávání souborů

Jen s maxUploadBytes > 0. Soubor jde v JSON těle jako {"name": "<název>", "content": "<base64>"}:

  • pole souboru ve formuláři (ve schématu má upload místo readOnly) v "files": {"<pole>": {…}} vedle values. Nový soubor nahradí ten dnešní — brána v tom případě neposílá název dnešního (u UploadImage by mu upload() dal přednost). Výběr obrázku z liquiddesign/forms a pole s pravidlem Form::Image berou jen obrázek (podle obsahu, jinak 422);
  • akce, která soubor bere (file: ve whitelistu, v mapě stránek file), v "file": {…} — například handleDropzoneUploadPhoto produktu (file: file, fileImage: true); JSON, kterým signál odpoví, je v response.

Náhled vrátí u souboru file (název, velikost, typ podle obsahu, rozměry obrázku, sha256) a token se váže na obsah: ostrý zápis s jiným souborem je 409. Soubor jde do administrace jako FileUpload nad dočasným souborem — do aplikačního požadavku (formulář) i do HTTP požadavku (signál) — a dočasné soubory se na konci požadavku mažou.

Mazání

POST /doryo-api/v1/admin/delete/Eshop:Admin:Producer/grid
{"ids": ["a1"], "dryRun": true}

Maže se tlačítkem „Smazat" nad vybranými řádky (AdminGrid::addButtonDeleteSelected()) — brána odešle formulář gridu s __selector a deleteAll jako prohlížeč, takže platí podmínka projektu (systémové stránky), úklid v onDelete (fotky a soubory produktu) i omezení zdroje gridu. Grid bez toho tlačítka (jen koš u řádku) brána mazat neumí: akce řádku má v gridu jen pořadové číslo.

Seznam, který patří k záznamu nebo má záložky, bere v těle id a tab stejně jako jeho čtení (GET /v1/admin/grids/…). Náhled vrátí records (počet, ukázka 20 záznamů) a token vázaný na otisk množiny i na id a tab; ostrý běh chce token i "confirm": true vždy, ne až od prahu. Smazání chce na stránce „Plný přístup" (777). addButtonDeleteSelected hlásí „Provedeno" i tehdy, když část záznamů nesmazal (cizí klíč, podmínka) — brána proto po běhu zjistí, co v tabulce zůstalo: deleted, deletedIds, remaining, complete. Nesmaže-li administrace nic, je to 422.

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í

  • Nahrávání je vypnuté, dokud ho e-shop nezapne (maxUploadBytes); bez nového souboru se u uloženého obrázku nebo souboru při zápisu pošle jeho současný název, takže zůstává. Mazání fotek z galerie (signály delete…) brána nedělá.
  • Mazání jen z gridů s tlačítkem „Smazat" nad výběrem; koš u jednoho řádku (addColumnActionDelete bez addButtonDeleteSelected) 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