From 00f0def462c36f5d95ce8244634e08774e2bd6fa Mon Sep 17 00:00:00 2001 From: april Date: Fri, 5 Jan 2024 17:03:39 -0600 Subject: [PATCH] Implement password updating and show login errors --- web/app/root.tsx | 69 +++--------------- web/app/routes/logbook.me/route.tsx | 109 ++++++++++++++++++++++++++-- web/app/routes/logbook/route.tsx | 42 ++++++++++- web/app/routes/login/route.tsx | 51 +++++++++++-- web/app/util/auth.tsx | 9 ++- 5 files changed, 204 insertions(+), 76 deletions(-) diff --git a/web/app/root.tsx b/web/app/root.tsx index b129019..ef174ce 100644 --- a/web/app/root.tsx +++ b/web/app/root.tsx @@ -14,20 +14,9 @@ import { isRouteErrorResponse, json, useLoaderData, - useNavigate, useRouteError, } from "@remix-run/react"; -import { - HydrationBoundary, - QueryCache, - QueryClient, - QueryClientProvider, -} from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -import { useDehydratedState } from "use-dehydrated-state"; - import { Button, ColorSchemeScript, @@ -41,8 +30,6 @@ import { ModalsProvider } from "@mantine/modals"; import { IconRocket } from "@tabler/icons-react"; import { AuthProvider } from "./util/auth"; -import { useState } from "react"; -import { AxiosError } from "axios"; import { ApiProvider } from "./util/api"; export const links: LinksFunction = () => [ @@ -117,33 +104,6 @@ export async function loader() { } export default function App() { - const navigate = useNavigate(); - const [queryClient] = useState( - () => - new QueryClient({ - queryCache: new QueryCache({ - onError: (error: Error) => { - if (error instanceof AxiosError && error.response?.status === 401) { - navigate("/login"); - } - }, - }), - defaultOptions: { - queries: { - staleTime: 1000, - retry: (failureCount, error: Error) => { - return ( - !error || - (error instanceof AxiosError && error.response?.status !== 401) - ); - }, - }, - }, - }) - ); - - const dehydratedState = useDehydratedState(); - const data = useLoaderData(); return ( @@ -156,23 +116,18 @@ export default function App() { - - - - - - - - - - - - - - - - - + + + + + + + + + + + + ); diff --git a/web/app/routes/logbook.me/route.tsx b/web/app/routes/logbook.me/route.tsx index 99c9a8d..1174b7d 100644 --- a/web/app/routes/logbook.me/route.tsx +++ b/web/app/routes/logbook.me/route.tsx @@ -1,7 +1,22 @@ import ErrorDisplay from "@/ui/error-display"; import { useApi } from "@/util/api"; -import { Center, Container, Loader, Text, Title } from "@mantine/core"; -import { useQuery } from "@tanstack/react-query"; +import { + Avatar, + Button, + Center, + Container, + Fieldset, + Group, + Loader, + PasswordInput, + Stack, + Text, + Title, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { IconFingerprint } from "@tabler/icons-react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { AxiosError } from "axios"; export default function Me() { const client = useApi(); @@ -11,6 +26,41 @@ export default function Me() { queryFn: async () => await client.get(`users/me`).then((res) => res.data), }); + const updatePassword = useMutation({ + mutationFn: async (values: { + current_psk: string; + new_psk: string; + confirm_new_psk: string; + }) => + await client.put(`/users/me/password`, { + current_password: values.current_psk, + new_password: values.new_psk, + }), + }); + + const updatePskForm = useForm({ + initialValues: { + current_psk: "", + new_psk: "", + confirm_new_psk: "", + }, + validate: { + current_psk: (value) => + value.length === 0 ? "Please enter your current password" : null, + new_psk: (value) => { + if (value.length === 0) return "Please enter a new password"; + if (value.length < 8 || value.length > 16) + return "Password must be between 8 and 16 characters"; + }, + confirm_new_psk: (value, values) => { + if (value.length === 0) return "Please confirm your new password"; + if (value.length < 8 || value.length > 16) + return "Password must be between 8 and 16 characters"; + if (value !== values.new_psk) return "Passwords must match"; + }, + }, + }); + return ( {user.isLoading ? ( @@ -22,10 +72,57 @@ export default function Me() { ) : user.data ? ( - <> - {user.data.username} - Level {user.data.level}{" "} - + + + + {user.data.username} + + {user.data.level === 2 + ? "Admin" + : user.data.level === 1 + ? "User" + : "Guest"} + {" "} + +
{ + updatePassword.mutate(values); + })} + > +
+ + + + + {updatePassword.isPending ? ( + Updating... + ) : updatePassword.isError ? ( + updatePassword.error && + (updatePassword.error as AxiosError).response?.status === + 403 ? ( + Incorrect password + ) : ( + Failed: {updatePassword.error.message} + ) + ) : updatePassword.isSuccess ? ( + Updated + ) : null} + + +
+
+
) : ( Unknown Error )} diff --git a/web/app/routes/logbook/route.tsx b/web/app/routes/logbook/route.tsx index dcd00fc..effad56 100644 --- a/web/app/routes/logbook/route.tsx +++ b/web/app/routes/logbook/route.tsx @@ -2,7 +2,14 @@ import { TailfinAppShell } from "@/ui/nav/app-shell"; import { useAuth } from "@/util/auth"; import type { MetaFunction } from "@remix-run/node"; import { Outlet, useNavigate } from "@remix-run/react"; -import { useEffect } from "react"; +import { + QueryCache, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { AxiosError } from "axios"; +import { useEffect, useState } from "react"; export const meta: MetaFunction = () => { return [ @@ -21,9 +28,36 @@ export default function Index() { } }, [user, loading, navigate]); + const [queryClient] = useState( + () => + new QueryClient({ + queryCache: new QueryCache({ + onError: (error: Error) => { + if (error instanceof AxiosError && error.response?.status === 401) { + navigate("/login"); + } + }, + }), + defaultOptions: { + queries: { + staleTime: 1000, + retry: (failureCount, error: Error) => { + return ( + !error || + (error instanceof AxiosError && error.response?.status !== 401) + ); + }, + }, + }, + }) + ); + return ( - - - + + + + + + ); } diff --git a/web/app/routes/login/route.tsx b/web/app/routes/login/route.tsx index 23203b4..9a0c437 100644 --- a/web/app/routes/login/route.tsx +++ b/web/app/routes/login/route.tsx @@ -7,19 +7,30 @@ import { Group, Image, PasswordInput, + Space, Stack, + Text, TextInput, Title, } from "@mantine/core"; import { useForm } from "@mantine/form"; -import { useEffect } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; + +function LoginPage() { + const [error, setError] = useState(""); -export default function Login() { const form = useForm({ initialValues: { username: "", password: "", }, + validate: { + username: (value) => + value.length === 0 ? "Please enter a username" : null, + password: (value) => + value.length === 0 ? "Please enter a password" : null, + }, }); const { signin } = useAuth(); @@ -40,8 +51,19 @@ export default function Login() {
{ - signin(values); + onSubmit={form.onSubmit(async (values) => { + setError(""); + try { + await signin(values) + .then(() => {}) + .catch((err) => { + console.log(err); + setError((err as Error).message); + }); + } catch (err) { + console.log(err); + setError((err as Error).message); + } })} > - - @@ -66,3 +95,13 @@ export default function Login() { ); } + +export default function Login() { + const queryClient = new QueryClient(); + + return ( + + + + ); +} diff --git a/web/app/util/auth.tsx b/web/app/util/auth.tsx index 9f7e04a..6e2e9ec 100644 --- a/web/app/util/auth.tsx +++ b/web/app/util/auth.tsx @@ -1,5 +1,6 @@ import { useApi } from "./api"; import { useNavigate } from "@remix-run/react"; +import { AxiosError } from "axios"; import { createContext, useContext, useEffect, useState } from "react"; interface AuthContextValues { @@ -11,7 +12,7 @@ interface AuthContextValues { }: { username: string; password: string; - }) => void; + }) => Promise; signout: () => void; clearUser: () => void; } @@ -72,9 +73,11 @@ function useProvideAuth() { await client .postForm("/auth/login", values) .then((response) => handleTokens(response.data)) - .catch(() => { + .catch((err: AxiosError) => { setLoading(false); - throw new Error("Invalid username or password"); + if (err.response?.status === 401) + throw new Error("Invalid username or password"); + throw new Error(err.message); }); setLoading(false);