import { useCallback, useEffect, useMemo, useState } from "react"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { FrigateConfig } from "@/types/frigateConfig"; import { Toaster } from "@/components/ui/sonner"; import useSWR from "swr"; import Heading from "@/components/ui/heading"; import { User } from "@/types/user"; import { Button } from "@/components/ui/button"; import SetPasswordDialog from "@/components/overlay/SetPasswordDialog"; import axios from "axios"; import CreateUserDialog from "@/components/overlay/CreateUserDialog"; import { toast } from "sonner"; import DeleteUserDialog from "@/components/overlay/DeleteUserDialog"; import { HiTrash } from "react-icons/hi"; import { FaUserEdit } from "react-icons/fa"; import { LuPencil, LuPlus, LuShield, LuUserCog } from "react-icons/lu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import RoleChangeDialog from "@/components/overlay/RoleChangeDialog"; import CreateRoleDialog from "@/components/overlay/CreateRoleDialog"; import EditRoleCamerasDialog from "@/components/overlay/EditRoleCamerasDialog"; import { useTranslation } from "react-i18next"; import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog"; import { Separator } from "@/components/ui/separator"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; type AuthenticationViewProps = { section?: "users" | "roles"; }; export default function AuthenticationView({ section, }: AuthenticationViewProps) { const { t } = useTranslation("views/settings"); const { data: config, mutate: updateConfig } = useSWR("config"); const { data: users, mutate: mutateUsers } = useSWR("users"); const [showSetPassword, setShowSetPassword] = useState(false); const [showCreate, setShowCreate] = useState(false); const [showDelete, setShowDelete] = useState(false); const [showRoleChange, setShowRoleChange] = useState(false); const [showCreateRole, setShowCreateRole] = useState(false); const [showEditRole, setShowEditRole] = useState(false); const [showDeleteRole, setShowDeleteRole] = useState(false); const [selectedUser, setSelectedUser] = useState(); const [selectedUserRole, setSelectedUserRole] = useState(); const [selectedRole, setSelectedRole] = useState(); const [currentRoleCameras, setCurrentRoleCameras] = useState([]); const [selectedRoleForDelete, setSelectedRoleForDelete] = useState(); useEffect(() => { document.title = t("documentTitle.authentication"); }, [t]); const onSavePassword = useCallback( (user: string, password: string) => { axios .put(`users/${user}/password`, { password }) .then((response) => { if (response.status === 200) { setShowSetPassword(false); toast.success(t("users.toast.success.updatePassword"), { position: "top-center", }); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("users.toast.error.setPasswordFailed", { errorMessage, }), { position: "top-center", }, ); }); }, [t], ); const onCreate = (user: string, password: string, role: string) => { axios .post("users", { username: user, password, role }) .then((response) => { if (response.status === 200 || response.status === 201) { setShowCreate(false); mutateUsers((users) => { users?.push({ username: user, role: role }); return users; }, false); toast.success(t("users.toast.success.createUser", { user }), { position: "top-center", }); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("users.toast.error.createUserFailed", { errorMessage, }), { position: "top-center", }, ); }); }; const onDelete = (user: string) => { axios .delete(`users/${user}`) .then((response) => { if (response.status === 200) { setShowDelete(false); mutateUsers( (users) => users?.filter((u) => u.username !== user), false, ); toast.success(t("users.toast.success.deleteUser", { user }), { position: "top-center", }); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("users.toast.error.deleteUserFailed", { errorMessage, }), { position: "top-center", }, ); }); }; const onChangeRole = (user: string, newRole: string) => { if (user === "admin") return; axios .put(`users/${user}/role`, { role: newRole }) .then((response) => { if (response.status === 200) { setShowRoleChange(false); mutateUsers( (users) => users?.map((u) => u.username === user ? { ...u, role: newRole } : u, ), false, ); toast.success(t("users.toast.success.roleUpdated", { user }), { position: "top-center", }); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("users.toast.error.roleUpdateFailed", { errorMessage, }), { position: "top-center", }, ); }); }; type ConfigSetBody = { requires_restart: number; config_data: { auth: { roles: { [key: string]: string[] | string; }; }; }; update_topic?: string; }; const onCreateRole = useCallback( async (role: string, cameras: string[]) => { const configBody: ConfigSetBody = { requires_restart: 0, config_data: { auth: { roles: { [role]: cameras, }, }, }, update_topic: "config/auth", }; return axios .put("config/set", configBody) .then((response) => { if (response.status === 200) { setShowCreateRole(false); updateConfig(); toast.success(t("roles.toast.success.createRole", { role }), { position: "top-center", }); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("roles.toast.error.createRoleFailed", { errorMessage, }), { position: "top-center", }, ); throw error; }); }, [t, updateConfig], ); const onEditRoleCameras = useCallback( async (cameras: string[]) => { if (!selectedRole) return; const configBody: ConfigSetBody = { requires_restart: 0, config_data: { auth: { roles: { [selectedRole]: cameras, }, }, }, update_topic: "config/auth", }; return axios .put("config/set", configBody) .then((response) => { if (response.status === 200) { setShowEditRole(false); setSelectedRole(undefined); setCurrentRoleCameras([]); updateConfig(); toast.success( t("roles.toast.success.updateCameras", { role: selectedRole }), { position: "top-center", }, ); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("roles.toast.error.updateCamerasFailed", { errorMessage, }), { position: "top-center", }, ); throw error; }); }, [t, selectedRole, updateConfig], ); const onDeleteRole = useCallback( async (role: string) => { // Update users assigned to this role to 'viewer' const usersToUpdate = users?.filter((user) => user.role === role) || []; if (usersToUpdate.length > 0) { Promise.all( usersToUpdate.map((user) => axios.put(`users/${user.username}/role`, { role: "viewer" }), ), ) .then(() => { mutateUsers( (users) => users?.map((u) => u.role === role ? { ...u, role: "viewer" } : u, ), false, ); toast.success( t("roles.toast.success.userRolesUpdated", { count: usersToUpdate.length, }), { position: "top-center" }, ); }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("roles.toast.error.userUpdateFailed", { errorMessage }), { position: "top-center" }, ); }); } // Now delete the role from config const configBody: ConfigSetBody = { requires_restart: 0, config_data: { auth: { roles: { [role]: "", }, }, }, update_topic: "config/auth", }; return axios .put("config/set", configBody) .then((response) => { if (response.status === 200) { setShowDeleteRole(false); setSelectedRoleForDelete(""); updateConfig(); toast.success(t("roles.toast.success.deleteRole", { role }), { position: "top-center", }); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("roles.toast.error.deleteRoleFailed", { errorMessage, }), { position: "top-center", }, ); throw error; }); }, [t, updateConfig, users, mutateUsers], ); const roles = config?.auth?.roles ? Object.entries(config.auth.roles) .filter(([name]) => name !== "admin") .map(([name, data]) => ({ name, cameras: Array.isArray(data) ? data : [], })) : []; const availableRoles = useMemo(() => { return config ? [...Object.keys(config.auth?.roles || {})] : []; }, [config]); if (!config || !users) { return (
); } // 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.noUsers")} ) : ( users.map((user) => (
{user.username === "admin" ? ( ) : ( )} {user.username}
{t("role." + (user.role || "viewer"), { ns: "common", })}
{user.username !== "admin" && (

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

)}

{t("users.updatePassword")}

{user.username !== "admin" && (

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

)}
)) )}
setShowSetPassword(false)} onSave={(password) => onSavePassword(selectedUser!, password)} /> setShowDelete(false)} onDelete={() => onDelete(selectedUser!)} /> setShowCreate(false)} /> {selectedUser && selectedUserRole && ( onChangeRole(selectedUser!, role)} 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")}

)}
)) )}
setShowCreateRole(false)} /> {selectedRole && ( { setShowEditRole(false); setSelectedRole(undefined); setCurrentRoleCameras([]); }} /> )} { setShowDeleteRole(false); setSelectedRoleForDelete(""); }} onDelete={async () => { if (selectedRoleForDelete) { try { await onDeleteRole(selectedRoleForDelete); } catch (error) { // Error handling is already done in onDeleteRole } } }} /> ); return (
{section === "users" && UsersSection} {section === "roles" && RolesSection} {!section && ( <> {UsersSection} {RolesSection} )}
); }