diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs
index 883537d0f..ab64df676 100644
--- a/web/.eslintrc.cjs
+++ b/web/.eslintrc.cjs
@@ -43,6 +43,14 @@ module.exports = {
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ caughtErrorsIgnorePattern: "^_",
+ },
+ ],
"no-console": "error",
"prettier/prettier": [
"warn",
diff --git a/web/login.html b/web/login.html
new file mode 100644
index 000000000..39ca78c3c
--- /dev/null
+++ b/web/login.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ Frigate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/LoginPage.tsx b/web/src/LoginPage.tsx
new file mode 100644
index 000000000..8b1b1decd
--- /dev/null
+++ b/web/src/LoginPage.tsx
@@ -0,0 +1,22 @@
+import { UserAuthForm } from "@/components/AuthForm";
+import Logo from "./components/Logo";
+import { ThemeProvider } from "./context/theme-provider";
+
+function LoginPage() {
+ return (
+
+
+
+ );
+}
+
+export default LoginPage;
diff --git a/web/src/components/AuthForm.tsx b/web/src/components/AuthForm.tsx
new file mode 100644
index 000000000..113ebf508
--- /dev/null
+++ b/web/src/components/AuthForm.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+import { Input } from "./ui/input";
+import { Button } from "./ui/button";
+import ActivityIndicator from "./indicators/activity-indicator";
+import axios from "axios";
+import { Toaster } from "./ui/sonner";
+import { toast } from "sonner";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+} from "@/components/ui/form";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+interface UserAuthFormProps extends React.HTMLAttributes {}
+
+export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ const formSchema = z.object({
+ user: z.string(),
+ password: z.string(),
+ });
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ mode: "onChange",
+ defaultValues: {
+ user: "",
+ password: "",
+ },
+ });
+
+ const onSubmit = async (values: z.infer) => {
+ setIsLoading(true);
+ try {
+ await axios.post(
+ "/api/login",
+ {
+ user: values.user,
+ password: values.password,
+ },
+ {
+ headers: {
+ "X-CSRF-TOKEN": 1,
+ },
+ },
+ );
+ window.location.href = "/";
+ } catch (error) {
+ toast.error("Login failed", {
+ position: "top-center",
+ });
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/web/src/login.tsx b/web/src/login.tsx
new file mode 100644
index 000000000..23ff71e3e
--- /dev/null
+++ b/web/src/login.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import LoginPage from "./LoginPage.tsx";
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);