diff --git a/app/soapbox/actions/auth.ts b/app/soapbox/actions/auth.ts index d3252e7fb..53209464e 100644 --- a/app/soapbox/actions/auth.ts +++ b/app/soapbox/actions/auth.ts @@ -202,9 +202,7 @@ export const rememberAuthAccount = (accountUrl: string) => export const loadCredentials = (token: string, accountUrl: string) => (dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl)) - .then(() => { - dispatch(verifyCredentials(token, accountUrl)); - }) + .then(() => dispatch(verifyCredentials(token, accountUrl))) .catch(() => dispatch(verifyCredentials(token, accountUrl))); /** Trim the username and strip the leading @. */ diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 099bb3b96..54404b92a 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -17,6 +17,7 @@ import * as BuildConfig from 'soapbox/build_config'; import GdprBanner from 'soapbox/components/gdpr-banner'; import Helmet from 'soapbox/components/helmet'; import LoadingScreen from 'soapbox/components/loading-screen'; +import { AuthProvider, useAuth } from 'soapbox/contexts/auth-context'; import AuthLayout from 'soapbox/features/auth_layout'; import EmbeddedStatus from 'soapbox/features/embedded-status'; import PublicLayout from 'soapbox/features/public_layout'; @@ -40,6 +41,7 @@ import { } from 'soapbox/hooks'; import MESSAGES from 'soapbox/locales/messages'; import { queryClient } from 'soapbox/queries/client'; +import { useAxiosInterceptors } from 'soapbox/queries/client'; import { useCachedLocationHandler } from 'soapbox/utils/redirect'; import { generateThemeCss } from 'soapbox/utils/theme'; @@ -63,7 +65,7 @@ const loadInitial = () => { // @ts-ignore return async(dispatch, getState) => { // Await for authenticated fetch - await dispatch(fetchMe()); + const account = await dispatch(fetchMe()); // Await for feature detection await dispatch(loadInstance()); // Await for configuration @@ -76,12 +78,15 @@ const loadInitial = () => { if (pepeEnabled && !state.me) { await dispatch(fetchVerificationConfig()); } + + return account; }; }; /** Highest level node with the Redux store. */ const SoapboxMount = () => { useCachedLocationHandler(); + const me = useAppSelector(state => state.me); const instance = useAppSelector(state => state.instance); const account = useOwnAccount(); @@ -204,6 +209,9 @@ interface ISoapboxLoad { /** Initial data loader. */ const SoapboxLoad: React.FC = ({ children }) => { const dispatch = useAppDispatch(); + const { setAccount, token, baseApiUri } = useAuth(); + + useAxiosInterceptors(token, baseApiUri); const me = useAppSelector(state => state.me); const account = useOwnAccount(); @@ -233,7 +241,8 @@ const SoapboxLoad: React.FC = ({ children }) => { // Load initial data from the API useEffect(() => { - dispatch(loadInitial()).then(() => { + dispatch(loadInitial()).then((account) => { + setAccount(account); setIsLoaded(true); }).catch(() => { setIsLoaded(true); @@ -292,13 +301,15 @@ const SoapboxHead: React.FC = ({ children }) => { const Soapbox: React.FC = () => { return ( - - - - - - - + + + + + + + + + ); }; diff --git a/app/soapbox/contexts/auth-context.tsx b/app/soapbox/contexts/auth-context.tsx new file mode 100644 index 000000000..7c023e92d --- /dev/null +++ b/app/soapbox/contexts/auth-context.tsx @@ -0,0 +1,77 @@ + +import React, { + createContext, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; + +import { localState } from 'soapbox/reducers/auth'; +import { parseBaseURL } from 'soapbox/utils/auth'; + +const AuthContext = createContext(null as any); + +interface IAccount { + acct: string + avatar: string + avatar_static: string + bot: boolean + created_at: string + discoverable: boolean + display_name: string + emojis: string[] + fields: any // not sure + followers_count: number + following_count: number + group: boolean + header: string + header_static: string + id: string + last_status_at: string + location: string + locked: boolean + note: string + statuses_count: number + url: string + username: string + verified: boolean + website: string +} + +export const AuthProvider: React.FC = ({ children }: { children: React.ReactNode }) => { + const [account, setAccount] = useState(); + const [token, setToken] = useState(); + const [baseApiUri, setBaseApiUri] = useState(); + + const value = useMemo(() => ({ + account, + baseApiUri, + setAccount, + token, + }), [account]); + + useEffect(() => { + const cachedAuth: any = localState?.toJS(); + + if (cachedAuth?.me) { + setToken(cachedAuth.users[cachedAuth.me].access_token); + setBaseApiUri(parseBaseURL(cachedAuth.users[cachedAuth.me].url)); + } + }, []); + + return ( + + {children} + + ); +}; + +interface IAuth { + account: IAccount + baseApiUri: string + setAccount(account: IAccount): void + token: string +} + +export const useAuth = (): IAuth => useContext(AuthContext); diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index e7c0f74e2..31d555f4a 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -1,5 +1,6 @@ 'use strict'; +import { QueryClientProvider } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { HotKeys } from 'react-hotkeys'; @@ -119,6 +120,7 @@ import { WrappedRoute } from './util/react_router_helpers'; // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import 'soapbox/components/status'; +import { queryClient } from 'soapbox/queries/client'; const EmptyPage = HomePage; @@ -648,51 +650,53 @@ const UI: React.FC = ({ children }) => { }; return ( - -
- + + +
+ -
- +
+ - - - {!standalone && } - + + + {!standalone && } + - - {children} - - + + {children} + + - {me && floatingActionButton} + {me && floatingActionButton} - - {Component => } - + + {Component => } + - {me && ( - + {me && ( + + {Component => } + + )} + {me && features.chats && !mobile && ( + + {Component => } + + )} + + + {Component => } - )} - {me && features.chats && !mobile && ( - + + {Component => } - )} - - - - {Component => } - - - - {Component => } - +
-
-
+ +
); }; diff --git a/app/soapbox/queries/client.ts b/app/soapbox/queries/client.ts index d772e9288..dd9fafa46 100644 --- a/app/soapbox/queries/client.ts +++ b/app/soapbox/queries/client.ts @@ -1,4 +1,38 @@ import { QueryClient } from '@tanstack/react-query'; +import axios, { AxiosRequestConfig } from 'axios'; + +import * as BuildConfig from 'soapbox/build_config'; +import { isURL } from 'soapbox/utils/auth'; + +const maybeParseJSON = (data: string) => { + try { + return JSON.parse(data); + } catch (Exception) { + return data; + } +}; + +const API = axios.create({ + transformResponse: [maybeParseJSON], +}); + +const useAxiosInterceptors = (token: string, baseApiUri: string) => { + API.interceptors.request.use( + async (config: AxiosRequestConfig) => { + if (token) { + config.baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseApiUri; + // eslint-disable-next-line no-param-reassign + config.headers = { + ...config.headers, + Authorization: (token ? `Bearer ${token}` : null) as string | number | boolean | string[] | undefined, + } as any; + } + + return config; + }, + (error) => Promise.reject(error), + ); +}; const queryClient = new QueryClient({ defaultOptions: { @@ -10,4 +44,4 @@ const queryClient = new QueryClient({ }, }); -export { queryClient }; +export { API as default, queryClient, useAxiosInterceptors }; diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 2f0c773cd..ad0da1a1d 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -38,7 +38,7 @@ const getSessionUser = () => { }; const sessionUser = getSessionUser(); -const localState = fromJS(JSON.parse(localStorage.getItem(STORAGE_KEY))); +export const localState = fromJS(JSON.parse(localStorage.getItem(STORAGE_KEY))); // Checks if the user has an ID and access token const validUser = user => {