Преглед на файлове

Permissions polish for restricted users

Jamie Curnow преди 1 месец
родител
ревизия
4709f9826c
променени са 28 файла, в които са добавени 457 реда и са изтрити 306 реда
  1. 27 12
      frontend/src/components/EmptyData.tsx
  2. 9 31
      frontend/src/components/HasPermission.tsx
  3. 35 23
      frontend/src/components/SiteMenu.tsx
  4. 34 3
      frontend/src/modals/PermissionsModal.tsx
  5. 20 15
      frontend/src/modals/ProxyHostModal.tsx
  6. 49 0
      frontend/src/modules/Permissions.ts
  7. 17 13
      frontend/src/pages/Access/Table.tsx
  8. 13 6
      frontend/src/pages/Access/TableWrapper.tsx
  9. 2 1
      frontend/src/pages/Access/index.tsx
  10. 2 1
      frontend/src/pages/AuditLog/index.tsx
  11. 28 23
      frontend/src/pages/Certificates/Table.tsx
  12. 46 44
      frontend/src/pages/Certificates/TableWrapper.tsx
  13. 2 1
      frontend/src/pages/Certificates/index.tsx
  14. 5 4
      frontend/src/pages/Dashboard/index.tsx
  15. 28 23
      frontend/src/pages/Nginx/DeadHosts/Table.tsx
  16. 9 6
      frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx
  17. 2 1
      frontend/src/pages/Nginx/DeadHosts/index.tsx
  18. 28 23
      frontend/src/pages/Nginx/ProxyHosts/Table.tsx
  19. 13 7
      frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx
  20. 2 1
      frontend/src/pages/Nginx/ProxyHosts/index.tsx
  21. 28 23
      frontend/src/pages/Nginx/RedirectionHosts/Table.tsx
  22. 13 11
      frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx
  23. 2 1
      frontend/src/pages/Nginx/RedirectionHosts/index.tsx
  24. 28 23
      frontend/src/pages/Nginx/Streams/Table.tsx
  25. 9 7
      frontend/src/pages/Nginx/Streams/TableWrapper.tsx
  26. 2 1
      frontend/src/pages/Nginx/Streams/index.tsx
  27. 2 1
      frontend/src/pages/Settings/index.tsx
  28. 2 1
      frontend/src/pages/Users/index.tsx

+ 27 - 12
frontend/src/components/EmptyData.tsx

@@ -1,8 +1,9 @@
 import type { Table as ReactTable } from "@tanstack/react-table";
 import cn from "classnames";
 import type { ReactNode } from "react";
-import { Button } from "src/components";
+import { Button, HasPermission } from "src/components";
 import { T } from "src/locale";
