mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05: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
|
context: ./frontend
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile.dev
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend/src/pages:/app/src/pages
|
- ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
||||||
- ./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/public:/app/public
|
- ./frontend/public:/app/public
|
||||||
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
|
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
|||||||
50
frontend/package-lock.json
generated
50
frontend/package-lock.json
generated
@@ -28,6 +28,7 @@
|
|||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
|
"@tanstack/react-query": "^4.23.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"axios-auth-refresh": "^3.3.3",
|
"axios-auth-refresh": "^3.3.3",
|
||||||
@@ -6646,6 +6647,41 @@
|
|||||||
"tailwindcss": ">=3.0.0 || insiders"
|
"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": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz",
|
||||||
@@ -26993,6 +27029,20 @@
|
|||||||
"postcss-selector-parser": "6.0.10"
|
"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": {
|
"@testing-library/dom": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
|
"@tanstack/react-query": "^4.23.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"axios-auth-refresh": "^3.3.3",
|
"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 {
|
export default class SecurityClient {
|
||||||
static #token = '';
|
|
||||||
|
|
||||||
static setToken(tokenStr: string) {
|
static setToken(tokenStr: string) {
|
||||||
this.#token = tokenStr;
|
setAuthToken(tokenStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
|
static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
|
||||||
const req = new Request(resource, options);
|
const req = new Request(resource, options);
|
||||||
|
|
||||||
if (this.#token === '') {
|
const token = getAuthToken();
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#token) {
|
if (token) {
|
||||||
req.headers.set('Authorization', `Bearer ${this.#token}`);
|
req.headers.set('Authorization', `Bearer ${token}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(req);
|
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 { 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 { useRouter } from 'next/router';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import Layout from '@app/components/basic/Layout';
|
import Layout from '@app/components/basic/Layout';
|
||||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||||
import RouteGuard from '@app/components/RouteGuard';
|
|
||||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||||
import { publicPaths } from '@app/const';
|
import { publicPaths } from '@app/const';
|
||||||
|
import { AuthProvider } from '@app/context/AuthContext';
|
||||||
|
import { queryClient } from '@app/reactQuery';
|
||||||
|
|
||||||
import '@fortawesome/fontawesome-svg-core/styles.css';
|
import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||||
import '../styles/globals.css';
|
import '../styles/globals.css';
|
||||||
@@ -54,17 +56,25 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
publicPaths.includes(`/${appProps.router.pathname.split('/')[1]}`) ||
|
publicPaths.includes(`/${appProps.router.pathname.split('/')[1]}`) ||
|
||||||
!Component.requireAuth
|
!Component.requireAuth
|
||||||
) {
|
) {
|
||||||
return <Component {...pageProps} />;
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AuthProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</AuthProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteGuard>
|
<QueryClientProvider client={queryClient}>
|
||||||
<NotificationProvider>
|
<AuthProvider>
|
||||||
<Layout>
|
<NotificationProvider>
|
||||||
<Component {...pageProps} />
|
<Layout>
|
||||||
</Layout>
|
<Component {...pageProps} />
|
||||||
</NotificationProvider>
|
</Layout>
|
||||||
</RouteGuard>
|
</NotificationProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import InputField from '@app/components/basic/InputField';
|
|||||||
import ListBox from '@app/components/basic/Listbox';
|
import ListBox from '@app/components/basic/Listbox';
|
||||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||||
|
import { isLoggedIn } from '@app/reactQuery';
|
||||||
|
|
||||||
import getWorkspaces from './api/workspace/getWorkspaces';
|
import getWorkspaces from './api/workspace/getWorkspaces';
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export default function Login() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// TODO(akhilmhdh): workspace will be controlled by a workspace context
|
||||||
const redirectToDashboard = async () => {
|
const redirectToDashboard = async () => {
|
||||||
let userWorkspace;
|
let userWorkspace;
|
||||||
try {
|
try {
|
||||||
@@ -41,7 +43,9 @@ export default function Login() {
|
|||||||
console.log('Error - Not logged in yet');
|
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