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,
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<typeof loader>();
return (
@ -156,8 +116,6 @@ export default function App() {
<ColorSchemeScript />
</head>
<body>
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<ApiProvider apiUrl={data.ENV.TAILFIN_API_URL}>
<MantineProvider theme={{ primaryColor: "violet" }}>
<ModalsProvider>
@ -170,9 +128,6 @@ export default function App() {
</ModalsProvider>
</MantineProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</HydrationBoundary>
</QueryClientProvider>
</body>
</html>
);

View File

@ -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 (
<Container>
{user.isLoading ? (
@ -22,10 +72,57 @@ export default function Me() {
<ErrorDisplay error="Error Loading User" />
</Center>
) : user.data ? (
<>
<Stack pt="xl">
<Stack align="center" pb="xl">
<Avatar size="xl" />
<Title order={2}>{user.data.username}</Title>
<Text>Level {user.data.level}</Text>{" "}
</>
<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>
)}

View File

@ -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 (
<QueryClientProvider client={queryClient}>
<TailfinAppShell>
<Outlet />
</TailfinAppShell>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}

View File

@ -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() {
<Center>
<Fieldset legend="Log In" w="350px">
<form
onSubmit={form.onSubmit((values) => {
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);
}
})}
>
<TextInput
@ -54,8 +76,15 @@ export default function Login() {
{...form.getInputProps("password")}
mt="md"
/>
<Group justify="center" mt="xl">
<Button type="submit" fullWidth>
{error === "" ? (
<Space mt="md" />
) : (
<Text mt="md" c="red">
{error}
</Text>
)}
<Group justify="center">
<Button type="submit" mt="xl" fullWidth>
Log In
</Button>
</Group>
@ -66,3 +95,13 @@ export default function Login() {
</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 { 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<string | void>;
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);
if (err.response?.status === 401)
throw new Error("Invalid username or password");
throw new Error(err.message);
});
setLoading(false);