From c36494c3f97d17ea4010aa359c62c7b520053b10 Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Tue, 2 Mar 2021 14:17:27 +0700 Subject: [PATCH] auth: refactored sagas to use try-catch --- package.json | 2 +- src/redux/auth/api.ts | 158 +++++----------- src/redux/auth/constants.ts | 26 +-- src/redux/auth/index.ts | 23 ++- src/redux/auth/sagas.ts | 353 ++++++++++++++++------------------- src/redux/auth/selectors.ts | 2 +- src/redux/auth/transforms.ts | 11 +- src/redux/auth/types.ts | 59 +++++- src/redux/boris/sagas.ts | 6 +- src/redux/flow/sagas.ts | 25 +-- src/redux/messages/index.ts | 2 +- src/redux/messages/sagas.ts | 24 +-- src/redux/modal/index.ts | 2 +- src/redux/node/sagas.ts | 56 +++--- src/redux/player/sagas.ts | 2 +- src/redux/store.ts | 28 ++- src/redux/tag/sagas.ts | 23 ++- src/redux/types.ts | 8 +- src/redux/uploads/sagas.ts | 4 +- src/utils/api/index.ts | 4 +- src/utils/validators.ts | 4 +- tsconfig.json | 2 +- 22 files changed, 400 insertions(+), 424 deletions(-) diff --git a/package.json b/package.json index 03f37c0f..4374ead7 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "start": "craco start", "build": "craco build", "test": "craco test", - "eject": "craco eject" + "ts-check": "tsc -p tsconfig.json --noEmit" }, "eslintConfig": { "extends": [ diff --git a/src/redux/auth/api.ts b/src/redux/auth/api.ts index 18cb240c..25f38508 100644 --- a/src/redux/auth/api.ts +++ b/src/redux/auth/api.ts @@ -1,55 +1,46 @@ -import { api, configWithToken, errorMiddleware, resultMiddleware } from '~/utils/api'; +import { api, cleanResult, errorMiddleware, resultMiddleware } from '~/utils/api'; import { API } from '~/constants/api'; -import { INotification, IResultWithStatus } from '~/redux/types'; -import { userLoginTransform } from '~/redux/auth/transforms'; -import { ISocialAccount, IUser } from './types'; +import { IResultWithStatus } from '~/redux/types'; +import { + ApiAttachSocialRequest, + ApiAttachSocialResult, + ApiAuthGetUpdatesRequest, + ApiAuthGetUpdatesResult, + ApiAuthGetUserProfileRequest, + ApiAuthGetUserProfileResult, + ApiAuthGetUserResult, + ApiCheckRestoreCodeRequest, + ApiCheckRestoreCodeResult, + ApiDropSocialRequest, + ApiDropSocialResult, + ApiGetSocialsResult, + ApiLoginWithSocialRequest, + ApiLoginWithSocialResult, + ApiRestoreCodeRequest, + ApiRestoreCodeResult, + ApiUpdateUserRequest, + ApiUpdateUserResult, + ApiUserLoginRequest, + ApiUserLoginResult, +} from './types'; -export const apiUserLogin = ({ - username, - password, -}: { - username: string; - password: string; -}): Promise> => +export const apiUserLogin = ({ username, password }: ApiUserLoginRequest) => api - .post(API.USER.LOGIN, { username, password }) - .then(resultMiddleware) - .catch(errorMiddleware) - .then(userLoginTransform); + .post(API.USER.LOGIN, { username, password }) + .then(cleanResult); -export const apiAuthGetUser = ({ access }): Promise> => - api - .get(API.USER.ME, configWithToken(access)) - .then(resultMiddleware) - .catch(errorMiddleware); +export const apiAuthGetUser = () => api.get(API.USER.ME).then(cleanResult); -export const apiAuthGetUserProfile = ({ - access, - username, -}): Promise> => - api - .get(API.USER.PROFILE(username), configWithToken(access)) - .then(resultMiddleware) - .catch(errorMiddleware); +export const apiAuthGetUserProfile = ({ username }: ApiAuthGetUserProfileRequest) => + api.get(API.USER.PROFILE(username)).then(cleanResult); -export const apiAuthGetUpdates = ({ - access, - exclude_dialogs, - last, -}): Promise> => +export const apiAuthGetUpdates = ({ exclude_dialogs, last }: ApiAuthGetUpdatesRequest) => api - .get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } })) - .then(resultMiddleware) - .catch(errorMiddleware); + .get(API.USER.GET_UPDATES, { params: { exclude_dialogs, last } }) + .then(cleanResult); -export const apiUpdateUser = ({ access, user }): Promise> => - api - .patch(API.USER.ME, user, configWithToken(access)) - .then(resultMiddleware) - .catch(errorMiddleware); +export const apiUpdateUser = ({ user }: ApiUpdateUserRequest) => + api.patch(API.USER.ME, user).then(cleanResult); export const apiRequestRestoreCode = ({ field }): Promise> => api @@ -57,75 +48,26 @@ export const apiRequestRestoreCode = ({ field }): Promise> .then(resultMiddleware) .catch(errorMiddleware); -export const apiCheckRestoreCode = ({ code }): Promise> => - api - .get(API.USER.REQUEST_CODE(code)) - .then(resultMiddleware) - .catch(errorMiddleware); +export const apiCheckRestoreCode = ({ code }: ApiCheckRestoreCodeRequest) => + api.get(API.USER.REQUEST_CODE(code)).then(cleanResult); -export const apiRestoreCode = ({ code, password }): Promise> => +export const apiRestoreCode = ({ code, password }: ApiRestoreCodeRequest) => api - .post(API.USER.REQUEST_CODE(code), { password }) - .then(resultMiddleware) - .catch(errorMiddleware); + .post(API.USER.REQUEST_CODE(code), { password }) + .then(cleanResult); -export const apiGetSocials = ({ - access, -}: { - access: string; -}): Promise> => - api - .get(API.USER.GET_SOCIALS, configWithToken(access)) - .then(resultMiddleware) - .catch(errorMiddleware); +export const apiGetSocials = () => + api.get(API.USER.GET_SOCIALS).then(cleanResult); -export const apiDropSocial = ({ - access, - id, - provider, -}: { - access: string; - id: string; - provider: string; -}): Promise> => - api - .delete(API.USER.DROP_SOCIAL(provider, id), configWithToken(access)) - .then(resultMiddleware) - .catch(errorMiddleware); +export const apiDropSocial = ({ id, provider }: ApiDropSocialRequest) => + api.delete(API.USER.DROP_SOCIAL(provider, id)).then(cleanResult); -export const apiAttachSocial = ({ - access, - token, -}: { - access: string; - token: string; -}): Promise> => +export const apiAttachSocial = ({ token }: ApiAttachSocialRequest) => api - .post(API.USER.ATTACH_SOCIAL, { token }, configWithToken(access)) - .then(resultMiddleware) - .catch(errorMiddleware); + .post(API.USER.ATTACH_SOCIAL, { token }) + .then(cleanResult); -export const apiLoginWithSocial = ({ - token, - username, - password, -}: { - token: string; - username?: string; - password?: string; -}): Promise; - needs_register: boolean; -}>> => +export const apiLoginWithSocial = ({ token, username, password }: ApiLoginWithSocialRequest) => api - .post(API.USER.LOGIN_WITH_SOCIAL, { token, username, password }) - .then(resultMiddleware) - .catch(errorMiddleware); + .post(API.USER.LOGIN_WITH_SOCIAL, { token, username, password }) + .then(cleanResult); diff --git a/src/redux/auth/constants.ts b/src/redux/auth/constants.ts index 5c66e845..d2959fdc 100644 --- a/src/redux/auth/constants.ts +++ b/src/redux/auth/constants.ts @@ -53,26 +53,26 @@ export const USER_ROLES = { }; export const EMPTY_TOKEN: IToken = { - access: null, - refresh: null, + access: '', + refresh: '', }; export const EMPTY_USER: IUser = { - id: null, + id: 0, role: USER_ROLES.GUEST, - email: null, - name: null, - username: null, - photo: null, - cover: null, + email: '', + name: '', + username: '', + photo: undefined, + cover: undefined, is_activated: false, is_user: false, - fullname: null, - description: null, + fullname: '', + description: '', - last_seen: null, - last_seen_messages: null, - last_seen_boris: null, + last_seen: '', + last_seen_messages: '', + last_seen_boris: '', }; export interface IApiUser { diff --git a/src/redux/auth/index.ts b/src/redux/auth/index.ts index 008d581f..357cb37e 100644 --- a/src/redux/auth/index.ts +++ b/src/redux/auth/index.ts @@ -8,17 +8,17 @@ const HANDLERS = { }; const INITIAL_STATE: IAuthState = { - token: null, + token: '', user: { ...EMPTY_USER }, updates: { - last: null, + last: '', notifications: [], - boris_commented_at: null, + boris_commented_at: '', }, login: { - error: null, + error: '', is_loading: false, is_registering: true, }, @@ -27,7 +27,7 @@ const INITIAL_STATE: IAuthState = { tab: 'profile', is_loading: true, - user: null, + user: undefined, patch_errors: {}, socials: { @@ -39,20 +39,19 @@ const INITIAL_STATE: IAuthState = { restore: { code: '', - user: null, + user: undefined, is_loading: false, is_succesfull: false, - error: null, + error: '', }, register_social: { errors: { - username: 'and this', - password: 'dislike this', + username: '', + password: '', }, - error: 'dont like this one', - token: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJEYXRhIjp7IlByb3ZpZGVyIjoiZ29vZ2xlIiwiSWQiOiJma2F0dXJvdkBpY2Vyb2NrZGV2LmNvbSIsIkVtYWlsIjoiZmthdHVyb3ZAaWNlcm9ja2Rldi5jb20iLCJUb2tlbiI6InlhMjkuYTBBZkg2U01EeXFGdlRaTExXckhsQm1QdGZIOFNIVGQteWlSYTFKSXNmVXluY2F6MTZ5UGhjRmxydTlDMWFtTEg0aHlHRzNIRkhrVGU0SXFUS09hVVBEREdqR2JQRVFJbGpPME9UbUp2T2RrdEtWNDVoUGpJcTB1cHVLc003UWJLSm1oRWhkMEFVa3YyejVHWlNSMjhaM2VOZVdwTEVYSGV0MW1yNyIsIkZldGNoZWQiOnsiUHJvdmlkZXIiOiJnb29nbGUiLCJJZCI6OTIyMzM3MjAzNjg1NDc3NTgwNywiTmFtZSI6IkZlZG9yIEthdHVyb3YiLCJQaG90byI6Imh0dHBzOi8vbGg2Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8ta1VMYXh0VV9jZTAvQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQU1adXVjbkEycTFReU1WLUN0RUtBclRhQzgydE52NTM2QS9waG90by5qcGcifX0sIlR5cGUiOiJvYXV0aF9jbGFpbSJ9.r1MY994BC_g4qRDoDoyNmwLs0qRzBLx6_Ez-3mHQtwg', + error: '', + token: '', is_loading: false, }, }; diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts index 89c75b15..71af4744 100644 --- a/src/redux/auth/sagas.ts +++ b/src/redux/auth/sagas.ts @@ -1,5 +1,5 @@ import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; -import { AUTH_USER_ACTIONS, EMPTY_USER, USER_ERRORS, USER_ROLES } from '~/redux/auth/constants'; +import { AUTH_USER_ACTIONS, EMPTY_USER, USER_ROLES } from '~/redux/auth/constants'; import { authAttachSocial, authDropSocial, @@ -48,49 +48,37 @@ import { selectAuthRestore, selectAuthUpdates, selectAuthUser, - selectToken, } from './selectors'; -import { IResultWithStatus, OAUTH_EVENT_TYPES, Unwrap } from '../types'; -import { IAuthState, IUser } from './types'; +import { OAUTH_EVENT_TYPES, Unwrap } from '../types'; +import { IAuthState } from './types'; import { REHYDRATE, RehydrateAction } from 'redux-persist'; import { selectModal } from '~/redux/modal/selectors'; import { IModalState } from '~/redux/modal'; import { DIALOGS } from '~/redux/modal/constants'; import { ERRORS } from '~/constants/errors'; import { messagesSet } from '~/redux/messages/actions'; +import { SagaIterator } from 'redux-saga'; -export function* reqWrapper(requestAction, props = {}): ReturnType { - const access = yield select(selectToken); - - const result = yield call(requestAction, { access, ...props }); - - if (result && result.status === 401) { - return { error: USER_ERRORS.UNAUTHORIZED, data: {} }; - } - - return result; +function* setTokenSaga({ token }: ReturnType) { + localStorage.setItem('token', token); } function* sendLoginRequestSaga({ username, password }: ReturnType) { if (!username || !password) return; - const { - error, - data: { token, user }, - }: IResultWithStatus<{ token: string; user: IUser }> = yield call(apiUserLogin, { - username, - password, - }); + try { + const { token, user }: Unwrap = yield call(apiUserLogin, { + username, + password, + }); - if (error) { - yield put(userSetLoginError(error)); - return; + yield put(authSetToken(token)); + yield put(authSetUser({ ...user, is_user: true })); + yield put(authLoggedIn()); + yield put(modalSetShown(false)); + } catch (e) { + yield put(userSetLoginError(e)); } - - yield put(authSetToken(token)); - yield put(authSetUser({ ...user, is_user: true })); - yield put(authLoggedIn()); - yield put(modalSetShown(false)); } function* refreshUser() { @@ -98,23 +86,18 @@ function* refreshUser() { if (!token) return; - const { - error, - data: { user }, - }: IResultWithStatus<{ user: IUser }> = yield call(reqWrapper, apiAuthGetUser); + try { + const { user }: Unwrap = yield call(apiAuthGetUser); - if (error) { + yield put(authSetUser({ ...user, is_user: true })); + } catch (e) { yield put( authSetUser({ ...EMPTY_USER, is_user: false, }) ); - - return; } - - yield put(authSetUser({ ...user, is_user: true })); } function* checkUserSaga({ key }: RehydrateAction) { @@ -126,44 +109,43 @@ function* gotPostMessageSaga({ token }: ReturnType) { yield put(authSetToken(token)); yield call(refreshUser); - const { is_shown, dialog }: IModalState = yield select(selectModal); + const { is_shown, dialog }: ReturnType = yield select(selectModal); if (is_shown && dialog === DIALOGS.LOGIN) yield put(modalSetShown(false)); } function* logoutSaga() { - yield put(authSetToken(null)); + yield put(authSetToken('')); yield put(authSetUser({ ...EMPTY_USER })); yield put( authSetUpdates({ - last: null, + last: '', notifications: [], }) ); } -function* loadProfile({ username }: ReturnType) { +function* loadProfile({ username }: ReturnType): SagaIterator { yield put(authSetProfile({ is_loading: true })); - const { - error, - data: { user }, - } = yield call(reqWrapper, apiAuthGetUserProfile, { username }); + try { + const { user }: Unwrap = yield call(apiAuthGetUserProfile, { + username, + }); - if (error || !user) { + yield put(authSetProfile({ is_loading: false, user })); + yield put(messagesSet({ messages: [] })); + return true; + } catch (error) { return false; } - - yield put(authSetProfile({ is_loading: false, user })); - yield put(messagesSet({ messages: [] })); - return true; } function* openProfile({ username, tab = 'profile' }: ReturnType) { yield put(modalShowDialog(DIALOGS.PROFILE)); yield put(authSetProfile({ tab })); - const success: boolean = yield call(loadProfile, authLoadProfile(username)); + const success: Unwrap = yield call(loadProfile, authLoadProfile(username)); if (!success) { return yield put(modalSetShown(false)); @@ -171,42 +153,39 @@ function* openProfile({ username, tab = 'profile' }: ReturnType = yield select(selectAuthUser); + try { + const user: ReturnType = yield select(selectAuthUser); - if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id) return; + if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id) return; - const modal: IModalState = yield select(selectModal); - const profile: IAuthState['profile'] = yield select(selectAuthProfile); - const { last, boris_commented_at }: IAuthState['updates'] = yield select(selectAuthUpdates); - const exclude_dialogs = - modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user.id ? profile.user.id : null; + const modal: ReturnType = yield select(selectModal); + const profile: ReturnType = yield select(selectAuthProfile); + const { last, boris_commented_at }: IAuthState['updates'] = yield select(selectAuthUpdates); + const exclude_dialogs = + modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user?.id ? profile.user.id : 0; - const { error, data }: Unwrap> = yield call( - reqWrapper, - apiAuthGetUpdates, - { exclude_dialogs, last: last || user.last_seen_messages } - ); + const data: Unwrap = yield call(apiAuthGetUpdates, { + exclude_dialogs, + last: last || user.last_seen_messages, + }); - if (error || !data) { - return; - } + if (data.notifications && data.notifications.length) { + yield put( + authSetUpdates({ + last: data.notifications[0].created_at, + notifications: data.notifications, + }) + ); + } - if (data.notifications && data.notifications.length) { - yield put( - authSetUpdates({ - last: data.notifications[0].created_at, - notifications: data.notifications, - }) - ); - } - - if (data.boris && data.boris.commented_at && boris_commented_at !== data.boris.commented_at) { - yield put( - authSetUpdates({ - boris_commented_at: data.boris.commented_at, - }) - ); - } + if (data.boris && data.boris.commented_at && boris_commented_at !== data.boris.commented_at) { + yield put( + authSetUpdates({ + boris_commented_at: data.boris.commented_at, + }) + ); + } + } catch (error) {} } function* startPollingSaga() { @@ -219,148 +198,136 @@ function* startPollingSaga() { function* setLastSeenMessages({ last_seen_messages }: ReturnType) { if (!Date.parse(last_seen_messages)) return; - yield call(reqWrapper, apiUpdateUser, { user: { last_seen_messages } }); + yield call(apiUpdateUser, { user: { last_seen_messages } }); } -function* patchUser({ user }: ReturnType) { - const me = yield select(selectAuthUser); +function* patchUser(payload: ReturnType) { + const me: ReturnType = yield select(selectAuthUser); - const { error, data } = yield call(reqWrapper, apiUpdateUser, { user }); + try { + const { user, errors }: Unwrap = yield call(apiUpdateUser, { + user: payload.user, + }); - if (error || !data.user || data.errors) { - return yield put(authSetProfile({ patch_errors: data.errors })); + if (errors && Object.keys(errors).length) { + yield put(authSetProfile({ patch_errors: errors })); + return; + } + + yield put(authSetUser({ ...me, ...user })); + yield put(authSetProfile({ user: { ...me, ...user }, tab: 'profile' })); + } catch (error) { + return; } - - yield put(authSetUser({ ...me, ...data.user })); - yield put(authSetProfile({ user: { ...me, ...data.user }, tab: 'profile' })); } function* requestRestoreCode({ field }: ReturnType) { if (!field) return; - yield put(authSetRestore({ error: null, is_loading: true })); - const { error, data } = yield call(apiRequestRestoreCode, { field }); + try { + yield put(authSetRestore({ error: '', is_loading: true })); + yield call(apiRequestRestoreCode, { + field, + }); - if (data.error || error) { - return yield put(authSetRestore({ is_loading: false, error: data.error || error })); + yield put(authSetRestore({ is_loading: false, is_succesfull: true })); + } catch (error) { + return yield put(authSetRestore({ is_loading: false, error })); } - - yield put(authSetRestore({ is_loading: false, is_succesfull: true })); } function* showRestoreModal({ code }: ReturnType) { - if (!code && !code.length) { - return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); + try { + if (!code && !code.length) { + return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); + } + + yield put(authSetRestore({ user: undefined, is_loading: true })); + + const data: Unwrap = yield call(apiCheckRestoreCode, { code }); + + yield put(authSetRestore({ user: data.user, code, is_loading: false })); + yield put(modalShowDialog(DIALOGS.RESTORE_PASSWORD)); + } catch (error) { + yield put(authSetRestore({ is_loading: false, error: error || ERRORS.CODE_IS_INVALID })); + yield put(modalShowDialog(DIALOGS.RESTORE_PASSWORD)); } - - yield put(authSetRestore({ user: null, is_loading: true })); - - const { error, data } = yield call(apiCheckRestoreCode, { code }); - - if (data.error || error || !data.user) { - yield put( - authSetRestore({ is_loading: false, error: data.error || error || ERRORS.CODE_IS_INVALID }) - ); - - return yield put(modalShowDialog(DIALOGS.RESTORE_PASSWORD)); - } - - yield put(authSetRestore({ user: data.user, code, is_loading: false })); - yield put(modalShowDialog(DIALOGS.RESTORE_PASSWORD)); } function* restorePassword({ password }: ReturnType) { - if (!password) return; + try { + if (!password) return; - yield put(authSetRestore({ is_loading: true })); - const { code } = yield select(selectAuthRestore); + yield put(authSetRestore({ is_loading: true })); + const { code } = yield select(selectAuthRestore); - if (!code) { - return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); + if (!code) { + return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); + } + + const data: Unwrap = yield call(apiRestoreCode, { code, password }); + + yield put(authSetToken(data.token)); + yield put(authSetUser(data.user)); + + yield put(authSetRestore({ is_loading: false, is_succesfull: true, error: '' })); + + yield call(refreshUser); + } catch (error) { + return yield put(authSetRestore({ is_loading: false, error: error || ERRORS.CODE_IS_INVALID })); } - - const { error, data } = yield call(apiRestoreCode, { code, password }); - - if (data.error || error || !data.user || !data.token) { - return yield put( - authSetRestore({ is_loading: false, error: data.error || error || ERRORS.CODE_IS_INVALID }) - ); - } - - yield put(authSetToken(data.token)); - yield put(authSetUser(data.user)); - - yield put(authSetRestore({ is_loading: false, is_succesfull: true, error: null })); - - yield call(refreshUser); } function* getSocials() { - yield put(authSetSocials({ is_loading: true, error: '' })); - try { - const { data, error }: Unwrap> = yield call( - reqWrapper, - apiGetSocials, - {} - ); - - if (error) { - throw new Error(error); - } - - yield put(authSetSocials({ is_loading: false, accounts: data.accounts, error: '' })); - } catch (e) { - yield put(authSetSocials({ is_loading: false, error: e.toString() })); + yield put(authSetSocials({ is_loading: true, error: '' })); + const data: Unwrap = yield call(apiGetSocials); + yield put(authSetSocials({ accounts: data.accounts })); + } catch (error) { + yield put(authSetSocials({ error })); + } finally { + yield put(authSetSocials({ is_loading: false })); } } +// TODO: start from here function* dropSocial({ provider, id }: ReturnType) { try { yield put(authSetSocials({ error: '' })); - const { error }: Unwrap> = yield call( - reqWrapper, - apiDropSocial, - { id, provider } - ); - - if (error) { - throw new Error(error); - } + yield call(apiDropSocial, { + id, + provider, + }); yield call(getSocials); - } catch (e) { - yield put(authSetSocials({ error: e.message })); + } catch (error) { + yield put(authSetSocials({ error })); } } function* attachSocial({ token }: ReturnType) { - if (!token) return; - try { + if (!token) return; + yield put(authSetSocials({ error: '', is_loading: true })); - const { data, error }: Unwrap> = yield call( - reqWrapper, - apiAttachSocial, - { token } - ); - - if (error) { - throw new Error(error); - } + const data: Unwrap = yield call(apiAttachSocial, { + token, + }); const { socials: { accounts }, }: ReturnType = yield select(selectAuthProfile); if (accounts.some(it => it.id === data.account.id && it.provider === data.account.provider)) { - yield put(authSetSocials({ is_loading: false })); - } else { - yield put(authSetSocials({ is_loading: false, accounts: [...accounts, data.account] })); + return; } + + yield put(authSetSocials({ accounts: [...accounts, data.account] })); } catch (e) { - yield put(authSetSocials({ is_loading: false, error: e.message })); + yield put(authSetSocials({ error: e.message })); + } finally { + yield put(authSetSocials({ is_loading: false })); } } @@ -368,10 +335,9 @@ function* loginWithSocial({ token }: ReturnType) { try { yield put(userSetLoginError('')); - const { - data, - error, - }: Unwrap> = yield call(apiLoginWithSocial, { token }); + const data: Unwrap = yield call(apiLoginWithSocial, { + token, + }); // Backend asks us for account registration if (data?.needs_register) { @@ -380,18 +346,14 @@ function* loginWithSocial({ token }: ReturnType) { return; } - if (error) { - throw new Error(error); - } - if (data.token) { yield put(authSetToken(data.token)); yield call(refreshUser); yield put(modalSetShown(false)); return; } - } catch (e) { - yield put(userSetLoginError(e.message)); + } catch (error) { + yield put(userSetLoginError(error)); } } @@ -414,23 +376,19 @@ function* authRegisterSocial({ username, password }: ReturnType> = yield select( + const { token }: Unwrap = yield select( selectAuthRegisterSocial ); - const { data, error }: Unwrap> = yield call( - apiLoginWithSocial, - { - token, - username, - password, - } - ); + const data: Unwrap = yield call(apiLoginWithSocial, { + token, + username, + password, + }); if (data?.errors) { yield put(authSetRegisterSocialErrors(data.errors)); - } else if (data?.error) { - throw new Error(error); + return; } if (data.token) { @@ -449,6 +407,7 @@ function* authSaga() { yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga); yield takeLatest(AUTH_USER_ACTIONS.LOGOUT, logoutSaga); + yield takeLatest(AUTH_USER_ACTIONS.SET_TOKEN, setTokenSaga); yield takeLatest(AUTH_USER_ACTIONS.SEND_LOGIN_REQUEST, sendLoginRequestSaga); yield takeLatest(AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE, gotPostMessageSaga); yield takeLatest(AUTH_USER_ACTIONS.OPEN_PROFILE, openProfile); diff --git a/src/redux/auth/selectors.ts b/src/redux/auth/selectors.ts index aa3aa475..6f6ed43b 100644 --- a/src/redux/auth/selectors.ts +++ b/src/redux/auth/selectors.ts @@ -5,7 +5,7 @@ export const selectUser = (state: IState) => state.auth.user; export const selectToken = (state: IState) => state.auth.token; export const selectAuthLogin = (state: IState) => state.auth.login; export const selectAuthProfile = (state: IState) => state.auth.profile; -export const selectAuthProfileUsername = (state: IState) => state.auth.profile.user.username; +export const selectAuthProfileUsername = (state: IState) => state.auth.profile.user?.username; export const selectAuthUser = (state: IState) => state.auth.user; export const selectAuthUpdates = (state: IState) => state.auth.updates; export const selectAuthRestore = (state: IState) => state.auth.restore; diff --git a/src/redux/auth/transforms.ts b/src/redux/auth/transforms.ts index 513b07fe..69c68fc8 100644 --- a/src/redux/auth/transforms.ts +++ b/src/redux/auth/transforms.ts @@ -1,13 +1,18 @@ import { IResultWithStatus } from '~/redux/types'; import { HTTP_RESPONSES } from '~/utils/api'; -export const userLoginTransform = ({ status, data, error }: IResultWithStatus): IResultWithStatus => { +export const userLoginTransform = ({ + status, + data, + error, +}: IResultWithStatus): IResultWithStatus => { switch (true) { - case (status === HTTP_RESPONSES.UNAUTHORIZED || !data.token) && status !== HTTP_RESPONSES.CONNECTION_REFUSED: + case (status === HTTP_RESPONSES.UNAUTHORIZED || !data.token) && + status !== HTTP_RESPONSES.CONNECTION_REFUSED: return { status, data, error: 'Пользователь не найден' }; case status === 200: - return { status, data, error: null }; + return { status, data, error: '' }; default: return { status, data, error: error || 'Неизвестная ошибка' }; diff --git a/src/redux/auth/types.ts b/src/redux/auth/types.ts index 52d417d4..55d7ae82 100644 --- a/src/redux/auth/types.ts +++ b/src/redux/auth/types.ts @@ -1,4 +1,4 @@ -import { IFile, INotification } from '../types'; +import { IFile, INotification, IResultWithStatus } from '../types'; export interface IToken { access: string; @@ -10,8 +10,8 @@ export interface IUser { username: string; email: string; role: string; - photo: IFile; - cover: IFile; + photo?: IFile; + cover?: IFile; name: string; fullname: string; description: string; @@ -53,7 +53,7 @@ export type IAuthState = Readonly<{ tab: 'profile' | 'messages' | 'settings'; is_loading: boolean; - user: IUser; + user?: IUser; patch_errors: Record; socials: { @@ -65,7 +65,7 @@ export type IAuthState = Readonly<{ restore: { code: string; - user: Pick; + user?: Pick; is_loading: boolean; is_succesfull: boolean; error: string; @@ -81,3 +81,52 @@ export type IAuthState = Readonly<{ is_loading: boolean; }; }>; + +export type ApiWithTokenRequest = { access: string }; + +export type ApiUserLoginRequest = Record<'username' | 'password', string>; +export type ApiUserLoginResult = { token: string; user: IUser }; + +export type ApiAuthGetUserRequest = {}; +export type ApiAuthGetUserResult = { user: IUser }; + +export type ApiUpdateUserRequest = { user: Partial }; +export type ApiUpdateUserResult = { user: IUser; errors: Record, string> }; + +export type ApiAuthGetUserProfileRequest = { username: string }; +export type ApiAuthGetUserProfileResult = { user: IUser }; + +export type ApiAuthGetUpdatesRequest = { + exclude_dialogs: number; + last: string; +}; +export type ApiAuthGetUpdatesResult = { + notifications: INotification[]; + boris: { commented_at: string }; +}; + +export type ApiCheckRestoreCodeRequest = { code: string }; +export type ApiCheckRestoreCodeResult = { user: IUser }; + +export type ApiRestoreCodeRequest = { code: string; password: string }; +export type ApiRestoreCodeResult = { token: string; user: IUser }; + +export type ApiGetSocialsResult = { accounts: ISocialAccount[] }; + +export type ApiDropSocialRequest = { id: string; provider: string }; +export type ApiDropSocialResult = { accounts: ISocialAccount[] }; + +export type ApiAttachSocialRequest = { token: string }; +export type ApiAttachSocialResult = { account: ISocialAccount }; + +export type ApiLoginWithSocialRequest = { + token: string; + username?: string; + password?: string; +}; + +export type ApiLoginWithSocialResult = { + token: string; + errors: Record; + needs_register: boolean; +}; diff --git a/src/redux/boris/sagas.ts b/src/redux/boris/sagas.ts index 1f92b6a3..cacb77f4 100644 --- a/src/redux/boris/sagas.ts +++ b/src/redux/boris/sagas.ts @@ -8,10 +8,8 @@ function* loadStats() { yield put(borisSetStats({ is_loading: true })); try { - const git: Unwrap> = yield call(getBorisGitStats); - const backend: Unwrap> = yield call( - getBorisBackendStats - ); + const git: Unwrap = yield call(getBorisGitStats); + const backend: Unwrap = yield call(getBorisBackendStats); yield put(borisSetStats({ git, backend: backend.data, is_loading: false })); } catch (e) { diff --git a/src/redux/flow/sagas.ts b/src/redux/flow/sagas.ts index 0929999b..d8e5ec36 100644 --- a/src/redux/flow/sagas.ts +++ b/src/redux/flow/sagas.ts @@ -14,7 +14,7 @@ import { } from './actions'; import { IResultWithStatus, INode, Unwrap } from '../types'; import { selectFlowNodes, selectFlow } from './selectors'; -import { reqWrapper } from '../auth/sagas'; +import { wrap } from '../auth/sagas'; import { postCellView, getSearchResults } from './api'; import { IFlowState } from './reducer'; import { uniq } from 'ramda'; @@ -47,7 +47,7 @@ function* onGetFlow() { recent: IFlowState['recent']; updated: IFlowState['updated']; valid: INode['id'][]; - }> = yield call(reqWrapper, getNodeDiff, { + }> = yield call(wrap, getNodeDiff, { start: new Date().toISOString(), end: new Date().toISOString(), with_heroes: true, @@ -71,7 +71,7 @@ function* onSetCellView({ id, flow }: ReturnType) { const nodes = yield select(selectFlowNodes); yield put(flowSetNodes(nodes.map(node => (node.id === id ? { ...node, flow } : node)))); - const { data, error } = yield call(reqWrapper, postCellView, { id, flow }); + const { data, error } = yield call(wrap, postCellView, { id, flow }); // TODO: error handling } @@ -83,7 +83,7 @@ function* getMore() { const start = nodes && nodes[0] && nodes[0].created_at; const end = nodes && nodes[nodes.length - 1] && nodes[nodes.length - 1].created_at; - const { error, data } = yield call(reqWrapper, getNodeDiff, { + const { error, data } = yield call(wrap, getNodeDiff, { start, end, with_heroes: false, @@ -124,13 +124,9 @@ function* changeSearch({ search }: ReturnType) { yield delay(500); - const { data, error }: Unwrap> = yield call( - reqWrapper, - getSearchResults, - { - ...search, - } - ); + const { data, error }: Unwrap = yield call(wrap, getSearchResults, { + ...search, + }); if (error) { yield put(flowSetSearch({ is_loading: false, results: [], total: 0 })); @@ -155,11 +151,8 @@ function* loadMoreSearch() { const { search }: ReturnType = yield select(selectFlow); - const { - result, - delay, - }: { result: Unwrap>; delay: any } = yield race({ - result: call(reqWrapper, getSearchResults, { + const { result, delay }: { result: Unwrap; delay: any } = yield race({ + result: call(wrap, getSearchResults, { ...search, skip: search.results.length, }), diff --git a/src/redux/messages/index.ts b/src/redux/messages/index.ts index c868715b..914eb0e3 100644 --- a/src/redux/messages/index.ts +++ b/src/redux/messages/index.ts @@ -12,7 +12,7 @@ export interface IMessagesState { const INITIAL_STATE: IMessagesState = { is_loading_messages: true, is_sending_messages: false, - error: null, + error: '', messages: [], }; diff --git a/src/redux/messages/sagas.ts b/src/redux/messages/sagas.ts index c45bc6ee..cbce096d 100644 --- a/src/redux/messages/sagas.ts +++ b/src/redux/messages/sagas.ts @@ -12,7 +12,7 @@ import { } from '~/redux/messages/api'; import { ERRORS } from '~/constants/errors'; import { IMessageNotification, Unwrap } from '~/redux/types'; -import { reqWrapper } from '~/redux/auth/sagas'; +import { wrap } from '~/redux/auth/sagas'; import { messagesDeleteMessage, messagesGetMessages, @@ -39,11 +39,8 @@ function* getMessages({ username }: ReturnType) { }) ); - const { - error, - data, - }: Unwrap> = yield call( - reqWrapper, + const { error, data }: Unwrap = yield call( + wrap, apiMessagesGetUserMessages, { username } ); @@ -82,8 +79,8 @@ function* sendMessage({ message, onSuccess }: ReturnType> = yield call( - reqWrapper, + const { error, data }: Unwrap = yield call( + wrap, apiMessagesSendMessage, { username, @@ -138,8 +135,8 @@ function* deleteMessage({ id, is_locked }: ReturnType> = yield call( - reqWrapper, + const { error, data }: Unwrap = yield call( + wrap, apiMessagesDeleteMessage, { username, @@ -187,11 +184,8 @@ function* refreshMessages({}: ReturnType) { const after = messages.length > 0 ? messages[0].created_at : undefined; - const { - data, - error, - }: Unwrap> = yield call( - reqWrapper, + const { data, error }: Unwrap = yield call( + wrap, apiMessagesGetUserMessages, { username, after } ); diff --git a/src/redux/modal/index.ts b/src/redux/modal/index.ts index 7b9dfaee..95aa0cb0 100644 --- a/src/redux/modal/index.ts +++ b/src/redux/modal/index.ts @@ -14,7 +14,7 @@ export interface IModalState { const INITIAL_STATE: IModalState = { is_shown: false, - dialog: null, + dialog: '', photoswipe: { images: [], index: 0, diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts index 95f43082..53c85f8e 100644 --- a/src/redux/node/sagas.ts +++ b/src/redux/node/sagas.ts @@ -2,7 +2,13 @@ import { all, call, delay, put, select, takeLatest, takeLeading } from 'redux-sa import { push } from 'connected-react-router'; import { omit } from 'ramda'; -import { COMMENTS_DISPLAY, EMPTY_COMMENT, EMPTY_NODE, NODE_ACTIONS, NODE_EDITOR_DATA } from './constants'; +import { + COMMENTS_DISPLAY, + EMPTY_COMMENT, + EMPTY_NODE, + NODE_ACTIONS, + NODE_EDITOR_DATA, +} from './constants'; import { nodeCancelCommentEdit, nodeCreate, @@ -39,7 +45,7 @@ import { postNodeStar, updateNodeTags, } from './api'; -import { reqWrapper } from '../auth/sagas'; +import { wrap } from '../auth/sagas'; import { flowSetNodes, flowSetUpdated } from '../flow/actions'; import { ERRORS } from '~/constants/errors'; import { modalSetShown, modalShowDialog } from '../modal/actions'; @@ -77,7 +83,7 @@ function* onNodeSave({ node }: ReturnType) { const { error, data: { errors, node: result }, - } = yield call(reqWrapper, postNode, { node }); + } = yield call(wrap, postNode, { node }); if (errors && Object.values(errors).length > 0) { return yield put(nodeSetSaveErrors(errors)); @@ -117,15 +123,11 @@ function* onNodeLoadMoreComments() { comments, }: ReturnType = yield select(selectNode); - const { data, error }: Unwrap> = yield call( - reqWrapper, - getNodeComments, - { - id, - take: COMMENTS_DISPLAY, - skip: comments.length, - } - ); + const { data, error }: Unwrap = yield call(wrap, getNodeComments, { + id, + take: COMMENTS_DISPLAY, + skip: comments.length, + }); const current: ReturnType = yield select(selectNode); @@ -147,7 +149,7 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType) { const { data: { node, error }, - } = yield call(reqWrapper, getNode, { id }); + } = yield call(wrap, getNode, { id }); if (error || !node || !node.id) { yield put(push(URLS.ERRORS.NOT_FOUND)); @@ -166,8 +168,8 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType) { data: { related }, }, } = yield all({ - comments: call(reqWrapper, getNodeComments, { id, take: COMMENTS_DISPLAY, skip: 0 }), - related: call(reqWrapper, getNodeRelated, { id }), + comments: call(wrap, getNodeComments, { id, take: COMMENTS_DISPLAY, skip: 0 }), + related: call(wrap, getNodeRelated, { id }), }); yield put( @@ -190,14 +192,10 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType) { } function* onPostComment({ nodeId, comment, callback }: ReturnType) { - const { data, error }: Unwrap> = yield call( - reqWrapper, - postNodeComment, - { - data: comment, - id: nodeId, - } - ); + const { data, error }: Unwrap = yield call(wrap, postNodeComment, { + data: comment, + id: nodeId, + }); if (error || !data.comment) { return callback(error); @@ -237,7 +235,7 @@ function* onUpdateTags({ id, tags }: ReturnType) { const { data: { node }, - }: IResultWithStatus<{ node: INode }> = yield call(reqWrapper, updateNodeTags, { id, tags }); + }: IResultWithStatus<{ node: INode }> = yield call(wrap, updateNodeTags, { id, tags }); const { current } = yield select(selectNode); @@ -259,7 +257,7 @@ function* onEditSaga({ id }: ReturnType) { const { data: { node }, error, - } = yield call(reqWrapper, getNode, { id }); + } = yield call(wrap, getNode, { id }); if (error || !node || !node.type || !NODE_EDITOR_DIALOGS[node.type]) return yield put(modalSetShown(false)); @@ -282,7 +280,7 @@ function* onLikeSaga({ id }: ReturnType) { like_count: is_liked ? Math.max(like_count - 1, 0) : like_count + 1, }); - const { data, error } = yield call(reqWrapper, postNodeLike, { id }); + const { data, error } = yield call(wrap, postNodeLike, { id }); if (!error || data.is_liked === !is_liked) return; // ok and matches @@ -297,7 +295,7 @@ function* onStarSaga({ id }: ReturnType) { yield call(updateNodeEverywhere, { ...current, is_heroic: !is_heroic }); - const { data, error } = yield call(reqWrapper, postNodeStar, { id }); + const { data, error } = yield call(wrap, postNodeStar, { id }); if (!error || data.is_heroic === !is_heroic) return; // ok and matches @@ -315,7 +313,7 @@ function* onLockSaga({ id, is_locked }: ReturnType) { deleted_at: is_locked ? new Date().toISOString() : null, }); - const { error } = yield call(reqWrapper, postNodeLock, { id, is_locked }); + const { error } = yield call(wrap, postNodeLock, { id, is_locked }); if (error) return yield call(updateNodeEverywhere, { ...current, deleted_at }); } @@ -333,7 +331,7 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType) { diff --git a/src/redux/player/sagas.ts b/src/redux/player/sagas.ts index 36e4b8b4..d4a199d8 100644 --- a/src/redux/player/sagas.ts +++ b/src/redux/player/sagas.ts @@ -57,7 +57,7 @@ function* getYoutubeInfo() { } if (ticker || ids.length > 25) { - const result: Unwrap> = yield call(getEmbedYoutube, ids); + const result: Unwrap = yield call(getEmbedYoutube, ids); if (!result.error && result.data.items && Object.keys(result.data.items).length) { const { youtubes }: ReturnType = yield select(selectPlayer); diff --git a/src/redux/store.ts b/src/redux/store.ts index a26248eb..8890775e 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -26,7 +26,7 @@ import playerSaga from '~/redux/player/sagas'; import modal, { IModalState } from '~/redux/modal'; import { modalSaga } from './modal/sagas'; -import { authOpenProfile, gotAuthPostMessage } from './auth/actions'; +import { authLogout, authOpenProfile, gotAuthPostMessage } from './auth/actions'; import boris, { IBorisState } from './boris/reducer'; import borisSaga from './boris/sagas'; @@ -36,6 +36,9 @@ import messagesSaga from './messages/sagas'; import tag, { ITagState } from './tag'; import tagSaga from './tag/sagas'; +import { AxiosError } from 'axios'; +import { api } from '~/utils/api'; +import { assocPath } from 'ramda'; const authPersistConfig: PersistConfig = { key: 'auth', @@ -116,5 +119,28 @@ export function configureStore(): { const persistor = persistStore(store); + // Pass token to axios + api.interceptors.request.use(options => { + const token = store.getState().auth.token; + + if (!token) { + return options; + } + + return assocPath(['headers', 'authorization'], `Bearer ${token}`, options); + }); + + // Logout on 401 + api.interceptors.response.use(undefined, (error: AxiosError<{ message: string }>) => { + if (error.response?.status === 401) { + store.dispatch(authLogout()); + } + + console.log('Вот что случилось на сервере:', error); + throw new Error( + error?.response?.data?.message || error?.message || error?.response?.statusText + ); + }); + return { store, persistor }; } diff --git a/src/redux/tag/sagas.ts b/src/redux/tag/sagas.ts index ade289bd..00bcdf35 100644 --- a/src/redux/tag/sagas.ts +++ b/src/redux/tag/sagas.ts @@ -1,7 +1,12 @@ import { TAG_ACTIONS } from '~/redux/tag/constants'; import { call, delay, put, select, takeLatest } from 'redux-saga/effects'; -import { tagLoadAutocomplete, tagLoadNodes, tagSetAutocomplete, tagSetNodes, } from '~/redux/tag/actions'; -import { reqWrapper } from '~/redux/auth/sagas'; +import { + tagLoadAutocomplete, + tagLoadNodes, + tagSetAutocomplete, + tagSetNodes, +} from '~/redux/tag/actions'; +import { wrap } from '~/redux/auth/sagas'; import { selectTagNodes } from '~/redux/tag/selectors'; import { getTagAutocomplete, getTagNodes } from '~/redux/tag/api'; import { Unwrap } from '~/redux/types'; @@ -11,11 +16,11 @@ function* loadTagNodes({ tag }: ReturnType) { try { const { list }: ReturnType = yield select(selectTagNodes); - const { data, error }: Unwrap> = yield call( - reqWrapper, - getTagNodes, - { tag, limit: 18, offset: list.length } - ); + const { data, error }: Unwrap = yield call(wrap, getTagNodes, { + tag, + limit: 18, + offset: list.length, + }); if (error) throw new Error(error); @@ -33,8 +38,8 @@ function* loadAutocomplete({ search, exclude }: ReturnType> = yield call( - reqWrapper, + const { data, error }: Unwrap = yield call( + wrap, getTagAutocomplete, { search, exclude } ); diff --git a/src/redux/types.ts b/src/redux/types.ts index d38db98c..9155b1ff 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -192,7 +192,13 @@ export type INodeNotification = { export type INotification = IMessageNotification | ICommentNotification; -export type Unwrap = T extends Promise ? U : T; +export type Unwrap = T extends (...args: any) => Promise + ? T extends (...args: any) => Promise + ? U + : T + : T extends () => Iterator + ? U + : any; export interface IEmbed { provider: string; diff --git a/src/redux/uploads/sagas.ts b/src/redux/uploads/sagas.ts index b24ada1c..e832d16f 100644 --- a/src/redux/uploads/sagas.ts +++ b/src/redux/uploads/sagas.ts @@ -8,7 +8,7 @@ import { uploadDropStatus, uploadAddFile, } from './actions'; -import { reqWrapper } from '../auth/sagas'; +import { wrap } from '../auth/sagas'; import { createUploader, uploadGetThumb } from '~/utils/uploader'; import { HTTP_RESPONSES } from '~/utils/api'; import { IFileWithUUID, IFile, IUploadProgressHandler } from '../types'; @@ -20,7 +20,7 @@ function* uploadCall({ type, onProgress, }: IFileWithUUID & { onProgress: IUploadProgressHandler }) { - return yield call(reqWrapper, postUploadFile, { + return yield call(wrap, postUploadFile, { file, temp_id, type, diff --git a/src/utils/api/index.ts b/src/utils/api/index.ts index e7268343..dc68cdee 100644 --- a/src/utils/api/index.ts +++ b/src/utils/api/index.ts @@ -1,4 +1,4 @@ -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; import { push } from 'connected-react-router'; import { API } from '~/constants/api'; import { store } from '~/redux/store'; @@ -50,3 +50,5 @@ export const configWithToken = ( ...config, headers: { ...(config.headers || {}), Authorization: `Bearer ${access}` }, }); + +export const cleanResult = (response: AxiosResponse): T => response?.data; diff --git a/src/utils/validators.ts b/src/utils/validators.ts index 8c50b790..0691546f 100644 --- a/src/utils/validators.ts +++ b/src/utils/validators.ts @@ -3,13 +3,13 @@ import { IMAGE_MIME_TYPES } from '~/utils/uploader'; const isValidEmail = (email: string): boolean => !!email && - String(email) && + !!String(email) && !!String(email).match( /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ ); const isLikeEmail = (email: string): boolean => - !!email && String(email) && !!String(email).match(/^([^\@]+)@([^\@]+)\.([^\@]+)$$/); + !!email && !!String(email) && !!String(email).match(/^([^\@]+)@([^\@]+)\.([^\@]+)$$/); const isNonEmpty = (value: string): boolean => !!value && value.trim().length > 0; const isLikePhone = isNonEmpty; diff --git a/tsconfig.json b/tsconfig.json index d0a4b9ad..6583d3c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext",