feat(19.7-01): account contributions, prompted record actions and backend subject events

- pact.HasAccountContributions compiled at Activate; GET /account/contributions
  and POST /account/contributions/{name}/actions/{action} behind interactiveOnly
  (AccountContributionView, AccountContributionActionView, AccountActionResultView)
- RecordAction.prompt (RecordActionPrompt) for a pact.AdminRecordAction with a
  Prompt; the record action route accepts payload only for such an action
  (trimmed JSON string, non-empty, capped at max_length) and hands it to Run as
  Value through CRUDService.RecordActionWithValue; a prompt without a label
  fails boot
- fireSubjectChanged: pact.MCPSubjectChanged for revoked tokens, Administrators
  saves that changed role, permissions or superuser, Roles saves that changed
  grants, deactivation and deletion
- phrases backend::lang.form.action_prompt_required/_too_long (en, pl)
- smoke tests TestAccountContributionsSmoke and TestRecordActionPromptSmoke;
  TestRecordActionPromptRun on Postgres; route inventories, CSRF walk and
  OpenAPI conformance cases extended for the two routes
- regenerated admin/openapi/admin.json and admin/src/api/schema.d.ts; cabana
  README, docs/backend/{admin-controllers,users-and-permissions}.md
This commit is contained in:
Jakub Zych
2026-10-22 16:31:29 +02:00
parent 6887abf575
commit 9f14cf603c
28 changed files with 1431 additions and 20 deletions

View File

@@ -210,6 +210,152 @@ export interface paths {
patch?: never;
trace?: never;
};
"/account/contributions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List My account contributions
* @description Lists the sections plugins contribute to the signed-in admin's My account page: label, description, the status for this admin and the actions they may run that apply to them. Empty when no plugin contributes. Personal API tokens are refused (403 interactive_only).
*/
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-array_cabana_AccountContributionView"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/account/contributions/{name}/actions/{action}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Run a My account contribution action
* @description Runs one action of a contributed section for the signed-in admin. The action's permissions are checked (403) and its Applies rule again (409 when it no longer applies); the answer is the action's message and an optional URL to open. Personal API tokens are refused (403 interactive_only).
*/
post: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Section name */
name: string;
/** @description Action name */
action: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-cabana_AccountActionResultView"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/account/preferences": {
parameters: {
query?: never;
@@ -5362,6 +5508,27 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
"cabana.AccountActionResultView": {
message: string;
url: string;
};
"cabana.AccountContributionActionView": {
confirm: string;
danger: boolean;
label: string;
name: string;
};
"cabana.AccountContributionView": {
actions: components["schemas"]["cabana.AccountContributionActionView"][];
description: string;
label: string;
name: string;
/**
* @description Status is the section's line for the signed-in admin; "" when the
* section has none.
*/
status: string;
};
"cabana.AdminAPIToken": {
created_at: string;
expired: boolean;
@@ -5569,6 +5736,10 @@ export interface components {
scripts: string[];
styles: string[];
};
"cabana.Envelope-array_cabana_AccountContributionView": {
data: components["schemas"]["cabana.AccountContributionView"][];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-array_cabana_AdminAPIToken": {
data: components["schemas"]["cabana.AdminAPIToken"][];
meta: components["schemas"]["cabana.SuccessMeta"];
@@ -5593,6 +5764,10 @@ export interface components {
data: components["schemas"]["cabana.SettingsEntry"][];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_AccountActionResultView": {
data: components["schemas"]["cabana.AccountActionResultView"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_AdminAPIToken": {
data: components["schemas"]["cabana.AdminAPIToken"];
meta: components["schemas"]["cabana.SuccessMeta"];
@@ -6164,6 +6339,18 @@ export interface components {
danger?: boolean;
label: string;
name: string;
/**
* @description Prompt is present only for an action that asks for one text value in
* its confirm dialog (pact.AdminRecordAction.Prompt); the admin posts the
* value as the body key payload, a JSON string.
*/
prompt?: components["schemas"]["cabana.RecordActionPrompt"];
};
"cabana.RecordActionPrompt": {
comment?: string;
label: string;
max_length: number;
placeholder?: string;
};
"cabana.RecordEnvelope": {
data: components["schemas"]["cabana.AdminRecord"];