Consolidate authentication code
This commit is contained in:
parent
73b11482ff
commit
912e4d1b0e
@ -14,6 +14,9 @@ import {
|
|||||||
useRouteError,
|
useRouteError,
|
||||||
} from "@remix-run/react";
|
} from "@remix-run/react";
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ColorSchemeScript,
|
ColorSchemeScript,
|
||||||
@ -23,11 +26,9 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { TailfinAppShell } from "./ui/nav/app-shell";
|
|
||||||
import { IconRocket } from "@tabler/icons-react";
|
import { IconRocket } from "@tabler/icons-react";
|
||||||
import Providers from "./providers";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { AuthProvider } from "./util/auth";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [
|
||||||
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
||||||
@ -60,7 +61,7 @@ export function ErrorBoundary() {
|
|||||||
<Button
|
<Button
|
||||||
leftSection={<IconRocket />}
|
leftSection={<IconRocket />}
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/"
|
to="/logbook"
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
Get me out of here!
|
Get me out of here!
|
||||||
@ -89,10 +90,12 @@ export default function App() {
|
|||||||
<body>
|
<body>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MantineProvider theme={{ primaryColor: "violet" }}>
|
<MantineProvider theme={{ primaryColor: "violet" }}>
|
||||||
|
<AuthProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
<LiveReload />
|
<LiveReload />
|
||||||
|
</AuthProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { client } from "@/util/api";
|
import { client } from "@/util/api";
|
||||||
import { List, Stack, Text } from "@mantine/core";
|
import { List, Stack, Text } from "@mantine/core";
|
||||||
import { useParams } from "@remix-run/react";
|
import { useParams } from "@remix-run/react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export default function Flight() {
|
export default function Flight() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const flight = useQuery({
|
const flight = useQuery({
|
||||||
queryKey: [params.id],
|
queryKey: [params.id],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
|
@ -1,20 +1,11 @@
|
|||||||
import { client } from "@/util/api";
|
import { client } from "@/util/api";
|
||||||
import {
|
import { Flight } from "@/util/types";
|
||||||
Divider,
|
import { NavLink, Text, Button, ScrollArea, Stack } from "@mantine/core";
|
||||||
NavLink,
|
|
||||||
Text,
|
|
||||||
Container,
|
|
||||||
Button,
|
|
||||||
ScrollArea,
|
|
||||||
Stack,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { Link, useLocation } from "@remix-run/react";
|
import { Link, useLocation } from "@remix-run/react";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export function FlightsList() {
|
export function FlightsList() {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const flights = useQuery({
|
const flights = useQuery({
|
||||||
queryKey: ["flights-list"],
|
queryKey: ["flights-list"],
|
||||||
queryFn: () => client.get(`/flights`).then((res) => res.data),
|
queryFn: () => client.get(`/flights`).then((res) => res.data),
|
||||||
@ -30,7 +21,7 @@ export function FlightsList() {
|
|||||||
</Button>
|
</Button>
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
{flights.data ? (
|
{flights.data ? (
|
||||||
flights.data.map((flight, index) => (
|
flights.data.map((flight: Flight, index: number) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={index}
|
key={index}
|
||||||
component={Link}
|
component={Link}
|
||||||
@ -48,8 +39,6 @@ export function FlightsList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MobileFlightsList() {
|
export function MobileFlightsList() {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const flights = useQuery({
|
const flights = useQuery({
|
||||||
queryKey: ["flights-list"],
|
queryKey: ["flights-list"],
|
||||||
queryFn: () => client.get(`/flights`).then((res) => res.data),
|
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)">
|
<Stack p="0" m="0" justify="space-between" h="calc(100vh - 95px)">
|
||||||
<ScrollArea h="calc(100vh - 95px - 50px">
|
<ScrollArea h="calc(100vh - 95px - 50px">
|
||||||
{flights.data ? (
|
{flights.data ? (
|
||||||
flights.data.map((flight, index) => (
|
flights.data.map((flight: Flight, index: number) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={index}
|
key={index}
|
||||||
component={Link}
|
component={Link}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { useMe } from "@/util/hooks";
|
import { useAuth } from "@/util/auth";
|
||||||
import { Container, Title } from "@mantine/core";
|
import { Container, Title } from "@mantine/core";
|
||||||
|
|
||||||
export default function Me() {
|
export default function Me() {
|
||||||
const me = useMe();
|
const { user } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Title order={2}>{me.data?.username}</Title>
|
<Title order={2}>{user}</Title>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { TailfinAppShell } from "@/ui/nav/app-shell";
|
import { TailfinAppShell } from "@/ui/nav/app-shell";
|
||||||
|
import { useAuth } from "@/util/auth";
|
||||||
import type { MetaFunction } from "@remix-run/node";
|
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 = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
@ -10,11 +12,20 @@ export const meta: MetaFunction = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
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 (
|
return (
|
||||||
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
|
|
||||||
<TailfinAppShell>
|
<TailfinAppShell>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</TailfinAppShell>
|
</TailfinAppShell>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { client } from "@/util/api";
|
import { useAuth } from "@/util/auth";
|
||||||
import { useLogin } from "@/util/hooks";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@ -19,7 +18,7 @@ export default function Login() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const signInMutation = useLogin();
|
const { signin } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md" h="100%" justify="center" align="stretch">
|
<Stack gap="md" h="100%" justify="center" align="stretch">
|
||||||
@ -29,7 +28,7 @@ export default function Login() {
|
|||||||
<Box maw={340} mx="auto">
|
<Box maw={340} mx="auto">
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit((values) => {
|
onSubmit={form.onSubmit((values) => {
|
||||||
signInMutation(values);
|
signin(values);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Title,
|
Title,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
Image,
|
|
||||||
Avatar,
|
Avatar,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { useMe, useSignOut } from "@/util/hooks";
|
import { useAuth } from "@/util/auth";
|
||||||
import { Stack, NavLink, ActionIcon } from "@mantine/core";
|
import { Stack, NavLink } from "@mantine/core";
|
||||||
import { Link, useLocation } from "@remix-run/react";
|
import { Link, useLocation } from "@remix-run/react";
|
||||||
import {
|
import {
|
||||||
IconBook2,
|
IconBook2,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconMapRoute,
|
|
||||||
IconPlaneDeparture,
|
IconPlaneDeparture,
|
||||||
IconUser,
|
IconUser,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
@ -13,8 +12,7 @@ export default function Navbar() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const page = location.pathname.split("/")[2];
|
const page = location.pathname.split("/")[2];
|
||||||
|
|
||||||
const me = useMe();
|
const { user, signout } = useAuth();
|
||||||
const signOut = useSignOut();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack justify="space-between" h="100%">
|
<Stack justify="space-between" h="100%">
|
||||||
@ -39,13 +37,7 @@ export default function Navbar() {
|
|||||||
<Stack gap="0">
|
<Stack gap="0">
|
||||||
<NavLink
|
<NavLink
|
||||||
p="md"
|
p="md"
|
||||||
label={
|
label={user ? user : "Not Logged In"}
|
||||||
me.isError
|
|
||||||
? me.error.message
|
|
||||||
: me.isFetched
|
|
||||||
? me.data?.username
|
|
||||||
: "Not Logged In"
|
|
||||||
}
|
|
||||||
leftSection={<IconUser />}
|
leftSection={<IconUser />}
|
||||||
>
|
>
|
||||||
<NavLink
|
<NavLink
|
||||||
@ -57,7 +49,7 @@ export default function Navbar() {
|
|||||||
/>
|
/>
|
||||||
<NavLink
|
<NavLink
|
||||||
p="md"
|
p="md"
|
||||||
onClick={() => signOut()}
|
onClick={() => signout()}
|
||||||
label="Sign Out"
|
label="Sign Out"
|
||||||
leftSection={<IconLogout />}
|
leftSection={<IconLogout />}
|
||||||
/>
|
/>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useNavigate } from "@remix-run/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const client = axios.create({
|
export const client = axios.create({
|
||||||
@ -13,15 +14,9 @@ client.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
client.interceptors.request.use(
|
|
||||||
(response) => response,
|
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
|
const navigate = useNavigate();
|
||||||
if (error.response.status == 401 && !originalRequest._retry) {
|
if (error.response.status == 401 && !originalRequest._retry) {
|
||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
const refreshToken = localStorage.getItem("refresh-token");
|
const refreshToken = localStorage.getItem("refresh-token");
|
||||||
@ -36,7 +31,10 @@ client.interceptors.request.use(
|
|||||||
] = `Bearer ${data.refreshToken}`;
|
] = `Bearer ${data.refreshToken}`;
|
||||||
return client(originalRequest);
|
return client(originalRequest);
|
||||||
} catch (_error) {
|
} 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