mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(ui): show alert permission errors (#11629)
This commit is contained in:
@@ -4,6 +4,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.31.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Controlled `402` and `403` Server Action error messages for alert seed and mutation flows [(#11629)](https://github.com/prowler-cloud/prowler/pull/11629)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Radio button no longer shifts vertically when selected [(#11608)](https://github.com/prowler-cloud/prowler/pull/11608)
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/app/(prowler)/alerts/_types";
|
||||
import type { ProviderProps } from "@/types/providers";
|
||||
|
||||
import { ALERTS_PERMISSION_ERROR } from "../../_lib/alert-errors";
|
||||
import { AlertFormModal } from "../alert-form-modal";
|
||||
|
||||
const recipientsActionMocks = vi.hoisted(() => ({
|
||||
@@ -726,6 +727,28 @@ describe("AlertFormModal", () => {
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should show the manage alerts permission error when save seed is forbidden", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
alertsActionMocks.seedAlertRule.mockResolvedValue({
|
||||
error: "You do not have permission to perform this action.",
|
||||
status: 403,
|
||||
});
|
||||
mockRecipientsList();
|
||||
renderCreateModal({ editingAlert: createEditingAlert() });
|
||||
|
||||
// When
|
||||
await user.click(screen.getByRole("button", { name: /^save$/i }));
|
||||
|
||||
// Then
|
||||
expect(await screen.findByText(ALERTS_PERMISSION_ERROR)).toBeVisible();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
"Apply at least one alert-compatible Findings filter.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hydrate advanced edit mode filters and normalize them on save", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
AlertFormValues,
|
||||
} from "@/app/(prowler)/alerts/_types/alert-form";
|
||||
|
||||
import { ALERTS_PERMISSION_ERROR } from "../../_lib/alert-errors";
|
||||
import { AlertsManager } from "../alerts-manager";
|
||||
|
||||
const actionMocks = vi.hoisted(() => ({
|
||||
@@ -316,11 +317,7 @@ describe("AlertsManager", () => {
|
||||
await user.click(screen.getByRole("button", { name: /submit alert/i }));
|
||||
|
||||
// Then
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"You don't have permission to manage alerts. Ask an administrator to update your role.",
|
||||
),
|
||||
).toBeVisible();
|
||||
expect(await screen.findByText(ALERTS_PERMISSION_ERROR)).toBeVisible();
|
||||
expect(toastMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -346,6 +343,32 @@ describe("AlertsManager", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a manage alerts permission toast for disable 403 errors", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const alert = makeAlert(true);
|
||||
actionMocks.disableAlert.mockResolvedValue({
|
||||
error: "You do not have permission to perform this action.",
|
||||
status: 403,
|
||||
});
|
||||
renderManager([alert]);
|
||||
|
||||
// When
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: /actions for enabled alert/i }),
|
||||
);
|
||||
await user.click(screen.getByRole("menuitem", { name: /disable/i }));
|
||||
|
||||
// Then
|
||||
await waitFor(() =>
|
||||
expect(toastMock).toHaveBeenCalledWith({
|
||||
variant: "destructive",
|
||||
title: "Alert update failed",
|
||||
description: ALERTS_PERMISSION_ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a success toast after enabling an alert", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
getEmptyAlertFormDefaults,
|
||||
getFindingsFiltersFromAlertCondition,
|
||||
} from "../_lib/alert-adapter";
|
||||
import { getAlertMutationError } from "../_lib/alert-errors";
|
||||
import { alertFormSchema } from "../_lib/alert-form-schema";
|
||||
import type {
|
||||
AlertFormSubmitResult,
|
||||
@@ -408,7 +409,7 @@ const AlertFormModalContent = ({
|
||||
if (seedResult?.error) {
|
||||
setPreview({
|
||||
status: "error",
|
||||
error: ALERT_SEED_ERROR,
|
||||
error: getAlertMutationError(seedResult, ALERT_SEED_ERROR),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -452,7 +453,9 @@ const AlertFormModalContent = ({
|
||||
: null;
|
||||
if (seedResult?.error) {
|
||||
setPreview(null);
|
||||
setErrors({ root: ALERT_SEED_ERROR });
|
||||
setErrors({
|
||||
root: getAlertMutationError(seedResult, ALERT_SEED_ERROR),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { ScanEntity } from "@/types";
|
||||
import type { ProviderProps } from "@/types/providers";
|
||||
|
||||
import { toAlertPayload } from "../_lib/alert-adapter";
|
||||
import { getAlertMutationError } from "../_lib/alert-errors";
|
||||
import type {
|
||||
AlertFormSubmitResult,
|
||||
AlertFormValues,
|
||||
@@ -49,8 +50,6 @@ interface AlertsManagerProps {
|
||||
|
||||
const ALERTS_FINDINGS_HREF =
|
||||
"/findings?filter[muted]=false&filter[status__in]=FAIL";
|
||||
const ALERTS_PERMISSION_ERROR =
|
||||
"You don't have permission to manage alerts. Ask an administrator to update your role.";
|
||||
|
||||
export const AlertsManager = ({
|
||||
alerts,
|
||||
@@ -113,7 +112,7 @@ export const AlertsManager = ({
|
||||
if (result?.error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: result.status === 403 ? ALERTS_PERMISSION_ERROR : result.error,
|
||||
error: getAlertMutationError(result),
|
||||
};
|
||||
}
|
||||
toast({
|
||||
@@ -134,7 +133,7 @@ export const AlertsManager = ({
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Alert update failed",
|
||||
description: result.error,
|
||||
description: getAlertMutationError(result),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -154,7 +153,7 @@ export const AlertsManager = ({
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Alert delete failed",
|
||||
description: result.error,
|
||||
description: getAlertMutationError(result),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ACTION_ERROR_STATUS,
|
||||
getActionErrorMessage,
|
||||
} from "@/lib/action-errors";
|
||||
|
||||
export const ALERTS_PERMISSION_ERROR =
|
||||
"You don't have permission to manage alerts. Ask an administrator to update your role.";
|
||||
|
||||
interface AlertActionErrorResult {
|
||||
error: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export const getAlertMutationError = (
|
||||
result: AlertActionErrorResult,
|
||||
fallback = result.error,
|
||||
): string =>
|
||||
getActionErrorMessage(
|
||||
{ ...result, error: fallback },
|
||||
{
|
||||
messages: {
|
||||
[ACTION_ERROR_STATUS.FORBIDDEN]: ALERTS_PERMISSION_ERROR,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
ACTION_ERROR_MESSAGES,
|
||||
ACTION_ERROR_STATUS,
|
||||
getActionErrorMessage,
|
||||
} from "./action-errors";
|
||||
|
||||
describe("getActionErrorMessage", () => {
|
||||
it("should use the default permission error for forbidden responses", () => {
|
||||
// Given
|
||||
const result = {
|
||||
error: "You do not have permission to perform this action.",
|
||||
status: ACTION_ERROR_STATUS.FORBIDDEN,
|
||||
};
|
||||
|
||||
// When
|
||||
const message = getActionErrorMessage(result);
|
||||
|
||||
// Then
|
||||
expect(message).toBe(ACTION_ERROR_MESSAGES[ACTION_ERROR_STATUS.FORBIDDEN]);
|
||||
});
|
||||
|
||||
it("should use the default subscription error for payment-required responses", () => {
|
||||
// Given
|
||||
const result = {
|
||||
error: "Payment required.",
|
||||
status: ACTION_ERROR_STATUS.PAYMENT_REQUIRED,
|
||||
};
|
||||
|
||||
// When
|
||||
const message = getActionErrorMessage(result);
|
||||
|
||||
// Then
|
||||
expect(message).toBe(
|
||||
ACTION_ERROR_MESSAGES[ACTION_ERROR_STATUS.PAYMENT_REQUIRED],
|
||||
);
|
||||
});
|
||||
|
||||
it("should use a feature override for handled statuses", () => {
|
||||
// Given
|
||||
const result = {
|
||||
error: "You do not have permission to perform this action.",
|
||||
status: ACTION_ERROR_STATUS.FORBIDDEN,
|
||||
};
|
||||
const override = "You don't have permission to manage alerts.";
|
||||
|
||||
// When
|
||||
const message = getActionErrorMessage(result, {
|
||||
messages: {
|
||||
[ACTION_ERROR_STATUS.FORBIDDEN]: override,
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(message).toBe(override);
|
||||
});
|
||||
|
||||
it("should keep the API error for unhandled statuses", () => {
|
||||
// Given
|
||||
const result = {
|
||||
error: "Apply at least one alert-compatible Findings filter.",
|
||||
status: 400,
|
||||
};
|
||||
|
||||
// When
|
||||
const message = getActionErrorMessage(result);
|
||||
|
||||
// Then
|
||||
expect(message).toBe(result.error);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
export const ACTION_ERROR_STATUS = {
|
||||
PAYMENT_REQUIRED: 402,
|
||||
FORBIDDEN: 403,
|
||||
} as const;
|
||||
|
||||
export type ActionErrorStatus =
|
||||
(typeof ACTION_ERROR_STATUS)[keyof typeof ACTION_ERROR_STATUS];
|
||||
|
||||
export const ACTION_ERROR_MESSAGES = {
|
||||
[ACTION_ERROR_STATUS.PAYMENT_REQUIRED]:
|
||||
"Your subscription doesn't allow this action. Upgrade your plan or contact an administrator.",
|
||||
[ACTION_ERROR_STATUS.FORBIDDEN]:
|
||||
"You don't have permission to perform this action. Ask an administrator to update your role.",
|
||||
} as const satisfies Record<ActionErrorStatus, string>;
|
||||
|
||||
interface ActionErrorResult {
|
||||
error?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
interface GetActionErrorMessageOptions {
|
||||
messages?: Partial<Record<ActionErrorStatus, string>>;
|
||||
fallback?: string;
|
||||
}
|
||||
|
||||
const isActionErrorStatus = (
|
||||
status: number | undefined,
|
||||
): status is ActionErrorStatus =>
|
||||
status === ACTION_ERROR_STATUS.PAYMENT_REQUIRED ||
|
||||
status === ACTION_ERROR_STATUS.FORBIDDEN;
|
||||
|
||||
export const getActionErrorMessage = (
|
||||
result: ActionErrorResult,
|
||||
options: GetActionErrorMessageOptions = {},
|
||||
): string => {
|
||||
if (isActionErrorStatus(result.status)) {
|
||||
return (
|
||||
options.messages?.[result.status] ?? ACTION_ERROR_MESSAGES[result.status]
|
||||
);
|
||||
}
|
||||
|
||||
return result.error ?? options.fallback ?? "Oops! Something went wrong.";
|
||||
};
|
||||
Reference in New Issue
Block a user