2024-05-18 19:36:13 +03:00
|
|
|
import { useCallback, useEffect, 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";
|
2024-05-29 17:01:39 +03:00
|
|
|
import Heading from "@/components/ui/heading";
|
2024-05-18 19:36:13 +03:00
|
|
|
import { User } from "@/types/user";
|
2024-05-29 17:01:39 +03:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import SetPasswordDialog from "@/components/overlay/SetPasswordDialog";
|
2024-05-18 19:36:13 +03:00
|
|
|
import axios from "axios";
|
2024-05-29 17:01:39 +03:00
|
|
|
import CreateUserDialog from "@/components/overlay/CreateUserDialog";
|
2024-05-18 19:36:13 +03:00
|
|
|
import { toast } from "sonner";
|
2024-05-29 17:01:39 +03:00
|
|
|
import DeleteUserDialog from "@/components/overlay/DeleteUserDialog";
|
2024-05-18 22:05:28 +03:00
|
|
|
import { HiTrash } from "react-icons/hi";
|
|
|
|
|
import { FaUserEdit } from "react-icons/fa";
|
2025-03-08 19:01:08 +03:00
|
|
|
import { 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";
|
2024-05-18 19:36:13 +03:00
|
|
|
|
2024-05-29 17:01:39 +03:00
|
|
|
export default function AuthenticationView() {
|
2024-05-18 19:36:13 +03:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config");
|
|
|
|
|
const { data: users, mutate: mutateUsers } = useSWR<User[]>("users");
|
|
|
|
|
|
|
|
|
|
const [showSetPassword, setShowSetPassword] = useState(false);
|
|
|
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
|
|
|
const [showDelete, setShowDelete] = useState(false);
|
2025-03-08 19:01:08 +03:00
|
|
|
const [showRoleChange, setShowRoleChange] = useState(false);
|
2024-05-18 19:36:13 +03:00
|
|
|
|
|
|
|
|
const [selectedUser, setSelectedUser] = useState<string>();
|
2025-03-08 19:01:08 +03:00
|
|
|
const [selectedUserRole, setSelectedUserRole] = useState<
|
|
|
|
|
"admin" | "viewer"
|
|
|
|
|
>();
|
2024-05-18 19:36:13 +03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
document.title = "Authentication Settings - Frigate";
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const onSavePassword = useCallback((user: string, password: string) => {
|
|
|
|
|
axios
|
2025-03-08 19:01:08 +03:00
|
|
|
.put(`users/${user}/password`, { password })
|
2024-05-18 19:36:13 +03:00
|
|
|
.then((response) => {
|
2025-03-08 19:01:08 +03:00
|
|
|
if (response.status === 200) {
|
2024-05-18 19:36:13 +03:00
|
|
|
setShowSetPassword(false);
|
2025-03-08 19:01:08 +03:00
|
|
|
toast.success("Password updated successfully", {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
2024-05-18 19:36:13 +03:00
|
|
|
}
|
|
|
|
|
})
|
2025-03-08 19:01:08 +03:00
|
|
|
.catch((error) => {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.message ||
|
|
|
|
|
error.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
|
|
|
|
toast.error(`Failed to save password: ${errorMessage}`, {
|
2024-05-18 19:36:13 +03:00
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-03-08 19:01:08 +03:00
|
|
|
const onCreate = (
|
|
|
|
|
user: string,
|
|
|
|
|
password: string,
|
|
|
|
|
role: "admin" | "viewer",
|
|
|
|
|
) => {
|
|
|
|
|
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(`User ${user} created successfully`, {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.message ||
|
|
|
|
|
error.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
|
|
|
|
toast.error(`Failed to create user: ${errorMessage}`, {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
2024-05-18 19:36:13 +03:00
|
|
|
});
|
2025-03-08 19:01:08 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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(`User ${user} deleted successfully`, {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.message ||
|
|
|
|
|
error.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
|
|
|
|
toast.error(`Failed to delete user: ${errorMessage}`, {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
2024-05-18 19:36:13 +03:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-08 19:01:08 +03:00
|
|
|
const onChangeRole = (user: string, newRole: "admin" | "viewer") => {
|
|
|
|
|
if (user === "admin") return; // Prevent role change for 'admin'
|
|
|
|
|
|
|
|
|
|
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(`Role updated for ${user}`, {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.message ||
|
|
|
|
|
error.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
|
|
|
|
toast.error(`Failed to update role: ${errorMessage}`, {
|
|
|
|
|
position: "top-center",
|
2024-05-18 19:36:13 +03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!config || !users) {
|
2025-03-08 19:01:08 +03:00
|
|
|
return (
|
|
|
|
|
<div className="flex h-full w-full items-center justify-center">
|
|
|
|
|
<ActivityIndicator />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2024-05-18 19:36:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex size-full flex-col md:flex-row">
|
|
|
|
|
<Toaster position="top-center" closeButton={true} />
|
2024-06-03 21:43:30 +03:00
|
|
|
<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">
|
2025-03-08 19:01:08 +03:00
|
|
|
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
|
|
|
|
<div className="flex flex-col items-start">
|
|
|
|
|
<Heading as="h3" className="my-2">
|
|
|
|
|
User Management
|
|
|
|
|
</Heading>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Manage this Frigate instance's user accounts.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2024-05-18 19:36:13 +03:00
|
|
|
<Button
|
2025-03-08 19:01:08 +03:00
|
|
|
className="flex items-center gap-2 self-start sm:self-auto"
|
2024-10-23 01:07:42 +03:00
|
|
|
aria-label="Add a new user"
|
2024-05-18 22:05:28 +03:00
|
|
|
variant="default"
|
2025-03-08 19:01:08 +03:00
|
|
|
onClick={() => setShowCreate(true)}
|
2024-05-18 19:36:13 +03:00
|
|
|
>
|
2025-03-08 19:01:08 +03:00
|
|
|
<LuPlus className="size-4" />
|
2024-05-18 19:36:13 +03:00
|
|
|
Add User
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-03-08 19:01:08 +03:00
|
|
|
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
|
|
|
|
|
<div className="h-full overflow-auto">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader className="sticky top-0 bg-muted/50">
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead className="w-[250px]">Username</TableHead>
|
|
|
|
|
<TableHead>Role</TableHead>
|
|
|
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{users.length === 0 ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={3} className="h-24 text-center">
|
|
|
|
|
No users found.
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
) : (
|
|
|
|
|
users.map((user) => (
|
|
|
|
|
<TableRow key={user.username} className="group">
|
|
|
|
|
<TableCell className="font-medium">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{user.username === "admin" ? (
|
|
|
|
|
<LuShield className="size-4 text-primary" />
|
|
|
|
|
) : (
|
|
|
|
|
<LuUserCog className="size-4 text-primary-variant" />
|
|
|
|
|
)}
|
|
|
|
|
{user.username}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Badge
|
|
|
|
|
variant={
|
|
|
|
|
user.role === "admin" ? "default" : "outline"
|
|
|
|
|
}
|
|
|
|
|
className={
|
|
|
|
|
user.role === "admin"
|
|
|
|
|
? "bg-primary/20 text-primary hover:bg-primary/30"
|
|
|
|
|
: ""
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{user.role || "viewer"}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right">
|
|
|
|
|
<TooltipProvider>
|
|
|
|
|
<div className="flex items-center justify-end gap-2">
|
|
|
|
|
{user.username !== "admin" && (
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="h-8 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelectedUser(user.username);
|
|
|
|
|
setSelectedUserRole(
|
|
|
|
|
(user.role as "admin" | "viewer") ||
|
|
|
|
|
"viewer",
|
|
|
|
|
);
|
|
|
|
|
setShowRoleChange(true);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<LuUserCog className="size-3.5" />
|
|
|
|
|
<span className="ml-1.5 hidden sm:inline-block">
|
|
|
|
|
Role
|
|
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
<p>Change user role</p>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="h-8 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowSetPassword(true);
|
|
|
|
|
setSelectedUser(user.username);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<FaUserEdit className="size-3.5" />
|
|
|
|
|
<span className="ml-1.5 hidden sm:inline-block">
|
|
|
|
|
Password
|
|
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
<p>Update password</p>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
{user.username !== "admin" && (
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
className="h-8 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowDelete(true);
|
|
|
|
|
setSelectedUser(user.username);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<HiTrash className="size-3.5" />
|
|
|
|
|
<span className="ml-1.5 hidden sm:inline-block">
|
|
|
|
|
Delete
|
|
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
<p>Delete user</p>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-05-18 19:36:13 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-03-08 19:01:08 +03:00
|
|
|
|
2024-05-18 19:36:13 +03:00
|
|
|
<SetPasswordDialog
|
|
|
|
|
show={showSetPassword}
|
2025-03-08 19:01:08 +03:00
|
|
|
onCancel={() => setShowSetPassword(false)}
|
|
|
|
|
onSave={(password) => onSavePassword(selectedUser!, password)}
|
2024-05-18 19:36:13 +03:00
|
|
|
/>
|
|
|
|
|
<DeleteUserDialog
|
|
|
|
|
show={showDelete}
|
2025-03-08 19:01:08 +03:00
|
|
|
username={selectedUser ?? "this user"}
|
|
|
|
|
onCancel={() => setShowDelete(false)}
|
|
|
|
|
onDelete={() => onDelete(selectedUser!)}
|
2024-05-18 19:36:13 +03:00
|
|
|
/>
|
|
|
|
|
<CreateUserDialog
|
|
|
|
|
show={showCreate}
|
|
|
|
|
onCreate={onCreate}
|
2025-03-08 19:01:08 +03:00
|
|
|
onCancel={() => setShowCreate(false)}
|
2024-05-18 19:36:13 +03:00
|
|
|
/>
|
2025-03-08 19:01:08 +03:00
|
|
|
{selectedUser && selectedUserRole && (
|
|
|
|
|
<RoleChangeDialog
|
|
|
|
|
show={showRoleChange}
|
|
|
|
|
username={selectedUser}
|
|
|
|
|
currentRole={selectedUserRole}
|
|
|
|
|
onSave={(role) => onChangeRole(selectedUser, role)}
|
|
|
|
|
onCancel={() => setShowRoleChange(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2024-05-18 19:36:13 +03:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|