Implement password updating and show login errors

This commit is contained in:
april 2024-01-05 17:03:39 -06:00
parent 4a0e49a959
commit 00f0def462
5 changed files with 204 additions and 76 deletions

View File

@ -14,20 +14,9 @@ import {
isRouteErrorResponse, isRouteErrorResponse,
json, json,
useLoaderData, useLoaderData,
useNavigate,
useRouteError, useRouteError,
} from "@remix-run/react"; } 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 { import {
Button, Button,
ColorSchemeScript, ColorSchemeScript,
@ -41,8 +30,6 @@ import { ModalsProvider } from "@mantine/modals";
import { IconRocket } from "@tabler/icons-react"; import { IconRocket } from "@tabler/icons-react";
import { AuthProvider } from "./util/auth"; import { AuthProvider } from "./util/auth";
import { useState } from "react";
import { AxiosError } from "axios";
import { ApiProvider } from "./util/api"; import { ApiProvider } from "./util/api";
export const links: LinksFunction = () => [ export const links: LinksFunction = () => [
@ -117,33 +104,6 @@ export async function loader() {
} }
export default function App() { 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<typeof loader>(); const data = useLoaderData<typeof loader>();
return ( return (
@ -156,23 +116,18 @@ export default function App() {
<ColorSchemeScript /> <ColorSchemeScript />
</head> </head>
<body> <body>
<QueryClientProvider client={queryClient}> <ApiProvider apiUrl={data.ENV.TAILFIN_API_URL}>
<HydrationBoundary state={dehydratedState}> <MantineProvider theme={{ primaryColor: "violet" }}>
<ApiProvider apiUrl={data.ENV.TAILFIN_API_URL}> <ModalsProvider>
<MantineProvider theme={{ primaryColor: "violet" }}> <AuthProvider>
<ModalsProvider> <Outlet />
<AuthProvider> <ScrollRestoration />
<Outlet /> <Scripts />
<ScrollRestoration /> <LiveReload />
<Scripts /> </AuthProvider>
<LiveReload /> </ModalsProvider>
</AuthProvider> </MantineProvider>
</ModalsProvider> </ApiProvider>
</MantineProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</HydrationBoundary>
</QueryClientProvider>
</body> </body>
</html> </html>
); );

View File

@ -1,7 +1,22 @@
import ErrorDisplay from "@/ui/error-display"; import ErrorDisplay from "@/ui/error-display";
import { useApi } from "@/util/api"; import { useApi } from "@/util/api";
import { Center, Container, Loader, Text, Title } from "@mantine/core"; import {
import { useQuery } from "@tanstack/react-query"; 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() { export default function Me() {
const client = useApi(); const client = useApi();
@ -11,6 +26,41 @@ export default function Me() {
queryFn: async () => await client.get(`users/me`).then((res) => res.data), 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 ( return (
<Container> <Container>
{user.isLoading ? ( {user.isLoading ? (
@ -22,10 +72,57 @@ export default function Me() {
<ErrorDisplay error="Error Loading User" /> <ErrorDisplay error="Error Loading User" />
</Center> </Center>
) : user.data ? ( ) : user.data ? (
<> <Stack pt="xl">
<Title order={2}>{user.data.username}</Title> <Stack align="center" pb="xl">
<Text>Level {user.data.level}</Text>{" "} <Avatar size="xl" />
</> <Title order={2}>{user.data.username}</Title>
<Text>
{user.data.level === 2
? "Admin"
: user.data.level === 1
? "User"
: "Guest"}
</Text>{" "}
</Stack>
<form
onSubmit={updatePskForm.onSubmit((values) => {
updatePassword.mutate(values);
})}
>
<Fieldset legend="Update Password">
<PasswordInput
label="Current Password"
{...updatePskForm.getInputProps("current_psk")}
/>
<PasswordInput
label="New Password"
{...updatePskForm.getInputProps("new_psk")}
/>
<PasswordInput
label="Confirm New Password"
{...updatePskForm.getInputProps("confirm_new_psk")}
/>
<Group justify="flex-end" mt="md">
{updatePassword.isPending ? (
<Text>Updating...</Text>
) : updatePassword.isError ? (
updatePassword.error &&
(updatePassword.error as AxiosError).response?.status ===
403 ? (
<Text c="red">Incorrect password</Text>
) : (
<Text c="red">Failed: {updatePassword.error.message}</Text>
)
) : updatePassword.isSuccess ? (
<Text c="green">Updated</Text>
) : null}
<Button type="submit" leftSection={<IconFingerprint />}>
Update
</Button>
</Group>
</Fieldset>
</form>
</Stack>
) : ( ) : (
<Text c="red">Unknown Error</Text> <Text c="red">Unknown Error</Text>
)} )}

View File

@ -2,7 +2,14 @@ import { TailfinAppShell } from "@/ui/nav/app-shell";
import { useAuth } from "@/util/auth"; import { useAuth } from "@/util/auth";
import type { MetaFunction } from "@remix-run/node"; import type { MetaFunction } from "@remix-run/node";
import { Outlet, useNavigate } from "@remix-run/react"; 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 = () => { export const meta: MetaFunction = () => {
return [ return [
@ -21,9 +28,36 @@ export default function Index() {
} }
}, [user, loading, navigate]); }, [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 ( return (
<TailfinAppShell> <QueryClientProvider client={queryClient}>
<Outlet /> <TailfinAppShell>
</TailfinAppShell> <Outlet />
</TailfinAppShell>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
); );
} }

View File

@ -7,19 +7,30 @@ import {
Group, Group,
Image, Image,
PasswordInput, PasswordInput,
Space,
Stack, Stack,
Text,
TextInput, TextInput,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useForm } from "@mantine/form"; 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({ const form = useForm({
initialValues: { initialValues: {
username: "", username: "",
password: "", 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(); const { signin } = useAuth();
@ -40,8 +51,19 @@ export default function Login() {
<Center> <Center>
<Fieldset legend="Log In" w="350px"> <Fieldset legend="Log In" w="350px">
<form <form
onSubmit={form.onSubmit((values) => { onSubmit={form.onSubmit(async (values) => {
signin(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);
}
})} })}
> >
<TextInput <TextInput
@ -54,8 +76,15 @@ export default function Login() {
{...form.getInputProps("password")} {...form.getInputProps("password")}
mt="md" mt="md"
/> />
<Group justify="center" mt="xl"> {error === "" ? (
<Button type="submit" fullWidth> <Space mt="md" />
) : (
<Text mt="md" c="red">
{error}
</Text>
)}
<Group justify="center">
<Button type="submit" mt="xl" fullWidth>
Log In Log In
</Button> </Button>
</Group> </Group>
@ -66,3 +95,13 @@ export default function Login() {
</Container> </Container>
); );
} }
export default function Login() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<LoginPage />
</QueryClientProvider>
);
}

View File

@ -1,5 +1,6 @@
import { useApi } from "./api"; import { useApi } from "./api";
import { useNavigate } from "@remix-run/react"; import { useNavigate } from "@remix-run/react";
import { AxiosError } from "axios";
import { createContext, useContext, useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState } from "react";
interface AuthContextValues { interface AuthContextValues {
@ -11,7 +12,7 @@ interface AuthContextValues {
}: { }: {
username: string; username: string;
password: string; password: string;
}) => void; }) => Promise<string | void>;
signout: () => void; signout: () => void;
clearUser: () => void; clearUser: () => void;
} }
@ -72,9 +73,11 @@ function useProvideAuth() {
await client await client
.postForm("/auth/login", values) .postForm("/auth/login", values)
.then((response) => handleTokens(response.data)) .then((response) => handleTokens(response.data))
.catch(() => { .catch((err: AxiosError) => {
setLoading(false); 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); setLoading(false);