From 56dc5773e1ff95c74740a5ef3b27e0f75a9f7c2e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:17:30 -0500 Subject: [PATCH] split users and roles into separate tabs in settings --- web/public/locales/en/views/settings.json | 10 +- web/src/pages/Settings.tsx | 7 +- web/src/views/settings/AuthenticationView.tsx | 605 +++++++++--------- web/src/views/settings/RolesView.tsx | 5 + web/src/views/settings/UsersView.tsx | 5 + 5 files changed, 335 insertions(+), 297 deletions(-) create mode 100644 web/src/views/settings/RolesView.tsx create mode 100644 web/src/views/settings/UsersView.tsx diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index d0ae5700d..7d31180a9 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -564,15 +564,15 @@ }, "roles": { "management": { - "title": "Role Management", - "desc": "Manage roles and their camera access permissions for this Frigate instance." + "title": "Viewer Role Management", + "desc": "Manage custom viewer roles and their camera access permissions for this Frigate instance." }, "addRole": "Add Role", "table": { "role": "Role", "cameras": "Cameras", "actions": "Actions", - "noRoles": "No roles found.", + "noRoles": "No custom roles found.", "editCameras": "Edit Cameras", "deleteRole": "Delete Role" }, @@ -581,7 +581,7 @@ "createRole": "Role {{role}} created successfully", "updateCameras": "Cameras updated for role {{role}}", "deleteRole": "Role {{role}} deleted successfully", - "userRolesUpdated": "{{count}} user(s) assigned to this role have been updated to 'viewer'." + "userRolesUpdated": "{{count}} user(s) assigned to this role have been updated to 'viewer', which has access to all cameras." }, "error": { "createRoleFailed": "Failed to create role: {{errorMessage}}", @@ -601,7 +601,7 @@ }, "deleteRole": { "title": "Delete Role", - "desc": "This action cannot be undone. This will permanently delete the role and assign any users with this role to the 'viewer' role.", + "desc": "This action cannot be undone. This will permanently delete the role and assign any users with this role to the 'viewer' role, which will give viewer access to all cameras.", "warn": "Are you sure you want to delete {{role}}?", "deleting": "Deleting..." }, diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index edbd304d8..53b20eab8 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -33,7 +33,8 @@ import CameraSettingsView from "@/views/settings/CameraSettingsView"; import ObjectSettingsView from "@/views/settings/ObjectSettingsView"; import MotionTunerView from "@/views/settings/MotionTunerView"; import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; -import AuthenticationView from "@/views/settings/AuthenticationView"; +import UsersView from "@/views/settings/UsersView"; +import RolesView from "@/views/settings/RolesView"; import NotificationView from "@/views/settings/NotificationsSettingsView"; import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView"; import UiSettingsView from "@/views/settings/UiSettingsView"; @@ -57,6 +58,7 @@ const allSettingsViews = [ "triggers", "debug", "users", + "roles", "notifications", "frigateplus", ] as const; @@ -288,7 +290,8 @@ export default function Settings() { setUnsavedChanges={setUnsavedChanges} /> )} - {page == "users" && } + {page == "users" && } + {page == "roles" && } {page == "notifications" && ( )} diff --git a/web/src/views/settings/AuthenticationView.tsx b/web/src/views/settings/AuthenticationView.tsx index 9c9920b69..6b60f1256 100644 --- a/web/src/views/settings/AuthenticationView.tsx +++ b/web/src/views/settings/AuthenticationView.tsx @@ -38,7 +38,13 @@ import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog"; import { Separator } from "@/components/ui/separator"; import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; -export default function AuthenticationView() { +type AuthenticationViewProps = { + section?: "users" | "roles"; +}; + +export default function AuthenticationView({ + section, +}: AuthenticationViewProps) { const { t } = useTranslation("views/settings"); const { data: config, mutate: updateConfig } = useSWR("config"); @@ -377,10 +383,12 @@ export default function AuthenticationView() { const roles = useMemo(() => { return config?.auth?.roles - ? Object.entries(config.auth.roles).map(([name, data]) => ({ - name, - cameras: Array.isArray(data) ? data : [], - })) + ? Object.entries(config.auth.roles) + .filter(([name]) => name !== "admin") + .map(([name, data]) => ({ + name, + cameras: Array.isArray(data) ? data : [], + })) : []; }, [config]); @@ -396,319 +404,166 @@ export default function AuthenticationView() { ); } - return ( -
- -
-
-
- - {t("users.management.title")} - -

- {t("users.management.desc")} -

-
- + // Users section + const UsersSection = ( + <> +
+
+ + {t("users.management.title")} + +

+ {t("users.management.desc")} +

-
-
-
- - + + +
+
+
+
+ + + + {t("users.table.username")} + + {t("users.table.role")} + + {t("users.table.actions")} + + + + + {users.length === 0 ? ( - - {t("users.table.username")} - - {t("users.table.role")} - - {t("users.table.actions")} - + + {t("users.table.noUsers")} + - - - {users.length === 0 ? ( - - - {t("users.table.noUsers")} + ) : ( + users.map((user) => ( + + +
+ {user.username === "admin" ? ( + + ) : ( + + )} + {user.username} +
-
- ) : ( - users.map((user) => ( - - -
- {user.username === "admin" ? ( - - ) : ( - - )} - {user.username} -
-
- - - {t("role." + (user.role || "viewer"), { - ns: "common", - })} - - - - -
- {user.username !== "admin" && - user.username !== "viewer" && ( - - - - - -

{t("users.table.changeRole")}

-
-
- )} - - - - - - -

{t("users.updatePassword")}

-
-
- - {user.username !== "admin" && ( + + + {t("role." + (user.role || "viewer"), { + ns: "common", + })} + + + + +
+ {user.username !== "admin" && + user.username !== "viewer" && ( -

{t("users.table.deleteUser")}

+

{t("users.table.changeRole")}

)} -
-
-
- - )) - )} - -
-
-
-
- + + + + + +

{t("users.updatePassword")}

+
+
-
-
- - {t("roles.management.title")} - -

- {t("roles.management.desc")} -

-
- -
-
-
-
- - - - - {t("roles.table.role")} - - {t("roles.table.cameras")} - - {t("roles.table.actions")} - - - - - {roles.length === 0 ? ( - - - {t("roles.table.noRoles")} + {user.username !== "admin" && ( + + + + + +

{t("users.table.deleteUser")}

+
+
+ )} + +
- ) : ( - roles.map((roleData) => ( - - - {roleData.name} - - - {roleData.cameras.length === 0 ? ( - - {t("menu.live.allCameras", { ns: "common" })} - - ) : roleData.cameras.length > 5 ? ( - - {roleData.cameras.length} cameras - - ) : ( -
- {roleData.cameras.map((camera) => ( - - - - ))} -
- )} -
- - -
- {roleData.name !== "admin" && - roleData.name !== "viewer" && ( - <> - - - - - -

{t("roles.table.editCameras")}

-
-
- - - - - - -

{t("roles.table.deleteRole")}

-
-
- - )} -
-
-
-
- )) - )} -
-
-
+ )) + )} + +
- setShowSetPassword(false)} @@ -735,6 +590,159 @@ export default function AuthenticationView() { onCancel={() => setShowRoleChange(false)} /> )} + + ); + + // Roles section + const RolesSection = ( + <> +
+
+ + {t("roles.management.title")} + +

+ {t("roles.management.desc")} +

+
+ +
+
+
+
+ + + + + {t("roles.table.role")} + + {t("roles.table.cameras")} + + {t("roles.table.actions")} + + + + + {roles.length === 0 ? ( + + + {t("roles.table.noRoles")} + + + ) : ( + roles.map((roleData) => ( + + + {roleData.name} + + + {roleData.cameras.length === 0 ? ( + + {t("menu.live.allCameras", { ns: "common" })} + + ) : roleData.cameras.length > 5 ? ( + + {roleData.cameras.length} cameras + + ) : ( +
+ {roleData.cameras.map((camera) => ( + + + + ))} +
+ )} +
+ + +
+ {roleData.name !== "admin" && + roleData.name !== "viewer" && ( + <> + + + + + +

{t("roles.table.editCameras")}

+
+
+ + + + + + +

{t("roles.table.deleteRole")}

+
+
+ + )} +
+
+
+
+ )) + )} +
+
+
+
+
+ + ); + + return ( +
+ +
+ {section === "users" && UsersSection} + {section === "roles" && RolesSection} + {!section && ( + <> + {UsersSection} + + {RolesSection} + + )} +
); } diff --git a/web/src/views/settings/RolesView.tsx b/web/src/views/settings/RolesView.tsx new file mode 100644 index 000000000..5afbf2eb0 --- /dev/null +++ b/web/src/views/settings/RolesView.tsx @@ -0,0 +1,5 @@ +import AuthenticationView from "./AuthenticationView"; + +export default function RolesView() { + return ; +} diff --git a/web/src/views/settings/UsersView.tsx b/web/src/views/settings/UsersView.tsx new file mode 100644 index 000000000..a30d7356f --- /dev/null +++ b/web/src/views/settings/UsersView.tsx @@ -0,0 +1,5 @@ +import AuthenticationView from "./AuthenticationView"; + +export default function UsersView() { + return ; +}