From 912e4d1b0e26190848627f9cf5f85552b960a3a7 Mon Sep 17 00:00:00 2001 From: april Date: Wed, 3 Jan 2024 10:01:15 -0600 Subject: [PATCH] Consolidate authentication code --- web/app/root.tsx | 21 ++-- web/app/routes/logbook.flights.$id/route.tsx | 3 +- .../routes/logbook.flights/flights-list.tsx | 21 +--- web/app/routes/logbook.me/route.tsx | 6 +- web/app/routes/logbook/route.tsx | 23 ++-- web/app/routes/login/route.tsx | 7 +- web/app/ui/nav/app-shell.tsx | 1 - web/app/ui/nav/navbar.tsx | 18 +--- web/app/util/api.ts | 14 ++- web/app/util/auth.tsx | 102 ++++++++++++++++++ web/app/util/hooks.ts | 52 --------- web/app/util/types.ts | 52 +++++++++ 12 files changed, 206 insertions(+), 114 deletions(-) create mode 100644 web/app/util/auth.tsx delete mode 100644 web/app/util/hooks.ts create mode 100644 web/app/util/types.ts diff --git a/web/app/root.tsx b/web/app/root.tsx index e831785..536be05 100644 --- a/web/app/root.tsx +++ b/web/app/root.tsx @@ -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() { {flights.data ? ( - flights.data.map((flight, index) => ( + flights.data.map((flight: Flight, index: number) => ( client.get(`/flights`).then((res) => res.data), @@ -62,7 +51,7 @@ export function MobileFlightsList() { {flights.data ? ( - flights.data.map((flight, index) => ( + flights.data.map((flight: Flight, index: number) => ( - {me.data?.username} + {user} ); } diff --git a/web/app/routes/logbook/route.tsx b/web/app/routes/logbook/route.tsx index da1a004..6877504 100644 --- a/web/app/routes/logbook/route.tsx +++ b/web/app/routes/logbook/route.tsx @@ -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 ( -
- - - -
+ + + ); } diff --git a/web/app/routes/login/route.tsx b/web/app/routes/login/route.tsx index 3e29a10..2ef3c71 100644 --- a/web/app/routes/login/route.tsx +++ b/web/app/routes/login/route.tsx @@ -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 ( @@ -29,7 +28,7 @@ export default function Login() {
{ - signInMutation(values); + signin(values); })} > @@ -39,13 +37,7 @@ export default function Navbar() { } > signOut()} + onClick={() => signout()} label="Sign Out" leftSection={} /> diff --git a/web/app/util/api.ts b/web/app/util/api.ts index 42ac734..32f381a 100644 --- a/web/app/util/api.ts +++ b/web/app/util/api.ts @@ -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"); } } } diff --git a/web/app/util/auth.tsx b/web/app/util/auth.tsx new file mode 100644 index 0000000..e29348f --- /dev/null +++ b/web/app/util/auth.tsx @@ -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(null); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const auth = useProvideAuth(); + return {children}; +} + +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(null); + const [loading, setLoading] = useState(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, + }; +} diff --git a/web/app/util/hooks.ts b/web/app/util/hooks.ts deleted file mode 100644 index cd304dc..0000000 --- a/web/app/util/hooks.ts +++ /dev/null @@ -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({ - 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; -} diff --git a/web/app/util/types.ts b/web/app/util/types.ts new file mode 100644 index 0000000..7722282 --- /dev/null +++ b/web/app/util/types.ts @@ -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 };