diff --git a/web/src/components/overlay/CreateUserDialog.tsx b/web/src/components/overlay/CreateUserDialog.tsx new file mode 100644 index 000000000..1216968db --- /dev/null +++ b/web/src/components/overlay/CreateUserDialog.tsx @@ -0,0 +1,111 @@ +import { Button } from "../ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; +import { Input } from "../ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; + +type CreateUserOverlayProps = { + show: boolean; + onCreate: (user: string, password: string) => void; + onCancel: () => void; +}; +export default function CreateUserDialog({ + show, + onCreate, + onCancel, +}: CreateUserOverlayProps) { + const [isLoading, setIsLoading] = useState(false); + + const formSchema = z.object({ + user: z + .string() + .min(1) + .regex(/^[A-Za-z0-9._]+$/, { + message: "Username may only include letters, numbers, . or _", + }), + password: z.string().min(8), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + user: "", + password: "", + }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + await onCreate(values.user, values.password); + form.reset(); + setIsLoading(false); + }; + + return ( + + + + Create User + +
+ + ( + + User + + + + + + )} + /> + ( + + Password + + + + + )} + /> + + + + + +
+
+ ); +} diff --git a/web/src/components/overlay/DeleteUserDialog.tsx b/web/src/components/overlay/DeleteUserDialog.tsx new file mode 100644 index 000000000..a1c0b2a32 --- /dev/null +++ b/web/src/components/overlay/DeleteUserDialog.tsx @@ -0,0 +1,40 @@ +import { Button } from "../ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; + +type SetPasswordProps = { + show: boolean; + onDelete: () => void; + onCancel: () => void; +}; +export default function DeleteUserDialog({ + show, + onDelete, + onCancel, +}: SetPasswordProps) { + return ( + + + + Delete User + +
Are you sure?
+ + + +
+
+ ); +} diff --git a/web/src/components/overlay/SetPasswordDialog.tsx b/web/src/components/overlay/SetPasswordDialog.tsx new file mode 100644 index 000000000..1dfc8dcf9 --- /dev/null +++ b/web/src/components/overlay/SetPasswordDialog.tsx @@ -0,0 +1,51 @@ +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; + +type SetPasswordProps = { + show: boolean; + onSave: (password: string) => void; + onCancel: () => void; +}; +export default function SetPasswordDialog({ + show, + onSave, + onCancel, +}: SetPasswordProps) { + const [password, setPassword] = useState(); + + return ( + + + + Set Password + + setPassword(event.target.value)} + /> + + + + + + ); +} diff --git a/web/src/components/settings/Authentication.tsx b/web/src/components/settings/Authentication.tsx new file mode 100644 index 000000000..15baca614 --- /dev/null +++ b/web/src/components/settings/Authentication.tsx @@ -0,0 +1,163 @@ +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"; +import Heading from "../ui/heading"; +import { User } from "@/types/user"; +import { Button } from "../ui/button"; +import SetPasswordDialog from "../overlay/SetPasswordDialog"; +import axios from "axios"; +import CreateUserDialog from "../overlay/CreateUserDialog"; +import { toast } from "sonner"; +import DeleteUserDialog from "../overlay/DeleteUserDialog"; +import { Card } from "../ui/card"; + +export default function Authentication() { + const { data: config } = 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 [selectedUser, setSelectedUser] = useState(); + + useEffect(() => { + document.title = "Authentication Settings - Frigate"; + }, []); + + const onSavePassword = useCallback((user: string, password: string) => { + axios + .put(`users/${user}/password`, { + password: password, + }) + .then((response) => { + if (response.status == 200) { + // console.log("saved"); + } + }) + .catch((error) => { + if (error.response?.data?.message) { + // console.log("error"); + } else { + // console.log("error"); + } + }); + }, []); + + const onCreate = async (user: string, password: string) => { + try { + await axios.post("users", { + username: user, + password: password, + }); + setShowCreate(false); + mutateUsers((users) => { + users?.push({ username: user }); + return users; + }, false); + } catch (error) { + toast.error("Error creating user. Check server logs.", { + position: "top-center", + }); + } + }; + + const onDelete = async (user: string) => { + try { + await axios.delete(`users/${user}`); + setShowDelete(false); + mutateUsers((users) => { + return users?.filter((u) => { + return u.username !== user; + }); + }, false); + } catch (error) { + toast.error("Error deleting user. Check server logs.", { + position: "top-center", + }); + } + }; + + if (!config || !users) { + return ; + } + + return ( +
+ +
+ + Users + +
+ +
+
+ {users.map((u) => ( + +
+
+ {u.username} +
+
+ + +
+
+
+ ))} +
+
+ { + setShowSetPassword(false); + }} + onSave={(password) => { + onSavePassword(selectedUser!, password); + }} + /> + { + setShowDelete(false); + }} + onDelete={() => { + onDelete(selectedUser!); + }} + /> + { + setShowCreate(false); + }} + /> +
+ ); +} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 083e2b891..590bc2096 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -33,6 +33,7 @@ import { PolygonType } from "@/types/canvas"; import ObjectSettings from "@/components/settings/ObjectSettings"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import scrollIntoView from "scroll-into-view-if-needed"; +import Authentication from "@/components/settings/Authentication"; export default function Settings() { const settingsViews = [ @@ -40,6 +41,7 @@ export default function Settings() { "masks / zones", "motion tuner", "debug", + "authentication", ] as const; type SettingsType = (typeof settingsViews)[number]; @@ -169,6 +171,7 @@ export default function Settings() { setUnsavedChanges={setUnsavedChanges} /> )} + {page == "authentication" && } {confirmationDialogOpen && (