+import { type ADMIN, MANAGE, type Permission, type Section } from "src/modules/Permissions";
 
 interface Props {
 	tableInstance: ReactTable<any>;
@@ -12,8 +13,20 @@ interface Props {
 	objects: string;
 	color?: string;
 	customAddBtn?: ReactNode;
+	permissionSection?: Section | typeof ADMIN;
+	permission?: Permission;
 }
-function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color = "primary", customAddBtn }: Props) {
+function EmptyData({
+	tableInstance,
+	onNew,
+	isFiltered,
+	object,
+	objects,
+	color = "primary",
+	customAddBtn,
+	permissionSection,
+	permission,
+}: Props) {
 	return (
 		<tr>
 			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
@@ -27,16 +40,18 @@ function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color =
 							<h2>
 								<T id="object.empty" tData={{ objects }} />
 							</h2>
-							<p className="text-muted">
-								<T id="empty-subtitle" />
-							</p>
-							{customAddBtn ? (
-								customAddBtn
-							) : (
-								<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
-									<T id="object.add" tData={{ object }} />
-								</Button>
-							)}
+							<HasPermission section={permissionSection} permission={permission || MANAGE} hideError>
+								<p className="text-muted">
+									<T id="empty-subtitle" />
+								</p>
+								{customAddBtn ? (
+									customAddBtn
+								) : (
+									<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
+										<T id="object.add" tData={{ object }} />
+									</Button>
+								)}
+							</HasPermission>
 						</>
 					)}
 				</div>

+ 9 - 31
frontend/src/components/HasPermission.tsx

@@ -3,25 +3,29 @@ import Alert from "react-bootstrap/Alert";
 import { Loading, LoadingPage } from "src/components";
 import { useUser } from "src/hooks";
 import { T } from "src/locale";
+import { type ADMIN, hasPermission, type Permission, type Section } from "src/modules/Permissions";
 
 interface Props {
-	permission: string;
-	type: "manage" | "view";
+	section?: Section | typeof ADMIN;
+	permission: Permission;
 	hideError?: boolean;
 	children?: ReactNode;
 	pageLoading?: boolean;
 	loadingNoLogo?: boolean;
 }
 function HasPermission({
+	section,
 	permission,
-	type,
 	children,
 	hideError = false,
 	pageLoading = false,
 	loadingNoLogo = false,
 }: Props) {
 	const { data, isLoading } = useUser("me");
-	const perms = data?.permissions;
+
+	if (!section) {
+		return <>{children}</>;
+	}
 
 	if (isLoading) {
 		if (hideError) {
@@ -33,33 +37,7 @@ function HasPermission({
 		return <Loading noLogo={loadingNoLogo} />;
 	}
 
-	let allowed = permission === "";
-	const acceptable = ["manage", type];
-
-	switch (permission) {
-		case "admin":
-			allowed = data?.roles?.includes("admin") || false;
-			break;
-		case "proxyHosts":
-			allowed = acceptable.indexOf(perms?.proxyHosts || "") !== -1;
-			break;
-		case "redirectionHosts":
-			allowed = acceptable.indexOf(perms?.redirectionHosts || "") !== -1;
-			break;
-		case "deadHosts":
-			allowed = acceptable.indexOf(perms?.deadHosts || "") !== -1;
-			break;
-		case "streams":
-			allowed = acceptable.indexOf(perms?.streams || "") !== -1;
-			break;
-		case "accessLists":
-			allowed = acceptable.indexOf(perms?.accessLists || "") !== -1;
-			break;
-		case "certificates":
-			allowed = acceptable.indexOf(perms?.certificates || "") !== -1;
-			break;
-	}
-
+	const allowed = hasPermission(section, permission, data?.permissions, data?.roles);
 	if (allowed) {
 		return <>{children}</>;
 	}

+ 35 - 23
frontend/src/components/SiteMenu.tsx

@@ -11,14 +11,26 @@ import cn from "classnames";
 import React from "react";
 import { HasPermission, NavLink } from "src/components";
 import { T } from "src/locale";
+import {
+	ACCESS_LISTS,
+	ADMIN,
+	CERTIFICATES,
+	DEAD_HOSTS,
+	type MANAGE,
+	PROXY_HOSTS,
+	REDIRECTION_HOSTS,
+	type Section,
+	STREAMS,
+	VIEW,
+} from "src/modules/Permissions";
 
 interface MenuItem {
 	label: string;
 	icon?: React.ElementType;
 	to?: string;
 	items?: MenuItem[];
-	permission?: string;
-	permissionType?: "view" | "manage";
+	permissionSection?: Section | typeof ADMIN;
+	permission?: typeof VIEW | typeof MANAGE;
 }
 
 const menuItems: MenuItem[] = [
@@ -34,26 +46,26 @@ const menuItems: MenuItem[] = [
 			{
 				to: "/nginx/proxy",
 				label: "proxy-hosts",
-				permission: "proxyHosts",
-				permissionType: "view",
+				permissionSection: PROXY_HOSTS,
+				permission: VIEW,
 			},
 			{
 				to: "/nginx/redirection",
 				label: "redirection-hosts",
-				permission: "redirectionHosts",
-				permissionType: "view",
+				permissionSection: REDIRECTION_HOSTS,
+				permission: VIEW,
 			},
 			{
 				to: "/nginx/stream",
 				label: "streams",
-				permission: "streams",
-				permissionType: "view",
+				permissionSection: STREAMS,
+				permission: VIEW,
 			},
 			{
 				to: "/nginx/404",
 				label: "dead-hosts",
-				permission: "deadHosts",
-				permissionType: "view",
+				permissionSection: DEAD_HOSTS,
+				permission: VIEW,
 			},
 		],
 	},
@@ -61,33 +73,33 @@ const menuItems: MenuItem[] = [
 		to: "/access",
 		icon: IconLock,
 		label: "access-lists",
-		permission: "accessLists",
-		permissionType: "view",
+		permissionSection: ACCESS_LISTS,
+		permission: VIEW,
 	},
 	{
 		to: "/certificates",
 		icon: IconShield,
 		label: "certificates",
-		permission: "certificates",
-		permissionType: "view",
+		permissionSection: CERTIFICATES,
+		permission: VIEW,
 	},
 	{
 		to: "/users",
 		icon: IconUser,
 		label: "users",
-		permission: "admin",
+		permissionSection: ADMIN,
 	},
 	{
 		to: "/audit-log",
 		icon: IconBook,
 		label: "auditlogs",
-		permission: "admin",
+		permissionSection: ADMIN,
 	},
 	{
 		to: "/settings",
 		icon: IconSettings,
 		label: "settings",
-		permission: "admin",
+		permissionSection: ADMIN,
 	},
 ];
 
@@ -99,8 +111,8 @@ const getMenuItem = (item: MenuItem, onClick?: () => void) => {
 	return (
 		<HasPermission
 			key={`item-${item.label}`}
-			permission={item.permission || ""}
-			type={item.permissionType || "view"}
+			section={item.permissionSection}
+			permission={item.permission || VIEW}
 			hideError
 		>
 			<li className="nav-item">
@@ -122,8 +134,8 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
 	return (
 		<HasPermission
 			key={`item-${item.label}`}
-			permission={item.permission || ""}
-			type={item.permissionType || "view"}
+			section={item.permissionSection}
+			permission={item.permission || VIEW}
 			hideError
 		>
 			<li className={cns}>
@@ -147,8 +159,8 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
 						return (
 							<HasPermission
 								key={`${idx}-${subitem.to}`}
-								permission={subitem.permission || ""}
-								type={subitem.permissionType || "view"}
+								section={subitem.permissionSection}
+								permission={subitem.permission || VIEW}
 								hideError
 							>
 								<NavLink to={subitem.to} isDropdownItem onClick={onClick}>

+ 34 - 3
frontend/src/modals/PermissionsModal.tsx

@@ -47,11 +47,41 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 		});
 	};
 
+	// given the field and clicked permission, intelligently set the value, and
+	// other values that depends on it.
+	const handleChange = (form: any, field: any, perm: string) => {
+		if (field.name === "proxyHosts" && perm !== "hidden" && form.values.accessLists === "hidden") {
+			form.setFieldValue("accessLists", "view");
+		}
+		// certs are required for proxy and redirection hosts, and streams
+		if (
+			["proxyHosts", "redirectionHosts", "deadHosts", "streams"].includes(field.name) &&
+			perm !== "hidden" &&
+			form.values.certificates === "hidden"
+		) {
+			form.setFieldValue("certificates", "view");
+		}
+
+		form.setFieldValue(field.name, perm);
+	};
+
 	const getPermissionButtons = (field: any, form: any) => {
 		const isManage = field.value === "manage";
 		const isView = field.value === "view";
 		const isHidden = field.value === "hidden";
 
+		let hiddenDisabled = false;
+		if (field.name === "accessLists") {
+			hiddenDisabled = form.values.proxyHosts !== "hidden";
+		}
+		if (field.name === "certificates") {
+			hiddenDisabled =
+				form.values.proxyHosts !== "hidden" ||
+				form.values.redirectionHosts !== "hidden" ||
+				form.values.deadHosts !== "hidden" ||
+				form.values.streams !== "hidden";
+		}
+
 		return (
 			<div>
 				<div className="btn-group w-100" role="group">
@@ -63,7 +93,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 						autoComplete="off"
 						value="manage"
 						checked={field.value === "manage"}
-						onChange={() => form.setFieldValue(field.name, "manage")}
+						onChange={() => handleChange(form, field, "manage")}
 					/>
 					<label htmlFor={`${field.name}-manage`} className={getClasses(isManage)}>
 						<T id="permissions.manage" />
@@ -76,7 +106,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 						autoComplete="off"
 						value="view"
 						checked={field.value === "view"}
-						onChange={() => form.setFieldValue(field.name, "view")}
+						onChange={() => handleChange(form, field, "view")}
 					/>
 					<label htmlFor={`${field.name}-view`} className={getClasses(isView)}>
 						<T id="permissions.view" />
@@ -89,7 +119,8 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 						autoComplete="off"
 						value="hidden"
 						checked={field.value === "hidden"}
-						onChange={() => form.setFieldValue(field.name, "hidden")}
+						disabled={hiddenDisabled}
+						onChange={() => handleChange(form, field, "hidden")}
 					/>
 					<label htmlFor={`${field.name}-hidden`} className={getClasses(isHidden)}>
 						<T id="permissions.hidden" />

+ 20 - 15
frontend/src/modals/ProxyHostModal.tsx

@@ -9,14 +9,16 @@ import {
 	AccessField,
 	Button,
 	DomainNamesField,
+	HasPermission,
 	Loading,
 	LocationsFields,
 	NginxConfigField,
 	SSLCertificateField,
 	SSLOptionsFields,
 } from "src/components";
-import { useProxyHost, useSetProxyHost } from "src/hooks";
+import { useProxyHost, useSetProxyHost, useUser } from "src/hooks";
 import { T } from "src/locale";
+import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
 import { validateNumber, validateString } from "src/modules/Validations";
 import { showObjectSuccess } from "src/notifications";
 
@@ -28,6 +30,7 @@ interface Props extends InnerModalProps {
 	id: number | "new";
 }
 const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
+	const { data: currentUser, isLoading: userIsLoading, error: userError } = useUser("me");
 	const { data, isLoading, error } = useProxyHost(id);
 	const { mutate: setProxyHost } = useSetProxyHost();
 	const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -58,13 +61,13 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 
 	return (
 		<Modal show={visible} onHide={remove}>
-			{!isLoading && error && (
+			{!isLoading && (error || userError) && (
 				<Alert variant="danger" className="m-3">
-					{error?.message || "Unknown error"}
+					{error?.message || userError?.message || "Unknown error"}
 				</Alert>
 			)}
-			{isLoading && <Loading noLogo />}
-			{!isLoading && data && (
+			{isLoading || (userIsLoading && <Loading noLogo />)}
+			{!isLoading && !userIsLoading && data && currentUser && (
 				<Formik
 					initialValues={
 						{
@@ -349,16 +352,18 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 								<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
 									<T id="cancel" />
 								</Button>
-								<Button
-									type="submit"
-									actionType="primary"
-									className="ms-auto bg-lime"
-									data-bs-dismiss="modal"
-									isLoading={isSubmitting}
-									disabled={isSubmitting}
-								>
-									<T id="save" />
-								</Button>
+								<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>
+									<Button
+										type="submit"
+										actionType="primary"
+										className="ms-auto bg-lime"
+										data-bs-dismiss="modal"
+										isLoading={isSubmitting}
+										disabled={isSubmitting}
+									>
+										<T id="save" />
+									</Button>
+								</HasPermission>
 							</Modal.Footer>
 						</Form>
 					)}

+ 49 - 0
frontend/src/modules/Permissions.ts

@@ -0,0 +1,49 @@
+import type { UserPermissions } from "src/api/backend";
+
+export const ADMIN = "admin";
+export const VISIBILITY = "visibility";
+export const PROXY_HOSTS = "proxyHosts";
+export const REDIRECTION_HOSTS = "redirectionHosts";
+export const DEAD_HOSTS = "deadHosts";
+export const STREAMS = "streams";
+export const CERTIFICATES = "certificates";
+export const ACCESS_LISTS = "accessLists";
+
+export const MANAGE = "manage";
+export const VIEW = "view";
+export const HIDDEN = "hidden";
+
+export const ALL = "all";
+export const USER = "user";
+
+export type Section =
+	| typeof ADMIN
+	| typeof VISIBILITY
+	| typeof PROXY_HOSTS
+	| typeof REDIRECTION_HOSTS
+	| typeof DEAD_HOSTS
+	| typeof STREAMS
+	| typeof CERTIFICATES
+	| typeof ACCESS_LISTS;
+
+export type Permission = typeof MANAGE | typeof VIEW;
+
+const hasPermission = (
+	section: Section,
+	perm: Permission,
+	userPerms: UserPermissions | undefined,
+	roles: string[] | undefined,
+): boolean => {
+	if (!userPerms) return false;
+	if (isAdmin(roles)) return true;
+	const acceptable = [MANAGE, perm];
+	// @ts-expect-error 7053
+	const v = typeof userPerms[section] !== "undefined" ? userPerms[section] : HIDDEN;
+	return acceptable.indexOf(v) !== -1;
+};
+
+const isAdmin = (roles: string[] | undefined): boolean => {
+	return roles?.includes("admin") || false;
+};
+
+export { hasPermission, isAdmin };

+ 17 - 13
frontend/src/pages/Access/Table.tsx

@@ -2,9 +2,10 @@ import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
 import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 import { useMemo } from "react";
 import type { AccessList } from "src/api/backend";
-import { EmptyData, GravatarFormatter, ValueWithDateFormatter } from "src/components";
+import { EmptyData, GravatarFormatter, HasPermission, ValueWithDateFormatter } from "src/components";
 import { TableLayout } from "src/components/Table/TableLayout";
 import { intl, T } from "src/locale";
+import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions";
 
 interface Props {
 	data: AccessList[];
@@ -84,18 +85,20 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 									<IconEdit size={16} />
 									<T id="action.edit" />
 								</a>
-								<div className="dropdown-divider" />
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDelete?.(info.row.original.id);
-									}}
-								>
-									<IconTrash size={16} />
-									<T id="action.delete" />
-								</a>
+								<HasPermission section={ACCESS_LISTS} permission={MANAGE} hideError>
+									<div className="dropdown-divider" />
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDelete?.(info.row.original.id);
+										}}
+									>
+										<IconTrash size={16} />
+										<T id="action.delete" />
+									</a>
+								</HasPermission>
 							</div>
 						</span>
 					);
@@ -130,6 +133,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 					onNew={onNew}
 					isFiltered={isFiltered}
 					color="cyan"
+					permissionSection={ACCESS_LISTS}
 				/>
 			}
 		/>

+ 13 - 6
frontend/src/pages/Access/TableWrapper.tsx

@@ -2,10 +2,11 @@ import { IconHelp, IconSearch } from "@tabler/icons-react";
 import { useState } from "react";
 import Alert from "react-bootstrap/Alert";
 import { deleteAccessList } from "src/api/backend";
-import { Button, LoadingPage } from "src/components";
+import { Button, HasPermission, LoadingPage } from "src/components";
 import { useAccessLists } from "src/hooks";
 import { T } from "src/locale";
 import { showAccessListModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
+import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions";
 import { showObjectSuccess } from "src/notifications";
 import Table from "./Table";
 
@@ -67,11 +68,17 @@ export default function TableWrapper() {
 								<Button size="sm" onClick={() => showHelpModal("AccessLists", "cyan")}>
 									<IconHelp size={20} />
 								</Button>
-								{data?.length ? (
-									<Button size="sm" className="btn-cyan" onClick={() => showAccessListModal("new")}>
-										<T id="object.add" tData={{ object: "access-list" }} />
-									</Button>
-								) : null}
+								<HasPermission section={ACCESS_LISTS} permission={MANAGE} hideError>
+									{data?.length ? (
+										<Button
+											size="sm"
+											className="btn-cyan"
+											onClick={() => showAccessListModal("new")}
+										>
+											<T id="object.add" tData={{ object: "access-list" }} />
+										</Button>
+									) : null}
+								</HasPermission>
 							</div>
 						</div>
 					</div>

+ 2 - 1
frontend/src/pages/Access/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { ACCESS_LISTS, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const Access = () => {
 	return (
-		<HasPermission permission="accessLists" type="view" pageLoading loadingNoLogo>
+		<HasPermission section={ACCESS_LISTS} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);

+ 2 - 1
frontend/src/pages/AuditLog/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { ADMIN, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const AuditLog = () => {
 	return (
-		<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
+		<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);

+ 28 - 23
frontend/src/pages/Certificates/Table.tsx

@@ -8,10 +8,12 @@ import {
 	DomainsFormatter,
 	EmptyData,
 	GravatarFormatter,
+	HasPermission,
 } from "src/components";
 import { TableLayout } from "src/components/Table/TableLayout";
 import { intl, T } from "src/locale";
 import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
+import { CERTIFICATES, MANAGE } from "src/modules/Permissions";
 
 interface Props {
 	data: Certificate[];
@@ -125,29 +127,31 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
 									<IconRefresh size={16} />
 									<T id="action.renew" />
 								</a>
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDownload?.(info.row.original.id);
-									}}
-								>
-									<IconDownload size={16} />
-									<T id="action.download" />
-								</a>
-								<div className="dropdown-divider" />
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDelete?.(info.row.original.id);
-									}}
-								>
-									<IconTrash size={16} />
-									<T id="action.delete" />
-								</a>
+								<HasPermission section={CERTIFICATES} permission={MANAGE} hideError>
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDownload?.(info.row.original.id);
+										}}
+									>
+										<IconDownload size={16} />
+										<T id="action.download" />
+									</a>
+									<div className="dropdown-divider" />
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDelete?.(info.row.original.id);
+										}}
+									>
+										<IconTrash size={16} />
+										<T id="action.delete" />
+									</a>
+								</HasPermission>
 							</div>
 						</span>
 					);
@@ -223,6 +227,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
 					isFiltered={isFiltered}
 					color="pink"
 					customAddBtn={customAddBtn}
+					permissionSection={CERTIFICATES}
 				/>
 			}
 		/>

+ 46 - 44
frontend/src/pages/Certificates/TableWrapper.tsx

@@ -2,7 +2,7 @@ import { IconHelp, IconSearch } from "@tabler/icons-react";
 import { useState } from "react";
 import Alert from "react-bootstrap/Alert";
 import { deleteCertificate, downloadCertificate } from "src/api/backend";
-import { Button, LoadingPage } from "src/components";
+import { Button, HasPermission, LoadingPage } from "src/components";
 import { useCertificates } from "src/hooks";
 import { T } from "src/locale";
 import {
@@ -13,6 +13,7 @@ import {
 	showHTTPCertificateModal,
 	showRenewCertificateModal,
 } from "src/modals";
+import { CERTIFICATES, MANAGE } from "src/modules/Permissions";
 import { showError, showObjectSuccess } from "src/notifications";
 import Table from "./Table";
 
@@ -70,7 +71,6 @@ export default function TableWrapper() {
 								<T id="certificates" />
 							</h2>
 						</div>
-
 						<div className="col-md-auto col-sm-12">
 							<div className="ms-auto d-flex flex-wrap btn-list">
 								{data?.length ? (
@@ -90,50 +90,52 @@ export default function TableWrapper() {
 								<Button size="sm" onClick={() => showHelpModal("Certificates", "pink")}>
 									<IconHelp size={20} />
 								</Button>
-								{data?.length ? (
-									<div className="dropdown">
-										<button
-											type="button"
-											className="btn btn-sm dropdown-toggle btn-pink mt-1"
-											data-bs-toggle="dropdown"
-										>
-											<T id="object.add" tData={{ object: "certificate" }} />
-										</button>
-										<div className="dropdown-menu">
-											<a
-												className="dropdown-item"
-												href="#"
-												onClick={(e) => {
-													e.preventDefault();
-													showHTTPCertificateModal();
-												}}
-											>
-												<T id="lets-encrypt-via-http" />
-											</a>
-											<a
-												className="dropdown-item"
-												href="#"
-												onClick={(e) => {
-													e.preventDefault();
-													showDNSCertificateModal();
-												}}
+								<HasPermission section={CERTIFICATES} permission={MANAGE} hideError>
+									{data?.length ? (
+										<div className="dropdown">
+											<button
+												type="button"
+												className="btn btn-sm dropdown-toggle btn-pink mt-1"
+												data-bs-toggle="dropdown"
 											>
-												<T id="lets-encrypt-via-dns" />
-											</a>
-											<div className="dropdown-divider" />
-											<a
-												className="dropdown-item"
-												href="#"
-												onClick={(e) => {
-													e.preventDefault();
-													showCustomCertificateModal();
-												}}
-											>
-												<T id="certificates.custom" />
-											</a>
+												<T id="object.add" tData={{ object: "certificate" }} />
+											</button>
+											<div className="dropdown-menu">
+												<a
+													className="dropdown-item"
+													href="#"
+													onClick={(e) => {
+														e.preventDefault();
+														showHTTPCertificateModal();
+													}}
+												>
+													<T id="lets-encrypt-via-http" />
+												</a>
+												<a
+													className="dropdown-item"
+													href="#"
+													onClick={(e) => {
+														e.preventDefault();
+														showDNSCertificateModal();
+													}}
+												>
+													<T id="lets-encrypt-via-dns" />
+												</a>
+												<div className="dropdown-divider" />
+												<a
+													className="dropdown-item"
+													href="#"
+													onClick={(e) => {
+														e.preventDefault();
+														showCustomCertificateModal();
+													}}
+												>
+													<T id="certificates.custom" />
+												</a>
+											</div>
 										</div>
-									</div>
-								) : null}
+									) : null}
+								</HasPermission>
 							</div>
 						</div>
 					</div>

+ 2 - 1
frontend/src/pages/Certificates/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { CERTIFICATES, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const Certificates = () => {
 	return (
-		<HasPermission permission="certificates" type="view" pageLoading loadingNoLogo>
+		<HasPermission section={CERTIFICATES} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);

+ 5 - 4
frontend/src/pages/Dashboard/index.tsx

@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
 import { HasPermission } from "src/components";
 import { useHostReport } from "src/hooks";
 import { T } from "src/locale";
+import { DEAD_HOSTS, PROXY_HOSTS, REDIRECTION_HOSTS, STREAMS, VIEW } from "src/modules/Permissions";
 
 const Dashboard = () => {
 	const { data: hostReport } = useHostReport();
@@ -16,7 +17,7 @@ const Dashboard = () => {
 			<div className="row row-deck row-cards">
 				<div className="col-12 my-4">
 					<div className="row row-cards">
-						<HasPermission permission="proxyHosts" type="view" hideError>
+						<HasPermission section={PROXY_HOSTS} permission={VIEW} hideError>
 							<div className="col-sm-6 col-lg-3">
 								<a
 									href="/nginx/proxy"
@@ -43,7 +44,7 @@ const Dashboard = () => {
 								</a>
 							</div>
 						</HasPermission>
-						<HasPermission permission="redirectionHosts" type="view" hideError>
+						<HasPermission section={REDIRECTION_HOSTS} permission={VIEW} hideError>
 							<div className="col-sm-6 col-lg-3">
 								<a
 									href="/nginx/redirection"
@@ -71,7 +72,7 @@ const Dashboard = () => {
 								</a>
 							</div>
 						</HasPermission>
-						<HasPermission permission="streams" type="view" hideError>
+						<HasPermission section={STREAMS} permission={VIEW} hideError>
 							<div className="col-sm-6 col-lg-3">
 								<a
 									href="/nginx/stream"
@@ -96,7 +97,7 @@ const Dashboard = () => {
 								</a>
 							</div>
 						</HasPermission>
-						<HasPermission permission="deadHosts" type="view" hideError>
+						<HasPermission section={DEAD_HOSTS} permission={VIEW} hideError>
 							<div className="col-sm-6 col-lg-3">
 								<a
 									href="/nginx/404"

+ 28 - 23
frontend/src/pages/Nginx/DeadHosts/Table.tsx

@@ -7,10 +7,12 @@ import {
 	DomainsFormatter,
 	EmptyData,
 	GravatarFormatter,
+	HasPermission,
 	TrueFalseFormatter,
 } from "src/components";
 import { TableLayout } from "src/components/Table/TableLayout";
 import { intl, T } from "src/locale";
+import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions";
 
 interface Props {
 	data: DeadHost[];
@@ -89,29 +91,31 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 									<IconEdit size={16} />
 									<T id="action.edit" />
 								</a>
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
-									}}
-								>
-									<IconPower size={16} />
-									<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
-								</a>
-								<div className="dropdown-divider" />
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDelete?.(info.row.original.id);
-									}}
-								>
-									<IconTrash size={16} />
-									<T id="action.delete" />
-								</a>
+								<HasPermission section={DEAD_HOSTS} permission={MANAGE} hideError>
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
+										}}
+									>
+										<IconPower size={16} />
+										<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
+									</a>
+									<div className="dropdown-divider" />
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDelete?.(info.row.original.id);
+										}}
+									>
+										<IconTrash size={16} />
+										<T id="action.delete" />
+									</a>
+								</HasPermission>
 							</div>
 						</span>
 					);
@@ -146,6 +150,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 					onNew={onNew}
 					isFiltered={isFiltered}
 					color="red"
+					permissionSection={DEAD_HOSTS}
 				/>
 			}
 		/>

+ 9 - 6
frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx

@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
 import { useState } from "react";
 import Alert from "react-bootstrap/Alert";
 import { deleteDeadHost, toggleDeadHost } from "src/api/backend";
-import { Button, LoadingPage } from "src/components";
+import { Button, HasPermission, LoadingPage } from "src/components";
 import { useDeadHosts } from "src/hooks";
 import { T } from "src/locale";
 import { showDeadHostModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
+import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions";
 import { showObjectSuccess } from "src/notifications";
 import Table from "./Table";
 
@@ -76,11 +77,13 @@ export default function TableWrapper() {
 								<Button size="sm" onClick={() => showHelpModal("DeadHosts", "red")}>
 									<IconHelp size={20} />
 								</Button>
-								{data?.length ? (
-									<Button size="sm" className="btn-red" onClick={() => showDeadHostModal("new")}>
-										<T id="object.add" tData={{ object: "dead-host" }} />
-									</Button>
-								) : null}
+								<HasPermission section={DEAD_HOSTS} permission={MANAGE} hideError>
+									{data?.length ? (
+										<Button size="sm" className="btn-red" onClick={() => showDeadHostModal("new")}>
+											<T id="object.add" tData={{ object: "dead-host" }} />
+										</Button>
+									) : null}
+								</HasPermission>
 							</div>
 						</div>
 					</div>

+ 2 - 1
frontend/src/pages/Nginx/DeadHosts/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { DEAD_HOSTS, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const DeadHosts = () => {
 	return (
-		<HasPermission permission="deadHosts" type="view" pageLoading loadingNoLogo>
+		<HasPermission section={DEAD_HOSTS} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);

+ 28 - 23
frontend/src/pages/Nginx/ProxyHosts/Table.tsx

@@ -8,10 +8,12 @@ import {
 	DomainsFormatter,
 	EmptyData,
 	GravatarFormatter,
+	HasPermission,
 	TrueFalseFormatter,
 } from "src/components";
 import { TableLayout } from "src/components/Table/TableLayout";
 import { intl, T } from "src/locale";
+import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
 
 interface Props {
 	data: ProxyHost[];
@@ -105,29 +107,31 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 									<IconEdit size={16} />
 									<T id="action.edit" />
 								</a>
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
-									}}
-								>
-									<IconPower size={16} />
-									<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
-								</a>
-								<div className="dropdown-divider" />
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDelete?.(info.row.original.id);
-									}}
-								>
-									<IconTrash size={16} />
-									<T id="action.delete" />
-								</a>
+								<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
+										}}
+									>
+										<IconPower size={16} />
+										<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
+									</a>
+									<div className="dropdown-divider" />
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDelete?.(info.row.original.id);
+										}}
+									>
+										<IconTrash size={16} />
+										<T id="action.delete" />
+									</a>
+								</HasPermission>
 							</div>
 						</span>
 					);
@@ -162,6 +166,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 					onNew={onNew}
 					isFiltered={isFiltered}
 					color="lime"
+					permissionSection={PROXY_HOSTS}
 				/>
 			}
 		/>

+ 13 - 7
frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx

@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
 import { useState } from "react";
 import Alert from "react-bootstrap/Alert";
 import { deleteProxyHost, toggleProxyHost } from "src/api/backend";
-import { Button, LoadingPage } from "src/components";
+import { Button, HasPermission, LoadingPage } from "src/components";
 import { useProxyHosts } from "src/hooks";
 import { T } from "src/locale";
 import { showDeleteConfirmModal, showHelpModal, showProxyHostModal } from "src/modals";
+import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
 import { showObjectSuccess } from "src/notifications";
 import Table from "./Table";
 
@@ -59,7 +60,6 @@ export default function TableWrapper() {
 								<T id="proxy-hosts" />
 							</h2>
 						</div>
-
 						<div className="col-md-auto col-sm-12">
 							<div className="ms-auto d-flex flex-wrap btn-list">
 								{data?.length ? (
@@ -79,11 +79,17 @@ export default function TableWrapper() {
 								<Button size="sm" onClick={() => showHelpModal("ProxyHosts", "lime")}>
 									<IconHelp size={20} />
 								</Button>
-								{data?.length ? (
-									<Button size="sm" className="btn-lime" onClick={() => showProxyHostModal("new")}>
-										<T id="object.add" tData={{ object: "proxy-host" }} />
-									</Button>
-								) : null}
+								<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>
+									{data?.length ? (
+										<Button
+											size="sm"
+											className="btn-lime"
+											onClick={() => showProxyHostModal("new")}
+										>
+											<T id="object.add" tData={{ object: "proxy-host" }} />
+										</Button>
+									) : null}
+								</HasPermission>
 							</div>
 						</div>
 					</div>

+ 2 - 1
frontend/src/pages/Nginx/ProxyHosts/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { PROXY_HOSTS, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const ProxyHosts = () => {
 	return (
-		<HasPermission permission="proxyHosts" type="view" pageLoading loadingNoLogo>
+		<HasPermission section={PROXY_HOSTS} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);

+ 28 - 23
frontend/src/pages/Nginx/RedirectionHosts/Table.tsx

@@ -7,10 +7,12 @@ import {
 	DomainsFormatter,
 	EmptyData,
 	GravatarFormatter,
+	HasPermission,
 	TrueFalseFormatter,
 } from "src/components";
 import { TableLayout } from "src/components/Table/TableLayout";
 import { intl, T } from "src/locale";
+import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions";
 
 interface Props {
 	data: RedirectionHost[];
@@ -110,29 +112,31 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 									<IconEdit size={16} />
 									<T id="action.edit" />
 								</a>
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
-									}}
-								>
-									<IconPower size={16} />
-									<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
-								</a>
-								<div className="dropdown-divider" />
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDelete?.(info.row.original.id);
-									}}
-								>
-									<IconTrash size={16} />
-									<T id="action.delete" />
-								</a>
+								<HasPermission section={REDIRECTION_HOSTS} permission={MANAGE} hideError>
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
+										}}
+									>
+										<IconPower size={16} />
+										<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
+									</a>
+									<div className="dropdown-divider" />
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDelete?.(info.row.original.id);
+										}}
+									>
+										<IconTrash size={16} />
+										<T id="action.delete" />
+									</a>
+								</HasPermission>
 							</div>
 						</span>
 					);
@@ -167,6 +171,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 					onNew={onNew}
 					isFiltered={isFiltered}
 					color="yellow"
+					permissionSection={REDIRECTION_HOSTS}
 				/>
 			}
 		/>

+ 13 - 11
frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx

@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
 import { useState } from "react";
 import Alert from "react-bootstrap/Alert";
 import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend";
-import { Button, LoadingPage } from "src/components";
+import { Button, HasPermission, LoadingPage } from "src/components";
 import { useRedirectionHosts } from "src/hooks";
 import { T } from "src/locale";
 import { showDeleteConfirmModal, showHelpModal, showRedirectionHostModal } from "src/modals";
+import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions";
 import { showObjectSuccess } from "src/notifications";
 import Table from "./Table";
 
@@ -59,7 +60,6 @@ export default function TableWrapper() {
 								<T id="redirection-hosts" />
 							</h2>
 						</div>
-
 						<div className="col-md-auto col-sm-12">
 							<div className="ms-auto d-flex flex-wrap btn-list">
 								{data?.length ? (
@@ -79,15 +79,17 @@ export default function TableWrapper() {
 								<Button size="sm" onClick={() => showHelpModal("RedirectionHosts", "yellow")}>
 									<IconHelp size={20} />
 								</Button>
-								{data?.length ? (
-									<Button
-										size="sm"
-										className="btn-yellow"
-										onClick={() => showRedirectionHostModal("new")}
-									>
-										<T id="object.add" tData={{ object: "redirection-host" }} />
-									</Button>
-								) : null}
+								<HasPermission section={REDIRECTION_HOSTS} permission={MANAGE} hideError>
+									{data?.length ? (
+										<Button
+											size="sm"
+											className="btn-yellow"
+											onClick={() => showRedirectionHostModal("new")}
+										>
+											<T id="object.add" tData={{ object: "redirection-host" }} />
+										</Button>
+									) : null}
+								</HasPermission>
 							</div>
 						</div>
 					</div>

+ 2 - 1
frontend/src/pages/Nginx/RedirectionHosts/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { REDIRECTION_HOSTS, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const RedirectionHosts = () => {
 	return (
-		<HasPermission permission="redirectionHosts" type="view" pageLoading loadingNoLogo>
+		<HasPermission section={REDIRECTION_HOSTS} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);

+ 28 - 23
frontend/src/pages/Nginx/Streams/Table.tsx

@@ -6,11 +6,13 @@ import {
 	CertificateFormatter,
 	EmptyData,
 	GravatarFormatter,
+	HasPermission,
 	TrueFalseFormatter,
 	ValueWithDateFormatter,
 } from "src/components";
 import { TableLayout } from "src/components/Table/TableLayout";
 import { intl, T } from "src/locale";
+import { MANAGE, STREAMS } from "src/modules/Permissions";
 
 interface Props {
 	data: Stream[];
@@ -118,29 +120,31 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 									<IconEdit size={16} />
 									<T id="action.edit" />
 								</a>
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
-									}}
-								>
-									<IconPower size={16} />
-									<T id="action.disable" />
-								</a>
-								<div className="dropdown-divider" />
-								<a
-									className="dropdown-item"
-									href="#"
-									onClick={(e) => {
-										e.preventDefault();
-										onDelete?.(info.row.original.id);
-									}}
-								>
-									<IconTrash size={16} />
-									<T id="action.delete" />
-								</a>
+								<HasPermission section={STREAMS} permission={MANAGE} hideError>
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
+										}}
+									>
+										<IconPower size={16} />
+										<T id="action.disable" />
+									</a>
+									<div className="dropdown-divider" />
+									<a
+										className="dropdown-item"
+										href="#"
+										onClick={(e) => {
+											e.preventDefault();
+											onDelete?.(info.row.original.id);
+										}}
+									>
+										<IconTrash size={16} />
+										<T id="action.delete" />
+									</a>
+								</HasPermission>
 							</div>
 						</span>
 					);
@@ -175,6 +179,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 					onNew={onNew}
 					isFiltered={isFiltered}
 					color="blue"
+					permissionSection={STREAMS}
 				/>
 			}
 		/>

+ 9 - 7
frontend/src/pages/Nginx/Streams/TableWrapper.tsx

@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
 import { useState } from "react";
 import Alert from "react-bootstrap/Alert";
 import { deleteStream, toggleStream } from "src/api/backend";
-import { Button, LoadingPage } from "src/components";
+import { Button, HasPermission, LoadingPage } from "src/components";
 import { useStreams } from "src/hooks";
 import { T } from "src/locale";
 import { showDeleteConfirmModal, showHelpModal, showStreamModal } from "src/modals";
+import { MANAGE, STREAMS } from "src/modules/Permissions";
 import { showObjectSuccess } from "src/notifications";
 import Table from "./Table";
 
@@ -61,7 +62,6 @@ export default function TableWrapper() {
 								<T id="streams" />
 							</h2>
 						</div>
-
 						<div className="col-md-auto col-sm-12">
 							<div className="ms-auto d-flex flex-wrap btn-list">
 								{data?.length ? (
@@ -81,11 +81,13 @@ export default function TableWrapper() {
 								<Button size="sm" onClick={() => showHelpModal("Streams", "blue")}>
 									<IconHelp size={20} />
 								</Button>
-								{data?.length ? (
-									<Button size="sm" className="btn-blue" onClick={() => showStreamModal("new")}>
-										<T id="object.add" tData={{ object: "stream" }} />
-									</Button>
-								) : null}
+								<HasPermission section={STREAMS} permission={MANAGE} hideError>
+									{data?.length ? (
+										<Button size="sm" className="btn-blue" onClick={() => showStreamModal("new")}>
+											<T id="object.add" tData={{ object: "stream" }} />
+										</Button>
+									) : null}
+								</HasPermission>
 							</div>
 						</div>
 					</div>

+ 2 - 1
frontend/src/pages/Nginx/Streams/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { STREAMS, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const Streams = () => {
 	return (
-		<HasPermission permission="streams" type="view" pageLoading loadingNoLogo>
+		<HasPermission section={STREAMS} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);

+ 2 - 1
frontend/src/pages/Settings/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { ADMIN, VIEW } from "src/modules/Permissions";
 import Layout from "./Layout";
 
 const Settings = () => {
 	return (
-		<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
+		<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>
 			<Layout />
 		</HasPermission>
 	);

+ 2 - 1
frontend/src/pages/Users/index.tsx

@@ -1,9 +1,10 @@
 import { HasPermission } from "src/components";
+import { ADMIN, VIEW } from "src/modules/Permissions";
 import TableWrapper from "./TableWrapper";
 
 const Users = () => {
 	return (
-		<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
+		<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>
 			<TableWrapper />
 		</HasPermission>
 	);