diff --git a/web/app/root.tsx b/web/app/root.tsx index 0b5a732..0ac04d6 100644 --- a/web/app/root.tsx +++ b/web/app/root.tsx @@ -12,6 +12,8 @@ import { Scripts, ScrollRestoration, isRouteErrorResponse, + json, + useLoaderData, useNavigate, useRouteError, } from "@remix-run/react"; @@ -40,6 +42,7 @@ 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 = () => [ { @@ -104,6 +107,14 @@ export function ErrorBoundary() { ); } +export async function loader() { + return json({ + ENV: { + TAILFIN_API_URL: process.env.TAILFIN_API_URL ?? "http://localhost:8081", + }, + }); +} + export default function App() { const navigate = useNavigate(); const [queryClient] = useState( @@ -132,6 +143,8 @@ export default function App() { const dehydratedState = useDehydratedState(); + const data = useLoaderData(); + return ( @@ -144,14 +157,16 @@ export default function App() { - - - - - - - - + + + + + + + + + + diff --git a/web/app/routes/logbook.flights.$id/route.tsx b/web/app/routes/logbook.flights.$id/route.tsx index a4eb8cb..a84d5f7 100644 --- a/web/app/routes/logbook.flights.$id/route.tsx +++ b/web/app/routes/logbook.flights.$id/route.tsx @@ -1,6 +1,6 @@ import { VerticalLogItem } from "@/ui/display/log-item"; import ErrorDisplay from "@/ui/error-display"; -import { client } from "@/util/api"; +import { useApi } from "@/util/api"; import { Center, Container, @@ -16,6 +16,8 @@ import { useQuery } from "@tanstack/react-query"; export default function Flight() { const params = useParams(); + const client = useApi(); + const flight = useQuery({ queryKey: [params.id], queryFn: async () => diff --git a/web/app/routes/logbook.flights.new/route.tsx b/web/app/routes/logbook.flights.new/route.tsx index 6dfb9ca..27a9ac9 100644 --- a/web/app/routes/logbook.flights.new/route.tsx +++ b/web/app/routes/logbook.flights.new/route.tsx @@ -21,7 +21,7 @@ import { HourInput, ZeroHourInput } from "@/ui/form/hour-input"; import { ZeroIntInput } from "@/ui/form/int-input"; import ListInput from "@/ui/form/list-input"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { client } from "@/util/api"; +import { useApi } from "@/util/api"; import { useNavigate } from "@remix-run/react"; import { useAuth } from "@/util/auth"; import { AxiosError } from "axios"; @@ -80,6 +80,8 @@ export default function NewFlight() { const navigate = useNavigate(); const queryClient = useQueryClient(); + const client = useApi(); + const { clearUser } = useAuth(); const createFlight = useMutation({ diff --git a/web/app/routes/logbook.flights/flights-list.tsx b/web/app/routes/logbook.flights/flights-list.tsx index c0e490e..4ac899e 100644 --- a/web/app/routes/logbook.flights/flights-list.tsx +++ b/web/app/routes/logbook.flights/flights-list.tsx @@ -1,4 +1,4 @@ -import { client } from "@/util/api"; +import { useApi } from "@/util/api"; import { FlightConciseSchema } from "@/util/types"; import { NavLink, @@ -22,6 +22,8 @@ import { import { UseQueryResult, useQuery } from "@tanstack/react-query"; function useFlights() { + const client = useApi(); + const flights = useQuery({ queryKey: ["flights-list"], queryFn: async () => diff --git a/web/app/routes/logbook.me/route.tsx b/web/app/routes/logbook.me/route.tsx index 8cc0a80..99c9a8d 100644 --- a/web/app/routes/logbook.me/route.tsx +++ b/web/app/routes/logbook.me/route.tsx @@ -1,9 +1,11 @@ import ErrorDisplay from "@/ui/error-display"; -import { client } from "@/util/api"; +import { useApi } from "@/util/api"; import { Center, Container, Loader, Text, Title } from "@mantine/core"; import { useQuery } from "@tanstack/react-query"; export default function Me() { + const client = useApi(); + const user = useQuery({ queryKey: ["user"], queryFn: async () => await client.get(`users/me`).then((res) => res.data), diff --git a/web/app/util/api.ts b/web/app/util/api.ts deleted file mode 100644 index d0e612a..0000000 --- a/web/app/util/api.ts +++ /dev/null @@ -1,36 +0,0 @@ -import axios from "axios"; -import { useAuth } from "./auth"; - -export const client = axios.create({ - baseURL: "http://localhost:8081", - headers: { "Access-Control-Allow-Origin": "*" }, -}); - -client.interceptors.request.use( - (config) => { - const token = localStorage.getItem("token"); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - async (error) => { - const { clearUser } = useAuth(); - console.log(error.response); - if (error.response && error.response.status === 401) { - try { - const refreshToken = localStorage.getItem("refresh-token"); - const response = await client.post("/auth/refresh", { refreshToken }); - const newAccessToken = response.data.access_token; - - localStorage.setItem("token", newAccessToken); - error.config.headers.Authorization = `Bearer ${newAccessToken}`; - return client(error.config); - } catch (err) { - clearUser(); - window.location.href = "/login"; - } - } - return Promise.reject(error); - } -); diff --git a/web/app/util/api.tsx b/web/app/util/api.tsx new file mode 100644 index 0000000..a17df2c --- /dev/null +++ b/web/app/util/api.tsx @@ -0,0 +1,60 @@ +import axios, { AxiosInstance } from "axios"; +import { createContext, useContext } from "react"; + +const ApiContext = createContext(null); + +export function ApiProvider({ + children, + apiUrl, +}: { + children: React.ReactNode; + apiUrl: string; +}) { + const api = useProvideApi(apiUrl); + return {children}; +} + +export function useApi(): AxiosInstance { + const api = useContext(ApiContext); + if (!api) throw new Error("Could not find API provider"); + + return api; +} + +function useProvideApi(apiUrl: string) { + const client = axios.create({ + baseURL: apiUrl, + headers: { "Access-Control-Allow-Origin": "*" }, + }); + + client.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + async (error) => { + console.log(error.response); + if (error.response && error.response.status === 401) { + try { + const refreshToken = localStorage.getItem("refresh-token"); + const response = await client.post("/auth/refresh", { refreshToken }); + const newAccessToken = response.data.access_token; + + localStorage.setItem("token", newAccessToken); + error.config.headers.Authorization = `Bearer ${newAccessToken}`; + return client(error.config); + } catch (err) { + localStorage.removeItem("token"); + localStorage.removeItem("refresh-token"); + window.location.href = "/login"; + } + } + return Promise.reject(error); + } + ); + + return client; +} diff --git a/web/app/util/auth.tsx b/web/app/util/auth.tsx index 28b13ff..9f7e04a 100644 --- a/web/app/util/auth.tsx +++ b/web/app/util/auth.tsx @@ -1,4 +1,4 @@ -import { client } from "./api"; +import { useApi } from "./api"; import { useNavigate } from "@remix-run/react"; import { createContext, useContext, useEffect, useState } from "react"; @@ -37,6 +37,8 @@ function useProvideAuth() { const navigate = useNavigate(); + const client = useApi(); + const handleUser = (rawUser: string | null) => { if (rawUser) { setUser(rawUser); diff --git a/web/package.json b/web/package.json index 0e98bd9..f65094c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,5 @@ { - "name": "", + "name": "tailfin-web", "private": true, "sideEffects": false, "type": "module",