Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions adminforth/modules/configValidator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
AdminForthConfig,
AdminForthResource,
IAdminForth, IConfigValidator,
import {
AdminForthConfig,
AdminForthResource,
IAdminForth, IConfigValidator,
AdminForthBulkAction,
AdminForthActionInput,
AdminForthInputConfig,
AdminForthConfigCustomization,
AdminForthResourceInput,
Expand Down Expand Up @@ -388,7 +389,7 @@ export default class ConfigValidator implements IConfigValidator {
});
}

validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): any[] {
validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): AdminForthActionInput[] {
if (!resInput.options?.actions) {
return [];
}
Expand Down Expand Up @@ -430,13 +431,18 @@ export default class ConfigValidator implements IConfigValidator {
action.showIn.showThreeDotsMenu = action.showIn.showThreeDotsMenu ?? false;
}

if (typeof action.allowed === 'boolean') {
const val = action.allowed;
action.allowed = () => val;
}
Comment on lines +434 to +437
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowed boolean normalization is only applied during validateConfig(), but plugins can mutate resource.options.actions later during modifyResourceConfig() (after validation). That means boolean allowed values introduced by plugins won't be normalized here; consider normalizing again after plugin activation (or ensure runtime checks handle boolean allowed consistently).

Copilot uses AI. Check for mistakes.

const shownInNonBulk = action.showIn.list || action.showIn.listThreeDotsMenu || action.showIn.showButton || action.showIn.showThreeDotsMenu;
if (shownInNonBulk && !action.action && !action.url) {
errors.push(`Resource "${res.resourceId}" action "${action.name}" has showIn enabled for non-bulk locations (list, listThreeDotsMenu, showButton, showThreeDotsMenu) but has no "action" or "url" handler. Either add an "action" handler or set those showIn flags to false.`);
}
});

return actions;
return actions as AdminForthActionInput[];
}

validateAndNormalizeResources(errors: string[], warnings: string[]): AdminForthResource[] {
Expand Down
34 changes: 24 additions & 10 deletions adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import { afLogger } from "./logger.js";
import { ADMINFORTH_VERSION, listify, md5hash, getLoginPromptHTML, hookResponseError } from './utils.js';

import AdminForthAuth from "../auth.js";
import { ActionCheckSource, AdminForthConfigMenuItem, AdminForthDataTypes, AdminForthFilterOperators, AdminForthResourceColumnInputCommon, AdminForthResourceFrontend, AdminForthResourcePages,
import { ActionCheckSource, AdminForthActionFront, AdminForthConfigMenuItem, AdminForthDataTypes, AdminForthFilterOperators, AdminForthResourceColumnInputCommon, AdminForthResourceFrontend, AdminForthResourcePages,
AdminForthSortDirections,
AdminUser, AllowedActionsEnum, AllowedActionsResolved,
AdminUser, AllowedActionsEnum, AllowedActionsResolved,
AnnouncementBadgeResponse,
GetBaseConfigResponse,
ShowInResolved} from "../types/Common.js";
Expand Down Expand Up @@ -1079,6 +1079,22 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
})
);

