-
+
+
+
Update failed
@@ -89,10 +95,14 @@ onMounted(async () => {
Email notifications
- Configure
+
+ Configure
+
- Send test notification
+
+ Send test notification
+
diff --git a/src/Frontend/src/components/configuration/RetryRedirects.vue b/src/Frontend/src/components/configuration/RetryRedirects.vue
index 4904439aad..b657f12fe8 100644
--- a/src/Frontend/src/components/configuration/RetryRedirects.vue
+++ b/src/Frontend/src/components/configuration/RetryRedirects.vue
@@ -11,11 +11,16 @@ import type Redirect from "@/resources/Redirect";
import RetryRedirectEdit, { type RetryRedirect } from "@/components/configuration/RetryRedirectEdit.vue";
import FAIcon from "@/components/FAIcon.vue";
import ActionButton from "@/components/ActionButton.vue";
+import PermissionGate from "@/components/PermissionGate.vue";
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { useRedirectsStore } from "@/stores/RedirectsStore";
import LoadingSpinner from "../LoadingSpinner.vue";
+import { storeToRefs } from "pinia";
const redirectsStore = useRedirectsStore();
+const { canManageRedirects } = storeToRefs(redirectsStore);
+// Creating, modifying and ending redirects all manage redirects.
+const redirectsDeniedTooltip = "You don't have permission to manage redirects.";
const loadingData = ref(true);
const redirects = redirectsStore.redirects;
@@ -131,7 +136,9 @@ onMounted(async () => {
@@ -163,8 +170,12 @@ onMounted(async () => {
- End Redirect
- Modify Redirect
+
+ End Redirect
+
+
+ Modify Redirect
+
diff --git a/src/Frontend/src/components/configuration/UserPermissions.vue b/src/Frontend/src/components/configuration/UserPermissions.vue
new file mode 100644
index 0000000000..ae81d37396
--- /dev/null
+++ b/src/Frontend/src/components/configuration/UserPermissions.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
Your permissions
+
+
+
+
+ | Area |
+ Capability |
+ Allowed |
+
+
+
+
+
+ | {{ row.area }} |
+ {{ cap.label }} |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/src/Frontend/src/components/customchecks/CustomCheckView.vue b/src/Frontend/src/components/customchecks/CustomCheckView.vue
index e59f0acfda..0385768233 100644
--- a/src/Frontend/src/components/customchecks/CustomCheckView.vue
+++ b/src/Frontend/src/components/customchecks/CustomCheckView.vue
@@ -1,16 +1,24 @@
@@ -36,7 +44,11 @@ const endpointColor = hexToCSSFilter("#929E9E").filter;
-
+
+
+
diff --git a/src/Frontend/src/components/failedmessages/DeletedMessageGroups.vue b/src/Frontend/src/components/failedmessages/DeletedMessageGroups.vue
index 39bc372591..4d0a313558 100644
--- a/src/Frontend/src/components/failedmessages/DeletedMessageGroups.vue
+++ b/src/Frontend/src/components/failedmessages/DeletedMessageGroups.vue
@@ -11,6 +11,9 @@ import routeLinks from "@/router/routeLinks";
import { TYPE } from "vue-toastification";
import MetadataItem from "@/components/MetadataItem.vue";
import ActionButton from "@/components/ActionButton.vue";
+import PermissionGate from "@/components/PermissionGate.vue";
+import { useAllowedRoutes } from "@/composables/useAllowedRoutes";
+import { ApiRoutes } from "@/composables/apiRoutes";
import { faArrowRotateRight, faEnvelope } from "@fortawesome/free-solid-svg-icons";
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { useDeletedMessageGroupsStore, statusesForRestoreOperation, type ExtendedFailureGroupView, type Status } from "@/stores/DeletedMessageGroupsStore";
@@ -25,6 +28,13 @@ const { autoRefresh, isRefreshing, updateInterval } = useStoreAutoRefresh("delet
const { store } = autoRefresh();
const { archiveGroups, classifiers, selectedClassifier } = storeToRefs(store);
const router = useRouter();
+
+const { canCall } = useAllowedRoutes();
+// Restoring a deleted group is an unarchive; keep the button visible but disabled with a
+// tooltip when the user lacks the permission, instead of silently failing server-side.
+const canRestoreGroups = computed(() => canCall(ApiRoutes.restoreGroup));
+const restoreDeniedTooltip = "You don't have permission to restore message groups.";
+
const showRestoreGroupModal = ref(false);
const selectedGroup = ref
();
@@ -163,16 +173,11 @@ watch(isRestoreInProgress, (restoreInProgress) => {
-
- Restore group
-
+
+
+ Restore group
+
+
diff --git a/src/Frontend/src/components/failedmessages/DeletedMessages.vue b/src/Frontend/src/components/failedmessages/DeletedMessages.vue
index b63533a181..11bc5ae1b5 100644
--- a/src/Frontend/src/components/failedmessages/DeletedMessages.vue
+++ b/src/Frontend/src/components/failedmessages/DeletedMessages.vue
@@ -10,6 +10,9 @@ import PaginationStrip from "../../components/PaginationStrip.vue";
import { FailedMessageStatus } from "@/resources/FailedMessage";
import { TYPE } from "vue-toastification";
import FAIcon from "@/components/FAIcon.vue";
+import PermissionGate from "@/components/PermissionGate.vue";
+import { useAllowedRoutes } from "@/composables/useAllowedRoutes";
+import { ApiRoutes } from "@/composables/apiRoutes";
import { faArrowRotateRight } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
import { useStoreAutoRefresh } from "@/composables/useAutoRefresh";
@@ -27,6 +30,12 @@ const { messages, groupId, groupName, totalCount, pageNumber, selectedPeriod } =
const showConfirmRestore = ref(false);
const messageList = ref();
+const { canCall } = useAllowedRoutes();
+// Restoring messages is an unarchive; keep the button visible but disabled with a tooltip
+// when the user lacks the permission, instead of silently failing server-side.
+const canRestoreMessages = computed(() => canCall(ApiRoutes.restoreMessage));
+const restoreDeniedTooltip = "You don't have permission to restore messages.";
+
function numberSelected() {
return messageList.value?.getSelectedMessages()?.length ?? 0;
}
@@ -103,7 +112,11 @@ watch(isRefreshing, () => {
diff --git a/src/Frontend/src/components/failedmessages/FailedMessages.vue b/src/Frontend/src/components/failedmessages/FailedMessages.vue
index 54cc9e1650..1277f865b2 100644
--- a/src/Frontend/src/components/failedmessages/FailedMessages.vue
+++ b/src/Frontend/src/components/failedmessages/FailedMessages.vue
@@ -16,6 +16,9 @@ import { TYPE } from "vue-toastification";
import type GroupOperation from "@/resources/GroupOperation";
import { faArrowDownAZ, faArrowDownZA, faArrowDownShortWide, faArrowDownWideShort, faArrowRotateRight, faTrash, faDownload } from "@fortawesome/free-solid-svg-icons";
import ActionButton from "@/components/ActionButton.vue";
+import PermissionGate from "@/components/PermissionGate.vue";
+import { useAllowedRoutes } from "@/composables/useAllowedRoutes";
+import { ApiRoutes } from "@/composables/apiRoutes";
import { useMessageStore } from "@/stores/MessageStore";
import { useRecoverabilityStore } from "@/stores/RecoverabilityStore";
import { useStoreAutoRefresh } from "@/composables/useAutoRefresh";
@@ -32,6 +35,18 @@ const { autoRefresh, isRefreshing, updateInterval } = useStoreAutoRefresh("recov
const { store } = autoRefresh();
const { messages, groupId, groupName, totalCount, pageNumber } = storeToRefs(store);
+const { canCall } = useAllowedRoutes();
+// Keep the toolbar actions visible but disabled (with a tooltip) when the user lacks the
+// permission, so the capability stays discoverable and clicks don't silently fail server-side.
+const canRetryMessages = computed(() => canCall(ApiRoutes.retryMessage));
+const canDeleteMessages = computed(() => canCall(ApiRoutes.deleteMessage));
+const canRetryGroup = computed(() => canCall(ApiRoutes.retryGroup));
+const canDeleteGroup = computed(() => canCall(ApiRoutes.deleteGroup));
+const retryDeniedTooltip = "You don't have permission to retry messages.";
+const deleteDeniedTooltip = "You don't have permission to delete messages.";
+const retryAllDeniedTooltip = "You don't have permission to retry message groups.";
+const deleteAllDeniedTooltip = "You don't have permission to delete message groups.";
+
const showDelete = ref(false);
const showConfirmRetryAll = ref(false);
const showConfirmDeleteAll = ref(false);
@@ -217,11 +232,19 @@ watch(isRefreshing, () => {
diff --git a/src/Frontend/src/components/failedmessages/MessageGroupList.vue b/src/Frontend/src/components/failedmessages/MessageGroupList.vue
index 55ee9307d6..977bbaf75b 100644
--- a/src/Frontend/src/components/failedmessages/MessageGroupList.vue
+++ b/src/Frontend/src/components/failedmessages/MessageGroupList.vue
@@ -1,5 +1,5 @@