From 347950adbf16eb985d6877c89830face8fb51b81 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 26 Dec 2021 02:03:36 +0900 Subject: [PATCH] Add refresh token senario on auth (#516) * Add datatype to schema * Add refresh token senario on auth * Add [ErrorBoundary] and `getIdToken` in `fetchGraphql` * Impl `getIdToken` api --- client/assets/langs/en.json | 1 + client/assets/langs/ko.json | 1 + client/package.json | 1 + client/src/App.tsx | 7 + client/src/apis/auth.ts | 45 +++++ client/src/apis/sample.ts | 4 +- .../MainStackNavigator/LeaveChannelModal.tsx | 4 +- client/src/relay/fetch.ts | 6 +- client/src/utils/error.tsx | 58 ++++++ client/yarn.lock | 5 + .../migration.sql | 167 ++++++++++++++++++ server/prisma/schema.prisma | 41 ++--- server/src/apis/root.ts | 39 +++- server/src/resolvers/User/mutation.ts | 24 +-- server/src/services/UserService.ts | 6 +- server/src/utils/const.ts | 2 + server/src/utils/jwt.ts | 139 +++++++++++++++ 17 files changed, 508 insertions(+), 42 deletions(-) create mode 100644 client/src/apis/auth.ts create mode 100644 client/src/utils/error.tsx create mode 100644 server/prisma/migrations/20211218094713_datatype_refresh_token/migration.sql create mode 100644 server/src/utils/jwt.ts diff --git a/client/assets/langs/en.json b/client/assets/langs/en.json index 65cfe8988..d06cbb565 100644 --- a/client/assets/langs/en.json +++ b/client/assets/langs/en.json @@ -109,6 +109,7 @@ "TAKE_A_PICTURE": "Take a picture", "TERMS_FOR_AGREEMENT": "Terms for agreement", "THUMBNAIL_ERROR": "No Video Preview", + "RELOAD": "Reload", "TRY_AGAIN": "Try again", "UNBAN_USER": "Unban User", "UNBAN_USER_TEXT": "Do you wan to unban current user? If then, you will start to see and receive messages from the user.", diff --git a/client/assets/langs/ko.json b/client/assets/langs/ko.json index aeee4576c..c011903c3 100644 --- a/client/assets/langs/ko.json +++ b/client/assets/langs/ko.json @@ -109,6 +109,7 @@ "TAKE_A_PICTURE": "촬영하기", "TERMS_FOR_AGREEMENT": "이용약관", "THUMBNAIL_ERROR": "미리보기 실패", + "RELOAD": "새로고침", "TRY_AGAIN": "다시 시도", "UNBAN_USER": "차단 해제", "UNBAN_USER_TEXT": "선택한 유저의 차단을 해지하시겠습니까? 앞으로 해당 유저의 모든 메시지를 보거나 받을 수 있습니다.", diff --git a/client/package.json b/client/package.json index 427425e67..f4bd36964 100644 --- a/client/package.json +++ b/client/package.json @@ -76,6 +76,7 @@ "react": "17.0.2", "react-native": "0.64.3", "react-native-appearance": "~0.3.4", + "react-native-error-boundary": "^1.1.12", "react-native-gesture-handler": "~1.10.2", "react-native-get-random-values": "~1.7.1", "react-native-modalbox": "^2.0.2", diff --git a/client/src/App.tsx b/client/src/App.tsx index b0a078d11..4cf408669 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,7 @@ import * as Notifications from 'expo-notifications'; import * as SplashScreen from 'expo-splash-screen'; +import FallbackComponent, {handleError} from './utils/error'; import React, {FC, ReactElement, ReactNode, useEffect} from 'react'; import {dark, light} from './theme'; @@ -12,6 +13,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import {AuthProvider} from './providers/AuthProvider'; import ComponentWrapper from './utils/ComponentWrapper'; import {DeviceProvider} from './providers/DeviceProvider'; +import ErrorBoundary from 'react-native-error-boundary'; import Icons from './utils/Icons'; import {RelayEnvironmentProvider} from 'react-relay'; import RootNavigator from './components/navigations/RootStackNavigator'; @@ -99,6 +101,11 @@ const WrappedApp = new ComponentWrapper(RootNavigator) .wrap(ActionSheetProviderWithChildren, {}) .wrap(AuthProvider, {}) .wrap(RelayEnvironmentProvider, {environment}) + // @ts-ignore + .wrap(ErrorBoundary, { + onError: handleError, + FallbackComponent, + }) .wrap(DeviceProvider, {}) .wrap(SnackbarProvider, {}) .wrap(HackatalkThemeProvider, {}) diff --git a/client/src/apis/auth.ts b/client/src/apis/auth.ts new file mode 100644 index 000000000..568e0370e --- /dev/null +++ b/client/src/apis/auth.ts @@ -0,0 +1,45 @@ +import * as Config from '../../config'; + +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const {ROOT_URL} = Config; + +type Result = { + token?: string; + message?: string; +}; + +export const getIdToken = async ( + body?: Record, + signal?: AbortController['signal'], +): Promise => { + const token = (await AsyncStorage.getItem('token')) || ''; + + if (!token) { + return ''; + } + + const fetchOption = { + signal, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: token, + }, + body: JSON.stringify(body), + }; + + try { + const res: Response = await fetch(`${ROOT_URL}/get_id_token`, fetchOption); + const json: Result = await res.json(); + + if (json.token && json.token !== token) { + await AsyncStorage.setItem('token', token); + } + + return json.token || ''; + } catch (err: any) { + throw new Error(err); + } +}; diff --git a/client/src/apis/sample.ts b/client/src/apis/sample.ts index 46f72e20d..c9d9df13c 100644 --- a/client/src/apis/sample.ts +++ b/client/src/apis/sample.ts @@ -9,10 +9,10 @@ export const sample = async ( const fetchOption = { // signal, method: 'POST', - headers: new Headers({ + headers: { Accept: 'application/json', 'Content-Type': 'application/json', - }), + }, body: JSON.stringify(body), }; diff --git a/client/src/components/navigations/MainStackNavigator/LeaveChannelModal.tsx b/client/src/components/navigations/MainStackNavigator/LeaveChannelModal.tsx index 13c8a14d8..1183e2d33 100644 --- a/client/src/components/navigations/MainStackNavigator/LeaveChannelModal.tsx +++ b/client/src/components/navigations/MainStackNavigator/LeaveChannelModal.tsx @@ -87,9 +87,7 @@ const ChannelModalContent: FC = ({ const {channelId} = leaveChannelModalState; const mutationConfig = { - variables: { - channelId, - }, + variables: {channelId}, updater: (store: RecordSourceSelectorProxy<{}>) => { deleteChannelAndUpdate(store, channelId); }, diff --git a/client/src/relay/fetch.ts b/client/src/relay/fetch.ts index 1aab7bc6a..e85567357 100644 --- a/client/src/relay/fetch.ts +++ b/client/src/relay/fetch.ts @@ -1,7 +1,7 @@ import * as Config from '../../config'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import {FetchFunction} from 'relay-runtime'; +import {getIdToken} from '../apis/auth'; const {GRAPHQL_URL} = Config; @@ -17,10 +17,12 @@ const fetchGraphQL: FetchFunction = async ( cacheConfig, uploadables, ) => { + const token = await getIdToken(); + const config: RequestProps = { method: 'POST', headers: { - Authorization: (await AsyncStorage.getItem('token')) || '', + Authorization: token, Accept: 'application/json', }, body: '', diff --git a/client/src/utils/error.tsx b/client/src/utils/error.tsx new file mode 100644 index 000000000..8806b28de --- /dev/null +++ b/client/src/utils/error.tsx @@ -0,0 +1,58 @@ +import {Button, Typography} from 'dooboo-ui'; +import {DevSettings, View} from 'react-native'; +import React, {ReactElement, useEffect} from 'react'; + +import AsyncStorage from '@react-native-async-storage/async-storage'; +import {getString} from '../../STRINGS'; +import styled from '@emotion/native'; + +const Container = styled.SafeAreaView` + margin: 0 24px; + + flex: 1; + justify-content: center; +`; + +export const handleError = (error: any, stack?: string): void => { + if (__DEV__) { + // eslint-disable-next-line no-console + console.error(stack, error); + } +}; + +interface Props { + error: Error; + resetError: Function; +} + +export default function FallbackComponent({resetError}: Props): ReactElement { + useEffect(() => { + AsyncStorage.removeItem('token'); + AsyncStorage.removeItem('push_token'); + + resetError(); + }, [resetError]); + + return ( + + <> + {getString('ERROR_OCCURED')} + + +