const allowedCustomActions = [];
if (resource.options.actions) {
await Promise.all(
resource.options.actions.map(async (action) => {
if (typeof action.allowed === 'function') {
const res = await action.allowed({ adminUser, standardAllowedActions: allowedActions });
if (res) {
allowedCustomActions.push(action);
}
} else {
allowedCustomActions.push(action);
}
Comment on lines +1086 to +1093
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

action.allowed is now typed as boolean | (fn), but this filtering treats any non-function (including false) as allowed and exposes the action to the frontend. This can bypass action-level authorization for configs/plugins that set allowed: false (plugins run after validateConfig, so the boolean->fn normalization may not apply). Handle boolean explicitly (e.g., skip when allowed === false, include when allowed === true/undefined, and only await when it's a function).

Copilot uses AI. Check for mistakes.
})
);
Comment on lines +1082 to +1095
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.all(...map(async ... allowedCustomActions.push(...) )) makes the returned actions order depend on async completion order rather than config order. If allowed checks have varying latency, the UI action ordering can become non-deterministic. Prefer building an array of results (e.g., map to boolean + filter by index) or for...of with await to preserve order.

Copilot uses AI. Check for mistakes.
}

// translate
const translateRoutines: Record<string, Promise<string>> = {};
translateRoutines.resLabel = tr(resource.label, `resource.${resource.resourceId}`);
Expand Down Expand Up @@ -1191,12 +1207,10 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
confirm: action.confirm ? translated[`bulkActionConfirm${i}`] : action.confirm,
})
),
actions: resource.options.actions?.map((action) => ({
...action,
id: action.id!,
hasBulkHandler: !!action.bulkHandler,
bulkHandler: undefined,
})),
actions: allowedCustomActions.map(({ bulkHandler, allowed, action: actionFn, ...rest }) => ({
...rest,
...(bulkHandler && { bulkHandler: true }),
})) as AdminForthActionFront[],
allowedActions,
}
}
Expand Down Expand Up @@ -2096,7 +2110,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
if (!action) {
return { error: await tr(`Action {actionId} not found`, 'errors', { actionId }) };
}
if (action.allowed) {
if (typeof action.allowed === 'function') {
const execAllowed = await action.allowed({ adminUser, standardAllowedActions: allowedActions });
if (!execAllowed) {
return { error: await tr(`Action "{actionId}" not allowed`, 'errors', { actionId: action.name }) };
Expand Down Expand Up @@ -2148,7 +2162,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
if (!action.bulkHandler) {
return { error: await tr(`Action "{actionId}" has no bulkHandler`, 'errors', { actionId }) };
}
if (action.allowed) {
if (typeof action.allowed === 'function') {
const execAllowed = await action.allowed({ adminUser, standardAllowedActions: allowedActions });
if (!execAllowed) {
return { error: await tr(`Action "{actionId}" not allowed`, 'errors', { actionId: action.name }) };
Expand Down
3 changes: 1 addition & 2 deletions adminforth/spa/src/components/ThreeDotsMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ import { useRoute, useRouter } from 'vue-router';
import CallActionWrapper from '@/components/CallActionWrapper.vue'
import { ref, type ComponentPublicInstance, onMounted, onUnmounted } from 'vue';
import type { AdminForthActionFront, AdminForthBulkActionFront, AdminForthComponentDeclarationFull } from '@/types/Common';
import type { AdminForthActionInput } from '@/types/Back';
import { Spinner } from '@/afcl';

const { list, alert} = useAdminforth();
Expand Down Expand Up @@ -137,7 +136,7 @@ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
}
}

async function handleActionClick(action: AdminForthActionInput, payload: any) {
async function handleActionClick(action: AdminForthActionFront, payload: any) {
list.closeThreeDotsDropdown();
await executeCustomAction({
actionId: action.id,
Expand Down
2 changes: 1 addition & 1 deletion adminforth/spa/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ export async function executeCustomBulkAction({
try {
const action = resource?.options?.actions?.find((a: any) => a.id === actionId) as AdminForthActionFront | undefined;

if (action?.hasBulkHandler && action?.showIn?.bulkButton) {
if (action?.bulkHandler && action?.showIn?.bulkButton) {
const result = await callAdminForthApi({
path: '/start_custom_bulk_action',
method: 'POST',
Expand Down
2 changes: 1 addition & 1 deletion adminforth/spa/src/views/ListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ async function startCustomBulkActionInner(actionId: string | number) {
const successResults = results.filter(r => r?.successMessage);
if (successResults.length > 0) {
alert({
message: action?.bulkSuccessMessage ? action.bulkSuccessMessage : action?.hasBulkHandler ? successResults[0].successMessage : `${successResults.length} out of ${results.length} items processed successfully`,
message: action?.bulkSuccessMessage ? action.bulkSuccessMessage : action?.bulkHandler ? successResults[0].successMessage : `${successResults.length} out of ${results.length} items processed successfully`,
variant: 'success'
});
}
Expand Down
4 changes: 2 additions & 2 deletions adminforth/types/Back.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,10 +1389,10 @@ export interface AdminForthActionInput {
showThreeDotsMenu?: boolean,
bulkButton?: boolean,
};
allowed?: (params: {
allowed?: boolean | ((params: {
adminUser: AdminUser;
standardAllowedActions: AllowedActions;
}) => boolean;
}) => boolean | Promise<boolean>);
url?: string;
bulkHandler?: (params: {
adminforth: IAdminForth;
Expand Down
2 changes: 1 addition & 1 deletion adminforth/types/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export type FieldGroup = {

export interface AdminForthActionFront extends Omit<AdminForthActionInput, 'id' | 'bulkHandler' | 'action' | 'allowed'> {
id: string;
hasBulkHandler?: boolean;
bulkHandler?: boolean;
}

export interface AdminForthBulkActionFront extends Omit<AdminForthBulkActionCommon, 'id'> {
Expand Down