mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #256 from akhilmhdh/feat/react-query
feat(ui): added new auth guard with react-query and axios
This commit is contained in:
@@ -46,11 +46,7 @@ services:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- ./frontend/src/pages:/app/src/pages
|
||||
- ./frontend/src/components:/app/src/components
|
||||
- ./frontend/src/ee:/app/src/ee
|
||||
- ./frontend/src/locales:/app/src/locales
|
||||
- ./frontend/src/styles:/app/src/styles
|
||||
- ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
||||
- ./frontend/public:/app/public
|
||||
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
|
||||
env_file: .env
|
||||
|
||||
50
frontend/package-lock.json
generated
50
frontend/package-lock.json
generated
@@ -28,6 +28,7 @@
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.46.0",
|
||||
"@tanstack/react-query": "^4.23.0",
|
||||
"add": "^2.0.6",
|
||||
"axios": "^0.27.2",
|
||||
"axios-auth-refresh": "^3.3.3",
|
||||
@@ -6646,6 +6647,41 @@
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.22.4.tgz",
|
||||
"integrity": "sha512-t79CMwlbBnj+yL82tEcmRN93bL4U3pae2ota4t5NN2z3cIeWw74pzdWrKRwOfTvLcd+b30tC+ciDlfYOKFPGUw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.23.0.tgz",
|
||||
"integrity": "sha512-cfQsrecZQjYYueiow4WcK8ItokXJnv+b2OrK8Lf5kF7lM9uCo1ilyygFB8wo4MfxchUBVM6Cs8wq4Ed7fouwkA==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "4.22.4",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz",
|
||||
@@ -26993,6 +27029,20 @@
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
}
|
||||
},
|
||||
"@tanstack/query-core": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.22.4.tgz",
|
||||
"integrity": "sha512-t79CMwlbBnj+yL82tEcmRN93bL4U3pae2ota4t5NN2z3cIeWw74pzdWrKRwOfTvLcd+b30tC+ciDlfYOKFPGUw=="
|
||||
},
|
||||
"@tanstack/react-query": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.23.0.tgz",
|
||||
"integrity": "sha512-cfQsrecZQjYYueiow4WcK8ItokXJnv+b2OrK8Lf5kF7lM9uCo1ilyygFB8wo4MfxchUBVM6Cs8wq4Ed7fouwkA==",
|
||||
"requires": {
|
||||
"@tanstack/query-core": "4.22.4",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.46.0",
|
||||
"@tanstack/react-query": "^4.23.0",
|
||||
"add": "^2.0.6",
|
||||
"axios": "^0.27.2",
|
||||
"axios-auth-refresh": "^3.3.3",
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import token from '@app/pages/api/auth/Token';
|
||||
import { getAuthToken, setAuthToken } from '@app/reactQuery';
|
||||
|
||||
// depreciated: go for apiRequest module in config/api
|
||||
export default class SecurityClient {
|
||||
static #token = '';
|
||||
|
||||
static setToken(tokenStr: string) {
|
||||
this.#token = tokenStr;
|
||||
setAuthToken(tokenStr);
|
||||
}
|
||||
|
||||
static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
|
||||
const req = new Request(resource, options);
|
||||
|
||||
if (this.#token === '') {
|
||||
try {
|
||||
// TODO: This should be moved to a context to do it only once when app loads
|
||||
// this try catch saves route guard from a stuck state
|
||||
this.setToken(await token());
|
||||
} catch (error) {
|
||||
console.error('Unauthorized access');
|
||||
}
|
||||
}
|
||||
const token = getAuthToken();
|
||||
|
||||
if (this.#token) {
|
||||
req.headers.set('Authorization', `Bearer ${this.#token}`);
|
||||
if (token) {
|
||||
req.headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
|
||||
19
frontend/src/config/request.ts
Normal file
19
frontend/src/config/request.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { getAuthToken } from '@app/reactQuery';
|
||||
|
||||
export const apiRequest = axios.create({
|
||||
baseURL: '/',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
apiRequest.interceptors.request.use((config) => {
|
||||
const token = getAuthToken();
|
||||
if (token && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
49
frontend/src/context/AuthContext/AuthContext.tsx
Normal file
49
frontend/src/context/AuthContext/AuthContext.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { publicPaths } from '@app/const';
|
||||
import { useToggle } from '@app/hooks';
|
||||
import { useGetAuthToken } from '@app/hooks/api';
|
||||
import { isLoggedIn } from '@app/reactQuery';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// TODO(akhilmhdh): Using react-simple-animate from hard dom offloading
|
||||
// smoother dom offloading needs to be done
|
||||
|
||||
// Authentication controller
|
||||
// Does route checking
|
||||
// Provide a context for whole app to notify user is authorized or not
|
||||
export const AuthProvider = ({ children }: Props): JSX.Element => {
|
||||
const { isLoading } = useGetAuthToken();
|
||||
const { pathname, push } = useRouter();
|
||||
const [isReady, setIsReady] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
// check if loading of auth is done
|
||||
if (!isLoading) {
|
||||
// not a public path and not authenticated kick to login page
|
||||
if (!publicPaths.includes(pathname) && !isLoggedIn()) {
|
||||
push('/login').then(() => {
|
||||
setIsReady.on();
|
||||
});
|
||||
} else {
|
||||
// else good to go
|
||||
setIsReady.on();
|
||||
}
|
||||
}
|
||||
}, [pathname, isLoading]);
|
||||
|
||||
// wait for app to load the auth state
|
||||
if (isLoading || !isReady) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||
<img src="/images/loading/loading.gif" alt="loading animation" className="w-80" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children as JSX.Element;
|
||||
};
|
||||
1
frontend/src/context/AuthContext/index.tsx
Normal file
1
frontend/src/context/AuthContext/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { AuthProvider } from './AuthContext';
|
||||
1
frontend/src/hooks/api/auth/index.tsx
Normal file
1
frontend/src/hooks/api/auth/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { useGetAuthToken } from './queries';
|
||||
26
frontend/src/hooks/api/auth/queries.tsx
Normal file
26
frontend/src/hooks/api/auth/queries.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
import { setAuthToken } from '@app/reactQuery';
|
||||
|
||||
import { GetAuthTokenAPI } from './types';
|
||||
|
||||
const authKeys = {
|
||||
getAuthToken: ['token'] as const
|
||||
};
|
||||
|
||||
// Refresh token is set as cookie when logged in
|
||||
// Using that we fetch the auth bearer token needed for auth calls
|
||||
const fetchAuthToken = async () => {
|
||||
const { data } = await apiRequest.post<GetAuthTokenAPI>('/api/v1/auth/token', undefined, {
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAuthToken = () =>
|
||||
useQuery(authKeys.getAuthToken, fetchAuthToken, {
|
||||
onSuccess: (data) => setAuthToken(data.token),
|
||||
retry: 0
|
||||
});
|
||||
3
frontend/src/hooks/api/auth/types.ts
Normal file
3
frontend/src/hooks/api/auth/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type GetAuthTokenAPI = {
|
||||
token: string;
|
||||
};
|
||||
1
frontend/src/hooks/api/index.tsx
Normal file
1
frontend/src/hooks/api/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { useGetAuthToken } from './auth';
|
||||
@@ -1 +1,2 @@
|
||||
export { usePopUp } from './usePopUp';
|
||||
export { useToggle } from './useToggle';
|
||||
|
||||
30
frontend/src/hooks/useToggle.tsx
Normal file
30
frontend/src/hooks/useToggle.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
type VoidFn = () => void;
|
||||
|
||||
type UseToggleReturn = [
|
||||
boolean,
|
||||
{
|
||||
on: VoidFn;
|
||||
off: VoidFn;
|
||||
toggle: VoidFn;
|
||||
}
|
||||
];
|
||||
|
||||
export const useToggle = (initialState = false): UseToggleReturn => {
|
||||
const [value, setValue] = useState(initialState);
|
||||
|
||||
const on = useCallback(() => {
|
||||
setValue(true);
|
||||
}, []);
|
||||
|
||||
const off = useCallback(() => {
|
||||
setValue(false);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback((isOpen?: boolean) => {
|
||||
setValue((prev) => (typeof isOpen === 'boolean' ? isOpen : !prev));
|
||||
}, []);
|
||||
|
||||
return [value, { on, off, toggle }];
|
||||
};
|
||||
@@ -4,12 +4,14 @@ import { AppProps } from 'next/app';
|
||||
import { useRouter } from 'next/router';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import Layout from '@app/components/basic/Layout';
|
||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||
import RouteGuard from '@app/components/RouteGuard';
|
||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||
import { publicPaths } from '@app/const';
|
||||
import { AuthProvider } from '@app/context/AuthContext';
|
||||
import { queryClient } from '@app/reactQuery';
|
||||
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
import '../styles/globals.css';
|
||||
@@ -54,17 +56,25 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
publicPaths.includes(`/${appProps.router.pathname.split('/')[1]}`) ||
|
||||
!Component.requireAuth
|
||||
) {
|
||||
return <Component {...pageProps} />;
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteGuard>
|
||||
<NotificationProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
</RouteGuard>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<NotificationProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import InputField from '@app/components/basic/InputField';
|
||||
import ListBox from '@app/components/basic/Listbox';
|
||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { isLoggedIn } from '@app/reactQuery';
|
||||
|
||||
import getWorkspaces from './api/workspace/getWorkspaces';
|
||||
|
||||
@@ -31,6 +32,7 @@ export default function Login() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// TODO(akhilmhdh): workspace will be controlled by a workspace context
|
||||
const redirectToDashboard = async () => {
|
||||
let userWorkspace;
|
||||
try {
|
||||
@@ -41,7 +43,9 @@ export default function Login() {
|
||||
console.log('Error - Not logged in yet');
|
||||
}
|
||||
};
|
||||
redirectToDashboard();
|
||||
if (isLoggedIn()) {
|
||||
redirectToDashboard();
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
21
frontend/src/reactQuery.ts
Normal file
21
frontend/src/reactQuery.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
// this is saved in react-query cache
|
||||
export const AUTH_TOKEN_CACHE_KEY = ['infisical__auth-token'];
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// set token in memory cache
|
||||
export const setAuthToken = (token: string) =>
|
||||
queryClient.setQueryData(AUTH_TOKEN_CACHE_KEY, token);
|
||||
|
||||
export const getAuthToken = () => queryClient.getQueryData(AUTH_TOKEN_CACHE_KEY) as string;
|
||||
|
||||
export const isLoggedIn = () => Boolean(getAuthToken());
|
||||
Reference in New Issue
Block a user