split users and roles into separate tabs in settings

This commit is contained in:
Josh Hawkins 2025-09-11 12:17:30 -05:00
parent e888a08fd1
commit 56dc5773e1
5 changed files with 335 additions and 297 deletions

View File

@ -564,15 +564,15 @@
}, },
"roles": { "roles": {
"management": { "management": {
"title": "Role Management", "title": "Viewer Role Management",
"desc": "Manage roles and their camera access permissions for this Frigate instance." "desc": "Manage custom viewer roles and their camera access permissions for this Frigate instance."
}, },
"addRole": "Add Role", "addRole": "Add Role",
"table": { "table": {
"role": "Role", "role": "Role",
"cameras": "Cameras", "cameras": "Cameras",
"actions": "Actions", "actions": "Actions",
"noRoles": "No roles found.", "noRoles": "No custom roles found.",
"editCameras": "Edit Cameras", "editCameras": "Edit Cameras",
"deleteRole": "Delete Role" "deleteRole": "Delete Role"
}, },
@ -581,7 +581,7 @@
"createRole": "Role {{role}} created successfully", "createRole": "Role {{role}} created successfully",
"updateCameras": "Cameras updated for role {{role}}", "updateCameras": "Cameras updated for role {{role}}",
"deleteRole": "Role {{role}} deleted successfully", "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": { "error": {
"createRoleFailed": "Failed to create role: {{errorMessage}}", "createRoleFailed": "Failed to create role: {{errorMessage}}",
@ -601,7 +601,7 @@
}, },
"deleteRole": { "deleteRole": {
"title": "Delete Role", "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 <strong>{{role}}</strong>?", "warn": "Are you sure you want to delete <strong>{{role}}</strong>?",
"deleting": "Deleting..." "deleting": "Deleting..."
}, },

View File

@ -33,7 +33,8 @@ import CameraSettingsView from "@/views/settings/CameraSettingsView";
import ObjectSettingsView from "@/views/settings/ObjectSettingsView"; import ObjectSettingsView from "@/views/settings/ObjectSettingsView";
import MotionTunerView from "@/views/settings/MotionTunerView"; import MotionTunerView from "@/views/settings/MotionTunerView";
import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; 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 NotificationView from "@/views/settings/NotificationsSettingsView";
import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView"; import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView";
import UiSettingsView from "@/views/settings/UiSettingsView"; import UiSettingsView from "@/views/settings/UiSettingsView";
@ -57,6 +58,7 @@ const allSettingsViews = [
"triggers", "triggers",
"debug", "debug",
"users", "users",
"roles",
"notifications", "notifications",
"frigateplus", "frigateplus",
] as const; ] as const;
@ -288,7 +290,8 @@ export default function Settings() {
setUnsavedChanges={setUnsavedChanges} setUnsavedChanges={setUnsavedChanges}
/> />
)} )}
{page == "users" && <AuthenticationView />} {page == "users" && <UsersView />}
{page == "roles" && <RolesView />}
{page == "notifications" && ( {page == "notifications" && (
<NotificationView setUnsavedChanges={setUnsavedChanges} /> <NotificationView setUnsavedChanges={setUnsavedChanges} />
)} )}

View File

@ -38,7 +38,13 @@ import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; 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 { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } = const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config"); useSWR<FrigateConfig>("config");
@ -377,7 +383,9 @@ export default function AuthenticationView() {
const roles = useMemo(() => { const roles = useMemo(() => {
return config?.auth?.roles return config?.auth?.roles
? Object.entries(config.auth.roles).map(([name, data]) => ({ ? Object.entries(config.auth.roles)
.filter(([name]) => name !== "admin")
.map(([name, data]) => ({
name, name,
cameras: Array.isArray(data) ? data : [], cameras: Array.isArray(data) ? data : [],
})) }))
@ -396,10 +404,9 @@ export default function AuthenticationView() {
); );
} }
return ( // Users section
<div className="flex size-full flex-col"> const UsersSection = (
<Toaster position="top-center" closeButton={true} /> <>
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
<div className="mb-5 flex flex-row items-center justify-between gap-2"> <div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
@ -557,9 +564,38 @@ export default function AuthenticationView() {
</div> </div>
</div> </div>
</div> </div>
<SetPasswordDialog
show={showSetPassword}
onCancel={() => setShowSetPassword(false)}
onSave={(password) => onSavePassword(selectedUser!, password)}
/>
<DeleteUserDialog
show={showDelete}
username={selectedUser ?? "this user"}
onCancel={() => setShowDelete(false)}
onDelete={() => onDelete(selectedUser!)}
/>
<CreateUserDialog
show={showCreate}
onCreate={onCreate}
onCancel={() => setShowCreate(false)}
/>
{selectedUser && selectedUserRole && (
<RoleChangeDialog
show={showRoleChange}
username={selectedUser}
currentRole={selectedUserRole}
availableRoles={availableRoles}
onSave={(role) => onChangeRole(selectedUser!, role)}
onCancel={() => setShowRoleChange(false)}
/>
)}
</>
);
<Separator className="my-6 flex bg-secondary" /> // Roles section
const RolesSection = (
<>
<div className="mb-5 flex flex-row items-center justify-between gap-2"> <div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
@ -707,34 +743,6 @@ export default function AuthenticationView() {
</div> </div>
</div> </div>
</div> </div>
</div>
<SetPasswordDialog
show={showSetPassword}
onCancel={() => setShowSetPassword(false)}
onSave={(password) => onSavePassword(selectedUser!, password)}
/>
<DeleteUserDialog
show={showDelete}
username={selectedUser ?? "this user"}
onCancel={() => setShowDelete(false)}
onDelete={() => onDelete(selectedUser!)}
/>
<CreateUserDialog
show={showCreate}
onCreate={onCreate}
onCancel={() => setShowCreate(false)}
/>
{selectedUser && selectedUserRole && (
<RoleChangeDialog
show={showRoleChange}
username={selectedUser}
currentRole={selectedUserRole}
availableRoles={availableRoles}
onSave={(role) => onChangeRole(selectedUser!, role)}
onCancel={() => setShowRoleChange(false)}
/>
)}
<CreateRoleDialog <CreateRoleDialog
show={showCreateRole} show={showCreateRole}
config={config} config={config}
@ -772,6 +780,23 @@ export default function AuthenticationView() {
} }
}} }}
/> />
</>
);
return (
<div className="flex size-full flex-col">
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
{section === "users" && UsersSection}
{section === "roles" && RolesSection}
{!section && (
<>
{UsersSection}
<Separator className="my-6 flex bg-secondary" />
{RolesSection}
</>
)}
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,5 @@
import AuthenticationView from "./AuthenticationView";
export default function RolesView() {
return <AuthenticationView section="roles" />;
}

View File

@ -0,0 +1,5 @@
import AuthenticationView from "./AuthenticationView";
export default function UsersView() {
return <AuthenticationView section="users" />;
}