Consolidate authentication code
This commit is contained in:
parent
73b11482ff
commit
912e4d1b0e
@ -14,6 +14,9 @@ import {
|
||||
useRouteError,
|
||||
} from "@remix-run/react";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ColorSchemeScript,
|
||||
@ -23,11 +26,9 @@ import {
|
||||
Stack,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { TailfinAppShell } from "./ui/nav/app-shell";
|
||||
import { IconRocket } from "@tabler/icons-react";
|
||||
import Providers from "./providers";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
import { AuthProvider } from "./util/auth";
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
||||
@ -60,7 +61,7 @@ export function ErrorBoundary() {
|
||||
<Button
|
||||
leftSection={<IconRocket />}
|
||||
component={Link}
|
||||
to="/"
|
||||
to="/logbook"
|
||||
variant="default"
|
||||
>
|
||||
Get me out of here!
|
||||
@ -89,10 +90,12 @@ export default function App() {
|
||||
<body>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider theme={{ primaryColor: "violet" }}>
|
||||
<AuthProvider>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</AuthProvider>
|
||||
</MantineProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { client } from "@/util/api";
|
||||
import { List, Stack, Text } from "@mantine/core";
|
||||
import { useParams } from "@remix-run/react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export default function Flight() {
|
||||
const params = useParams();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const flight = useQuery({
|
||||
queryKey: [params.id],
|
||||
queryFn: async () =>
|
||||
|
@ -1,20 +1,11 @@
|
||||
import { client } from "@/util/api";
|
||||
import {
|
||||
Divider,
|
||||
NavLink,
|
||||
Text,
|
||||
Container,
|
||||
Button,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
} from "@mantine/core";
|
||||
import { Flight } from "@/util/types";
|
||||
import { NavLink, Text, Button, ScrollArea, Stack } from "@mantine/core";
|
||||
import { Link, useLocation } from "@remix-run/react";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export function FlightsList() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const flights = useQuery({
|
||||
queryKey: ["flights-list"],
|
||||
queryFn: () => client.get(`/flights`).then((res) => res.data),
|
||||
@ -30,7 +21,7 @@ export function FlightsList() {
|
||||
</Button>
|
||||
<ScrollArea>
|
||||
{flights.data ? (
|
||||
flights.data.map((flight, index) => (
|
||||
flights.data.map((flight: Flight, index: number) => (
|
||||
<NavLink
|
||||
key={index}
|
||||
component={Link}
|
||||
@ -48,8 +39,6 @@ export function FlightsList() {
|
||||
}
|
||||
|
||||
export function MobileFlightsList() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const flights = useQuery({
|
||||
queryKey: ["flights-list"],
|
||||
queryFn: () => client.get(`/flights`).then((res) => res.data),
|
||||
@ -62,7 +51,7 @@ export function MobileFlightsList() {
|
||||
<Stack p="0" m="0" justify="space-between" h="calc(100vh - 95px)">
|
||||
<ScrollArea h="calc(100vh - 95px - 50px">
|
||||
{flights.data ? (
|
||||
flights.data.map((flight, index) => (
|
||||
flights.data.map((flight: Flight, index: number) => (
|
||||
<NavLink
|
||||
key={index}
|
||||
component={Link}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useMe } from "@/util/hooks";
|
||||
import { useAuth } from "@/util/auth";
|
||||
import { Container, Title } from "@mantine/core";
|
||||
|
||||
export default function Me() {
|
||||
const me = useMe();
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title order={2}>{me.data?.username}</Title>
|
||||
<Title order={2}>{user}</Title>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { TailfinAppShell } from "@/ui/nav/app-shell";
|
||||
import { useAuth } from "@/util/auth";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { Outlet, useNavigate } from "@remix-run/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@ -10,11 +12,20 @@ export const meta: MetaFunction = () => {
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const { user, loading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("loading: " + loading);
|
||||
console.log("user: " + user);
|
||||
if (!loading && !user) {
|
||||
navigate("/login");
|
||||
}
|
||||
}, [user, loading, navigate]);
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
|
||||
<TailfinAppShell>
|
||||
<Outlet />
|
||||
</TailfinAppShell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { client } from "@/util/api";
|
||||
import { useLogin } from "@/util/hooks";
|
||||
import { useAuth } from "@/util/auth";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@ -19,7 +18,7 @@ export default function Login() {
|
||||
},
|
||||
});
|
||||
|
||||
const signInMutation = useLogin();
|
||||
const { signin } = useAuth();
|
||||
|
||||
return (
|
||||
<Stack gap="md" h="100%" justify="center" align="stretch">
|
||||
@ -29,7 +28,7 @@ export default function Login() {
|
||||
<Box maw={340} mx="auto">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
signInMutation(values);
|
||||
signin(values);
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
Group,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
Image,
|
||||
Avatar,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { useMe, useSignOut } from "@/util/hooks";
|
||||
import { Stack, NavLink, ActionIcon } from "@mantine/core";
|
||||
import { useAuth } from "@/util/auth";
|
||||
import { Stack, NavLink } from "@mantine/core";
|
||||
import { Link, useLocation } from "@remix-run/react";
|
||||
import {
|
||||
IconBook2,
|
||||
IconLogout,
|
||||
IconMapRoute,
|
||||
IconPlaneDeparture,
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
@ -13,8 +12,7 @@ export default function Navbar() {
|
||||
const location = useLocation();
|
||||
const page = location.pathname.split("/")[2];
|
||||
|
||||
const me = useMe();
|
||||
const signOut = useSignOut();
|
||||
const { user, signout } = useAuth();
|
||||
|
||||
return (
|
||||
<Stack justify="space-between" h="100%">
|
||||
@ -39,13 +37,7 @@ export default function Navbar() {
|
||||
<Stack gap="0">
|
||||
<NavLink
|
||||
p="md"
|
||||
label={
|
||||
me.isError
|
||||
? me.error.message
|
||||
: me.isFetched
|
||||
? me.data?.username
|
||||
: "Not Logged In"
|
||||
}
|
||||
label={user ? user : "Not Logged In"}
|
||||
leftSection={<IconUser />}
|
||||
>
|
||||
<NavLink
|
||||
@ -57,7 +49,7 @@ export default function Navbar() {
|
||||
/>
|
||||
<NavLink
|
||||
p="md"
|
||||
onClick={() => signOut()}
|
||||
onClick={() => signout()}
|
||||
label="Sign Out"
|
||||
leftSection={<IconLogout />}
|
||||
/>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import axios from "axios";
|
||||
|
||||
export const client = axios.create({
|
||||
@ -13,15 +14,9 @@ client.interceptors.request.use(
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
client.interceptors.request.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
const navigate = useNavigate();
|
||||
if (error.response.status == 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
const refreshToken = localStorage.getItem("refresh-token");
|
||||
@ -36,7 +31,10 @@ client.interceptors.request.use(
|
||||
] = `Bearer ${data.refreshToken}`;
|
||||
return client(originalRequest);
|
||||
} catch (_error) {
|
||||
return Promise.reject(_error);
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("refresh-token");
|
||||
console.log("Oh no!!!");
|
||||
navigate("/login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
web/app/util/auth.tsx
Normal file
102
web/app/util/auth.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { client } from "./api";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
interface AuthContextValues {
|
||||
user: string | null;
|
||||
loading: boolean;
|
||||
signin: ({
|
||||
username,
|
||||
password,
|
||||
}: {
|
||||
username: string;
|
||||
password: string;
|
||||
}) => void;
|
||||
signout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValues | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const auth = useProvideAuth();
|
||||
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValues {
|
||||
const data = useContext(AuthContext);
|
||||
if (!data) {
|
||||
throw new Error("Could not find AuthContext provider");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function useProvideAuth() {
|
||||
const [user, setUser] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleUser = (rawUser: string | null) => {
|
||||
if (rawUser) {
|
||||
setUser(rawUser);
|
||||
setLoading(false);
|
||||
return rawUser;
|
||||
} else {
|
||||
setUser(null);
|
||||
clearTokens();
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTokens = (tokens: {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}) => {
|
||||
if (tokens) {
|
||||
localStorage.setItem("token", tokens.access_token);
|
||||
localStorage.setItem("refresh-token", tokens.refresh_token);
|
||||
}
|
||||
};
|
||||
|
||||
const clearTokens = () => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("refresh-token");
|
||||
};
|
||||
|
||||
const signin = async (values: { username: string; password: string }) => {
|
||||
setLoading(true);
|
||||
await client
|
||||
.postForm("/auth/login", values)
|
||||
.then((response) => handleTokens(response.data))
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
throw new Error("Invalid username or password");
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
await client
|
||||
.get("/users/me")
|
||||
.then((response) => handleUser(response.data.username))
|
||||
.catch(() => handleUser(null));
|
||||
navigate("/logbook");
|
||||
};
|
||||
|
||||
const signout = async () => {
|
||||
return await client.post("/auth/logout").then(() => handleUser(null));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
client
|
||||
.get("/users/me")
|
||||
.then((response) => handleUser(response.data.username))
|
||||
.catch(() => handleUser(null));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
signin,
|
||||
signout,
|
||||
};
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { client } from "./api";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
|
||||
type User = {
|
||||
username: string;
|
||||
level: number;
|
||||
};
|
||||
|
||||
export function useMe() {
|
||||
return useQuery<User, Error>({
|
||||
queryKey: ["me"],
|
||||
queryFn: () => client.get(`/users/me`).then((res) => res.data),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSignOut() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onSignOut = async () => {
|
||||
queryClient.setQueryData(["user"], null);
|
||||
const res = await client.post("/auth/logout");
|
||||
if (res.status == 200) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
console.error("Failed to log out");
|
||||
}
|
||||
};
|
||||
|
||||
return onSignOut;
|
||||
}
|
||||
|
||||
export function useLogin() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { mutate: signInMutation } = useMutation({
|
||||
mutationFn: async (values) => {
|
||||
return await client.postForm("/auth/login", values);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
localStorage.setItem("token", data.data.access_token);
|
||||
localStorage.setItem("refresh-token", data.data.refresh_token);
|
||||
navigate("/logbook");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
return signInMutation;
|
||||
}
|
52
web/app/util/types.ts
Normal file
52
web/app/util/types.ts
Normal file
@ -0,0 +1,52 @@
|
||||
type Flight = {
|
||||
id: string;
|
||||
user: string;
|
||||
|
||||
date: string;
|
||||
aircraft: string | null;
|
||||
waypoint_from: string | null;
|
||||
waypoint_to: string | null;
|
||||
route: string | null;
|
||||
|
||||
hobbs_start: number | null;
|
||||
hobbs_end: number | null;
|
||||
tach_start: number | null;
|
||||
tach_end: number | null;
|
||||
|
||||
time_start: number | null;
|
||||
time_off: number | null;
|
||||
time_down: number | null;
|
||||
time_stop: number | null;
|
||||
|
||||
time_total: number;
|
||||
time_pic: number;
|
||||
time_sic: number;
|
||||
time_night: number;
|
||||
time_solo: number;
|
||||
|
||||
time_xc: number;
|
||||
dist_xc: number;
|
||||
|
||||
takeoffs_day: number;
|
||||
landings_day: number;
|
||||
takeoffs_night: number;
|
||||
landings_night: number;
|
||||
|
||||
time_instrument: number;
|
||||
time_sim_instrument: number;
|
||||
holds_instrument: number;
|
||||
|
||||
dual_given: number;
|
||||
dual_recvd: number;
|
||||
time_sim: number;
|
||||
time_ground: number;
|
||||
|
||||
tags: string[];
|
||||
|
||||
pax: string[];
|
||||
crew: string[];
|
||||
|
||||
comments: string;
|
||||
};
|
||||
|
||||
export { type Flight };
|
Loading…
x
Reference in New Issue
Block a user