From 71073bc8a2c5a1f2030a49bbe351bf2baad1a480 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 4 Oct 2026 23:53:34 +0200 Subject: [PATCH] feat(12.1-01): cabana.ForbiddenError answers a refused write with 403 - hooks and bulk, record, toolbar and widget actions may return it - 403 forbidden with the localized message and field details; the write's transaction is rolled back; other errors stay the opaque 500 - form shows a refused save as a persistent banner and keeps the values; a refused delete is a toast - smoke tests, OpenAPI notes, dist, README, docs --- admin/openapi/admin.json | 12 +- admin/src/api/schema.d.ts | 12 +- admin/src/components/form/FormErrorBanner.vue | 23 ++- admin/src/views/FormView.vue | 14 +- admin/tests/smoke/actions.smoke.test.ts | 142 +++++++++++++++++- admin/tests/smoke/edit.smoke.test.ts | 7 +- docs/backend/admin-controllers.md | 14 ++ docs/backend/users-and-permissions.md | 4 +- .../{index-BgVbexs3.js => index-8CEYdgqp.js} | 8 +- modules/boardwalk/dist/index.html | 2 +- modules/cabana/README.md | 2 + modules/cabana/actions.go | 10 +- modules/cabana/admin_openapi.go | 12 +- modules/cabana/crud.go | 88 ++++++++++- modules/cabana/phase101_actions_test.go | 5 + modules/cabana/phase121_actions_test.go | 116 ++++++++++++++ modules/cabana/phase121_fixture_test.go | 33 ++++ modules/cabana/relation.go | 10 +- modules/cabana/relation_child.go | 12 +- .../cabana/testdata/roster/lang/en/lang.yaml | 3 + .../cabana/testdata/roster/lang/pl/lang.yaml | 3 + modules/phrasebook/backend/lang/en/lang.yaml | 1 + modules/phrasebook/backend/lang/pl/lang.yaml | 1 + 23 files changed, 486 insertions(+), 48 deletions(-) rename modules/boardwalk/dist/assets/{index-BgVbexs3.js => index-8CEYdgqp.js} (53%) diff --git a/admin/openapi/admin.json b/admin/openapi/admin.json index c68245e..9b44f46 100644 --- a/admin/openapi/admin.json +++ b/admin/openapi/admin.json @@ -2662,7 +2662,7 @@ } } }, - "description": "Forbidden" + "description": "also returned when controller code refuses the write; details may name fields" }, "404": { "content": { @@ -2767,7 +2767,7 @@ } } }, - "description": "Forbidden" + "description": "also returned when controller code refuses the write; details may name fields" }, "404": { "content": { @@ -2892,7 +2892,7 @@ } } }, - "description": "Forbidden" + "description": "also returned when controller code refuses the write; details may name fields" }, "404": { "content": { @@ -3820,7 +3820,7 @@ } } }, - "description": "Forbidden" + "description": "also returned when controller code refuses the write; details may name fields" }, "404": { "content": { @@ -4032,7 +4032,7 @@ } } }, - "description": "Forbidden" + "description": "also returned when controller code refuses the write; details may name fields" }, "404": { "content": { @@ -4156,7 +4156,7 @@ } } }, - "description": "Forbidden" + "description": "also returned when controller code refuses the write; details may name fields" }, "404": { "content": { diff --git a/admin/src/api/schema.d.ts b/admin/src/api/schema.d.ts index d2a69de..3da0977 100644 --- a/admin/src/api/schema.d.ts +++ b/admin/src/api/schema.d.ts @@ -737,7 +737,7 @@ export interface paths { "application/json": components["schemas"]["cabana.ErrorEnvelope"]; }; }; - /** @description Forbidden */ + /** @description also returned when controller code refuses the write; details may name fields */ 403: { headers: { [name: string]: unknown; @@ -821,7 +821,7 @@ export interface paths { "application/json": components["schemas"]["cabana.ErrorEnvelope"]; }; }; - /** @description Forbidden */ + /** @description also returned when controller code refuses the write; details may name fields */ 403: { headers: { [name: string]: unknown; @@ -919,7 +919,7 @@ export interface paths { "application/json": components["schemas"]["cabana.ErrorEnvelope"]; }; }; - /** @description Forbidden */ + /** @description also returned when controller code refuses the write; details may name fields */ 403: { headers: { [name: string]: unknown; @@ -1709,7 +1709,7 @@ export interface paths { "application/json": components["schemas"]["cabana.ErrorEnvelope"]; }; }; - /** @description Forbidden */ + /** @description also returned when controller code refuses the write; details may name fields */ 403: { headers: { [name: string]: unknown; @@ -1776,7 +1776,7 @@ export interface paths { "application/json": components["schemas"]["cabana.ErrorEnvelope"]; }; }; - /** @description Forbidden */ + /** @description also returned when controller code refuses the write; details may name fields */ 403: { headers: { [name: string]: unknown; @@ -1866,7 +1866,7 @@ export interface paths { "application/json": components["schemas"]["cabana.ErrorEnvelope"]; }; }; - /** @description Forbidden */ + /** @description also returned when controller code refuses the write; details may name fields */ 403: { headers: { [name: string]: unknown; diff --git a/admin/src/components/form/FormErrorBanner.vue b/admin/src/components/form/FormErrorBanner.vue index 1b4371f..75d49c8 100644 --- a/admin/src/components/form/FormErrorBanner.vue +++ b/admin/src/components/form/FormErrorBanner.vue @@ -5,8 +5,13 @@ import { t, tc } from '../../app/i18n' // 422 banner (design screen 4): "Nie udało się zapisać. Popraw N pola…". // Messages of keys that are not form fields are listed here, so no server -// message is lost. -const props = defineProps<{ errors: Record; fieldNames: string[] }>() +// message is lost. With `forbidden` (UI-SPEC S6: a save the server refused +// with 403) the same geometry shows that text instead; it stays until the +// next save attempt, and field messages still render on their fields. +const props = withDefaults( + defineProps<{ errors: Record; fieldNames: string[]; forbidden?: string | null }>(), + { forbidden: null }, +) const count = computed(() => Object.keys(props.errors).length) const orphans = computed(() => @@ -18,7 +23,19 @@ const orphans = computed(() =>