From cc4a87539d9ee3ee4d49c72358c1f91444df8b7d Mon Sep 17 00:00:00 2001 From: oliviercperrier Date: Wed, 5 Feb 2025 13:55:48 -0500 Subject: [PATCH 1/5] refresh tokens --- frontend/apps/variant/package.json | 4 ++- frontend/apps/variant/src/App.tsx | 31 +++++++++++-------- frontend/apps/variant/tsconfig.app.json | 1 + frontend/docs/project-structure.md | 14 +++++++++ frontend/package-lock.json | 14 +++++++-- frontend/package.json | 3 ++ .../portals/radiant/app/api/occurrences.ts | 22 +++++++++---- .../portals/radiant/app/api/refresh-token.ts | 19 +++++++----- frontend/portals/radiant/app/routes.ts | 5 ++- frontend/utils/axios.ts | 29 +++++++++++++++++ 10 files changed, 112 insertions(+), 30 deletions(-) create mode 100644 frontend/utils/axios.ts diff --git a/frontend/apps/variant/package.json b/frontend/apps/variant/package.json index fb5a3d8..fc713f8 100644 --- a/frontend/apps/variant/package.json +++ b/frontend/apps/variant/package.json @@ -24,9 +24,11 @@ }, "dependencies": { "components": "file:../../components", + "utils": "file:../../utils", "react": "^18.3.1", "react-dom": "^18.3.1", - "swr": "^2.3.1" + "swr": "^2.3.1", + "axios": "1.7.9" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/frontend/apps/variant/src/App.tsx b/frontend/apps/variant/src/App.tsx index 93a99e6..796e533 100644 --- a/frontend/apps/variant/src/App.tsx +++ b/frontend/apps/variant/src/App.tsx @@ -14,14 +14,20 @@ import { defaultSettings, } from "./include_variant_table"; import { IVariantEntity } from "@/variant_type"; -import useSWR, { Fetcher } from "swr"; +import useSWR from "swr"; +import { axiosClient } from "@/utils/axios"; -export interface AppProps {} +type OccurrenceInput = { + seqId: string; + listBody: ListBody; +}; -const fetcher: Fetcher = (url: string) => - fetch(url, { - method: "POST", - body: JSON.stringify({ +const fetcher = (input: OccurrenceInput) => + axiosClient.post("/occurrences", input).then((res) => res.data); + +function App() { + const { data } = useSWR( + { seqId: "5011", listBody: { selected_fields: ["hgvsg", "variant_class"], @@ -39,13 +45,12 @@ const fetcher: Fetcher = (url: string) => value: [4], }, }, - }), - }).then((res) => res.json()); - -function App({}: AppProps) { - const { data, error, isLoading } = useSWR("/api/occurrences", fetcher, { - revalidateOnFocus: false, - }); + }, + fetcher, + { + revalidateOnFocus: true, + } + ); const occurrences = data || []; return ( diff --git a/frontend/apps/variant/tsconfig.app.json b/frontend/apps/variant/tsconfig.app.json index 1d1b0bc..d914a77 100644 --- a/frontend/apps/variant/tsconfig.app.json +++ b/frontend/apps/variant/tsconfig.app.json @@ -27,6 +27,7 @@ "noUncheckedSideEffectImports": true, "paths": { "@/api/*": ["../../api/*"], + "@/utils/*": ["../../utils/*"], "@/components/*": ["../../components/*"], "@/base/ui/*": ["../../components/base/ui/*"], "@/lib/*": ["../../components/lib/*"], diff --git a/frontend/docs/project-structure.md b/frontend/docs/project-structure.md index fe9e625..8d2ed26 100644 --- a/frontend/docs/project-structure.md +++ b/frontend/docs/project-structure.md @@ -13,6 +13,7 @@ Instead of working by portals, now we work by applications and we have to make s ## Project Structure ### Directory Organization + ``` frontend/ ├── apps/ # Full applications for a domain (e.g., Variant, Prescription) @@ -28,36 +29,43 @@ frontend/ ├── themes/ # │ ├── themesX/ # Theme-specific assets │ | └── assets/ # +│ utils/ # Shared utils └── types/ # TypeScript types/interfaces ``` --- ### 1. **Apps** + The `Apps` directory contains full applications designed to serve specific business domains. Each application is a complete unit, including pages and modal dialogs specific to its purpose. It does not contain navigation, site layout. Only the core features of the application. #### Examples: + - **Variant**: Application related to variant exploration. - **Prescription**: Handles prescription-related functionalities. - **Community**: Manages community-specific features. #### Key Characteristics: + - Combines pages and modals for cohesive user flows for a specific domain. - Leverages components from the `Components` directory for reusable building blocks. --- ### 2. **Components** + The `Components` directory holds all basic to advanced UI components required to build applications. These components are categorized into generic and custom components. #### Types of Components: + - **Generic Components**: Built using [shadcn](https://shadcn.dev), leveraging pre-made, highly customizable components. - **Custom Components**: Created with React and TailwindCSS for unique design and behavior tailored to application needs. #### Key Features: + - Encourages reusability across multiple applications. - Provides a shared library of consistent UI elements. @@ -66,26 +74,32 @@ The `Components` directory holds all basic to advanced UI components required to --- ### 3. **Portals** + The `Portals` directory contains the layout and setup for portals, including the base structure for generating different portals (e.g., "Radiant"). #### Functionality: + - Uses `react-router` for navigation and routing. - Capable of generating multiple portals based on different themes and environment configurations during the build process. --- ### 4. **Themes** + The `Themes` directory houses theme-related assets such as images and TailwindCSS configurations. These themes are used to style the portals and applications, ensuring a consistent look and feel across the project. #### Contents: + - **Images**: Logos, backgrounds, and other visual assets. - **CSS**: TailwindCSS configurations for styling components and layouts. --- ### 5. **Storybook** + The `Storybook` directory contains a setup for testing and demonstrating components. It allows developers to preview individual components or groups of components in isolation. #### Capabilities: + - Select and test components with different themes applied. - Serves as a living documentation for the component library. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 867e429..6b807b3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,9 @@ "components/*", "portals/*" ], + "dependencies": { + "axios": "^1.7.9" + }, "devDependencies": { "@ferlab/eslint-config": "^2.0.0", "eslint-plugin-prettier": "^5.2.3" @@ -18,10 +21,12 @@ "apps/variant": { "version": "0.0.0", "dependencies": { + "axios": "1.7.9", "components": "file:../../components", "react": "^18.3.1", "react-dom": "^18.3.1", - "swr": "^2.3.1" + "swr": "^2.3.1", + "utils": "file:../../utils" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -14666,6 +14671,10 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils": { + "resolved": "utils", + "link": true + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -16478,6 +16487,7 @@ "optional": true } } - } + }, + "utils": {} } } diff --git a/frontend/package.json b/frontend/package.json index c31868d..0250c75 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,5 +9,8 @@ "devDependencies": { "@ferlab/eslint-config": "^2.0.0", "eslint-plugin-prettier": "^5.2.3" + }, + "dependencies": { + "axios": "^1.7.9" } } diff --git a/frontend/portals/radiant/app/api/occurrences.ts b/frontend/portals/radiant/app/api/occurrences.ts index 9fdf1ed..00cc959 100644 --- a/frontend/portals/radiant/app/api/occurrences.ts +++ b/frontend/portals/radiant/app/api/occurrences.ts @@ -1,5 +1,6 @@ import { getOccurrencesApi } from "~/utils/api.server"; import type { Route } from "./+types/occurrences"; +import { AxiosError } from "axios"; export function action({ request }: Route.ActionArgs) { switch (request.method) { @@ -25,11 +26,20 @@ const fetchOccurences = async (request: Request) => { }, }); } catch (error: any) { - return new Response(JSON.stringify([]), { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); + if (error instanceof AxiosError) { + return new Response(JSON.stringify(error.response?.data), { + status: error.response?.status || 500, + headers: { + "Content-Type": "application/json", + }, + }); + } else { + return new Response(JSON.stringify(error), { + status: 500, + headers: { + "Content-Type": "application/json", + }, + }); + } } }; diff --git a/frontend/portals/radiant/app/api/refresh-token.ts b/frontend/portals/radiant/app/api/refresh-token.ts index 035ad6b..40014a4 100644 --- a/frontend/portals/radiant/app/api/refresh-token.ts +++ b/frontend/portals/radiant/app/api/refresh-token.ts @@ -1,13 +1,18 @@ +import { HttpStatusCode } from "axios"; import type { Route } from "./+types/occurrences"; import { refreshAccessToken } from "~/utils/auth.server"; export async function action({ request }: Route.LoaderArgs) { - const results = await refreshAccessToken(request); + if (request.method === "POST") { + const results = await refreshAccessToken(request); - return new Response(JSON.stringify({ success: true }), { - status: 200, - headers: { - "Set-Cookie": results.cookie, - }, - }); + return new Response(JSON.stringify({ success: true }), { + status: HttpStatusCode.Ok, + headers: { + "Set-Cookie": results.cookie, + }, + }); + } + + return new Response(null, { status: HttpStatusCode.MethodNotAllowed }); } diff --git a/frontend/portals/radiant/app/routes.ts b/frontend/portals/radiant/app/routes.ts index dc2b871..64962e2 100644 --- a/frontend/portals/radiant/app/routes.ts +++ b/frontend/portals/radiant/app/routes.ts @@ -10,5 +10,8 @@ export default [ layout("./layout/protected-layout.tsx", [index("./routes/home.tsx")]), route("auth/callback", "./routes/auth/callback.ts"), route("auth/logout", "./routes/auth/logout.ts"), - ...prefix("api", [route("occurrences", "./api/occurrences.ts")]), + ...prefix("api", [ + route("occurrences", "./api/occurrences.ts"), + route("refresh-token", "./api/refresh-token.ts"), + ]), ] satisfies RouteConfig; diff --git a/frontend/utils/axios.ts b/frontend/utils/axios.ts new file mode 100644 index 0000000..d855cec --- /dev/null +++ b/frontend/utils/axios.ts @@ -0,0 +1,29 @@ +import axios from "axios"; + +export const axiosClient = axios.create({ + baseURL: "/api", + headers: { + "Content-Type": "application/json", + }, +}); + +axiosClient.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + try { + await axios.post("/api/refresh-token"); + + return axiosClient(originalRequest); + } catch (refreshError) { + window.location.href = "/auth/logout"; + return Promise.reject(refreshError); + } + } + return Promise.reject(error); + } +); From a1472f1c49da8ba3e9b701bd1c031ec81d510e42 Mon Sep 17 00:00:00 2001 From: oliviercperrier Date: Wed, 5 Feb 2025 14:15:21 -0500 Subject: [PATCH 2/5] refresh tokens --- frontend/portals/radiant/app/api/occurrences.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/portals/radiant/app/api/occurrences.ts b/frontend/portals/radiant/app/api/occurrences.ts index 00cc959..4c34996 100644 --- a/frontend/portals/radiant/app/api/occurrences.ts +++ b/frontend/portals/radiant/app/api/occurrences.ts @@ -1,6 +1,6 @@ import { getOccurrencesApi } from "~/utils/api.server"; import type { Route } from "./+types/occurrences"; -import { AxiosError } from "axios"; +import { AxiosError, HttpStatusCode } from "axios"; export function action({ request }: Route.ActionArgs) { switch (request.method) { @@ -28,14 +28,14 @@ const fetchOccurences = async (request: Request) => { } catch (error: any) { if (error instanceof AxiosError) { return new Response(JSON.stringify(error.response?.data), { - status: error.response?.status || 500, + status: error.response?.status || HttpStatusCode.InternalServerError, headers: { "Content-Type": "application/json", }, }); } else { return new Response(JSON.stringify(error), { - status: 500, + status: HttpStatusCode.InternalServerError, headers: { "Content-Type": "application/json", }, From 922d3719c31b5cf513dbb84f40687b1899653bb6 Mon Sep 17 00:00:00 2001 From: oliviercperrier Date: Wed, 5 Feb 2025 14:19:49 -0500 Subject: [PATCH 3/5] refresh tokens --- frontend/apps/variant/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/apps/variant/src/App.tsx b/frontend/apps/variant/src/App.tsx index 796e533..50bb0a8 100644 --- a/frontend/apps/variant/src/App.tsx +++ b/frontend/apps/variant/src/App.tsx @@ -48,7 +48,7 @@ function App() { }, fetcher, { - revalidateOnFocus: true, + revalidateOnFocus: false, } ); const occurrences = data || []; From 5d2f002cbe1fe2fda014c84b3e9a45cc82c632b7 Mon Sep 17 00:00:00 2001 From: oliviercperrier Date: Wed, 5 Feb 2025 14:23:12 -0500 Subject: [PATCH 4/5] refresh tokens --- frontend/utils/axios.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/utils/axios.ts b/frontend/utils/axios.ts index d855cec..f8d6854 100644 --- a/frontend/utils/axios.ts +++ b/frontend/utils/axios.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { HttpStatusCode } from "axios"; export const axiosClient = axios.create({ baseURL: "/api", @@ -12,7 +12,10 @@ axiosClient.interceptors.response.use( async (error) => { const originalRequest = error.config; - if (error.response.status === 401 && !originalRequest._retry) { + if ( + error.response.status === HttpStatusCode.Unauthorized && + !originalRequest._retry + ) { originalRequest._retry = true; try { From 55776021540aa335e567adb25d919e86cd3fa120 Mon Sep 17 00:00:00 2001 From: oliviercperrier Date: Wed, 5 Feb 2025 14:50:59 -0500 Subject: [PATCH 5/5] refresh tokens multiple calls --- frontend/utils/axios.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/utils/axios.ts b/frontend/utils/axios.ts index f8d6854..803d26f 100644 --- a/frontend/utils/axios.ts +++ b/frontend/utils/axios.ts @@ -7,11 +7,19 @@ export const axiosClient = axios.create({ }, }); +let refreshTokenPromise: Promise | null = null; + axiosClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; + if (refreshTokenPromise) { + await refreshTokenPromise; + + return axiosClient(originalRequest); + } + if ( error.response.status === HttpStatusCode.Unauthorized && !originalRequest._retry @@ -19,7 +27,11 @@ axiosClient.interceptors.response.use( originalRequest._retry = true; try { - await axios.post("/api/refresh-token"); + refreshTokenPromise = axios.post("/api/refresh-token").finally(() => { + refreshTokenPromise = null; + }); + + await refreshTokenPromise; return axiosClient(originalRequest); } catch (refreshError) {