1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

auth: refactored sagas to use try-catch

This commit is contained in:
Fedor Katurov 2021-03-02 14:17:27 +07:00
parent 7d2511e7e9
commit c36494c3f9
22 changed files with 400 additions and 424 deletions

View file

@ -44,7 +44,7 @@
"start": "craco start", "start": "craco start",
"build": "craco build", "build": "craco build",
"test": "craco test", "test": "craco test",
"eject": "craco eject" "ts-check": "tsc -p tsconfig.json --noEmit"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View file

@ -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 { API } from '~/constants/api';
import { INotification, IResultWithStatus } from '~/redux/types'; import { IResultWithStatus } from '~/redux/types';
import { userLoginTransform } from '~/redux/auth/transforms'; import {
import { ISocialAccount, IUser } from './types'; 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 = ({ export const apiUserLogin = ({ username, password }: ApiUserLoginRequest) =>
username,
password,
}: {
username: string;
password: string;
}): Promise<IResultWithStatus<{ token: string; status?: number }>> =>
api api
.post(API.USER.LOGIN, { username, password }) .post<ApiUserLoginResult>(API.USER.LOGIN, { username, password })
.then(resultMiddleware) .then(cleanResult);
.catch(errorMiddleware)
.then(userLoginTransform);
export const apiAuthGetUser = ({ access }): Promise<IResultWithStatus<{ user: IUser }>> => export const apiAuthGetUser = () => api.get<ApiAuthGetUserResult>(API.USER.ME).then(cleanResult);
api
.get(API.USER.ME, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiAuthGetUserProfile = ({ export const apiAuthGetUserProfile = ({ username }: ApiAuthGetUserProfileRequest) =>
access, api.get<ApiAuthGetUserProfileResult>(API.USER.PROFILE(username)).then(cleanResult);
username,
}): Promise<IResultWithStatus<{ user: IUser }>> =>
api
.get(API.USER.PROFILE(username), configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiAuthGetUpdates = ({ export const apiAuthGetUpdates = ({ exclude_dialogs, last }: ApiAuthGetUpdatesRequest) =>
access,
exclude_dialogs,
last,
}): Promise<IResultWithStatus<{
notifications: INotification[];
boris: { commented_at: string };
}>> =>
api api
.get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } })) .get<ApiAuthGetUpdatesResult>(API.USER.GET_UPDATES, { params: { exclude_dialogs, last } })
.then(resultMiddleware) .then(cleanResult);
.catch(errorMiddleware);
export const apiUpdateUser = ({ access, user }): Promise<IResultWithStatus<{ user: IUser }>> => export const apiUpdateUser = ({ user }: ApiUpdateUserRequest) =>
api api.patch<ApiUpdateUserResult>(API.USER.ME, user).then(cleanResult);
.patch(API.USER.ME, user, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiRequestRestoreCode = ({ field }): Promise<IResultWithStatus<{}>> => export const apiRequestRestoreCode = ({ field }): Promise<IResultWithStatus<{}>> =>
api api
@ -57,75 +48,26 @@ export const apiRequestRestoreCode = ({ field }): Promise<IResultWithStatus<{}>>
.then(resultMiddleware) .then(resultMiddleware)
.catch(errorMiddleware); .catch(errorMiddleware);
export const apiCheckRestoreCode = ({ code }): Promise<IResultWithStatus<{}>> => export const apiCheckRestoreCode = ({ code }: ApiCheckRestoreCodeRequest) =>
api api.get<ApiCheckRestoreCodeResult>(API.USER.REQUEST_CODE(code)).then(cleanResult);
.get(API.USER.REQUEST_CODE(code))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiRestoreCode = ({ code, password }): Promise<IResultWithStatus<{}>> => export const apiRestoreCode = ({ code, password }: ApiRestoreCodeRequest) =>
api api
.post(API.USER.REQUEST_CODE(code), { password }) .post<ApiRestoreCodeResult>(API.USER.REQUEST_CODE(code), { password })
.then(resultMiddleware) .then(cleanResult);
.catch(errorMiddleware);
export const apiGetSocials = ({ export const apiGetSocials = () =>
access, api.get<ApiGetSocialsResult>(API.USER.GET_SOCIALS).then(cleanResult);
}: {
access: string;
}): Promise<IResultWithStatus<{
accounts: ISocialAccount[];
}>> =>
api
.get(API.USER.GET_SOCIALS, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiDropSocial = ({ export const apiDropSocial = ({ id, provider }: ApiDropSocialRequest) =>
access, api.delete<ApiDropSocialResult>(API.USER.DROP_SOCIAL(provider, id)).then(cleanResult);
id,
provider,
}: {
access: string;
id: string;
provider: string;
}): Promise<IResultWithStatus<{
accounts: ISocialAccount[];
}>> =>
api
.delete(API.USER.DROP_SOCIAL(provider, id), configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiAttachSocial = ({ export const apiAttachSocial = ({ token }: ApiAttachSocialRequest) =>
access,
token,
}: {
access: string;
token: string;
}): Promise<IResultWithStatus<{
account: ISocialAccount;
}>> =>
api api
.post(API.USER.ATTACH_SOCIAL, { token }, configWithToken(access)) .post<ApiAttachSocialResult>(API.USER.ATTACH_SOCIAL, { token })
.then(resultMiddleware) .then(cleanResult);
.catch(errorMiddleware);
export const apiLoginWithSocial = ({ export const apiLoginWithSocial = ({ token, username, password }: ApiLoginWithSocialRequest) =>
token,
username,
password,
}: {
token: string;
username?: string;
password?: string;
}): Promise<IResultWithStatus<{
token: string;
error: string;
errors: Record<string, string>;
needs_register: boolean;
}>> =>
api api
.post(API.USER.LOGIN_WITH_SOCIAL, { token, username, password }) .post<ApiLoginWithSocialResult>(API.USER.LOGIN_WITH_SOCIAL, { token, username, password })
.then(resultMiddleware) .then(cleanResult);
.catch(errorMiddleware);

View file

@ -53,26 +53,26 @@ export const USER_ROLES = {
}; };
export const EMPTY_TOKEN: IToken = { export const EMPTY_TOKEN: IToken = {
access: null, access: '',
refresh: null, refresh: '',
}; };
export const EMPTY_USER: IUser = { export const EMPTY_USER: IUser = {
id: null, id: 0,
role: USER_ROLES.GUEST, role: USER_ROLES.GUEST,
email: null, email: '',
name: null, name: '',
username: null, username: '',
photo: null, photo: undefined,
cover: null, cover: undefined,
is_activated: false, is_activated: false,
is_user: false, is_user: false,
fullname: null, fullname: '',
description: null, description: '',
last_seen: null, last_seen: '',
last_seen_messages: null, last_seen_messages: '',
last_seen_boris: null, last_seen_boris: '',
}; };
export interface IApiUser { export interface IApiUser {

View file

@ -8,17 +8,17 @@ const HANDLERS = {
}; };
const INITIAL_STATE: IAuthState = { const INITIAL_STATE: IAuthState = {
token: null, token: '',
user: { ...EMPTY_USER }, user: { ...EMPTY_USER },
updates: { updates: {
last: null, last: '',
notifications: [], notifications: [],
boris_commented_at: null, boris_commented_at: '',
}, },
login: { login: {
error: null, error: '',
is_loading: false, is_loading: false,
is_registering: true, is_registering: true,
}, },
@ -27,7 +27,7 @@ const INITIAL_STATE: IAuthState = {
tab: 'profile', tab: 'profile',
is_loading: true, is_loading: true,
user: null, user: undefined,
patch_errors: {}, patch_errors: {},
socials: { socials: {
@ -39,20 +39,19 @@ const INITIAL_STATE: IAuthState = {
restore: { restore: {
code: '', code: '',
user: null, user: undefined,
is_loading: false, is_loading: false,
is_succesfull: false, is_succesfull: false,
error: null, error: '',
}, },
register_social: { register_social: {
errors: { errors: {
username: 'and this', username: '',
password: 'dislike this', password: '',
}, },
error: 'dont like this one', error: '',
token: token: '',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJEYXRhIjp7IlByb3ZpZGVyIjoiZ29vZ2xlIiwiSWQiOiJma2F0dXJvdkBpY2Vyb2NrZGV2LmNvbSIsIkVtYWlsIjoiZmthdHVyb3ZAaWNlcm9ja2Rldi5jb20iLCJUb2tlbiI6InlhMjkuYTBBZkg2U01EeXFGdlRaTExXckhsQm1QdGZIOFNIVGQteWlSYTFKSXNmVXluY2F6MTZ5UGhjRmxydTlDMWFtTEg0aHlHRzNIRkhrVGU0SXFUS09hVVBEREdqR2JQRVFJbGpPME9UbUp2T2RrdEtWNDVoUGpJcTB1cHVLc003UWJLSm1oRWhkMEFVa3YyejVHWlNSMjhaM2VOZVdwTEVYSGV0MW1yNyIsIkZldGNoZWQiOnsiUHJvdmlkZXIiOiJnb29nbGUiLCJJZCI6OTIyMzM3MjAzNjg1NDc3NTgwNywiTmFtZSI6IkZlZG9yIEthdHVyb3YiLCJQaG90byI6Imh0dHBzOi8vbGg2Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8ta1VMYXh0VV9jZTAvQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQU1adXVjbkEycTFReU1WLUN0RUtBclRhQzgydE52NTM2QS9waG90by5qcGcifX0sIlR5cGUiOiJvYXV0aF9jbGFpbSJ9.r1MY994BC_g4qRDoDoyNmwLs0qRzBLx6_Ez-3mHQtwg',
is_loading: false, is_loading: false,
}, },
}; };

View file

@ -1,5 +1,5 @@
import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; 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 { import {
authAttachSocial, authAttachSocial,
authDropSocial, authDropSocial,
@ -48,49 +48,37 @@ import {
selectAuthRestore, selectAuthRestore,
selectAuthUpdates, selectAuthUpdates,
selectAuthUser, selectAuthUser,
selectToken,
} from './selectors'; } from './selectors';
import { IResultWithStatus, OAUTH_EVENT_TYPES, Unwrap } from '../types'; import { OAUTH_EVENT_TYPES, Unwrap } from '../types';
import { IAuthState, IUser } from './types'; import { IAuthState } from './types';
import { REHYDRATE, RehydrateAction } from 'redux-persist'; import { REHYDRATE, RehydrateAction } from 'redux-persist';
import { selectModal } from '~/redux/modal/selectors'; import { selectModal } from '~/redux/modal/selectors';
import { IModalState } from '~/redux/modal'; import { IModalState } from '~/redux/modal';
import { DIALOGS } from '~/redux/modal/constants'; import { DIALOGS } from '~/redux/modal/constants';
import { ERRORS } from '~/constants/errors'; import { ERRORS } from '~/constants/errors';
import { messagesSet } from '~/redux/messages/actions'; import { messagesSet } from '~/redux/messages/actions';
import { SagaIterator } from 'redux-saga';
export function* reqWrapper(requestAction, props = {}): ReturnType<typeof requestAction> { function* setTokenSaga({ token }: ReturnType<typeof authSetToken>) {
const access = yield select(selectToken); localStorage.setItem('token', token);
const result = yield call(requestAction, { access, ...props });
if (result && result.status === 401) {
return { error: USER_ERRORS.UNAUTHORIZED, data: {} };
}
return result;
} }
function* sendLoginRequestSaga({ username, password }: ReturnType<typeof userSendLoginRequest>) { function* sendLoginRequestSaga({ username, password }: ReturnType<typeof userSendLoginRequest>) {
if (!username || !password) return; if (!username || !password) return;
const { try {
error, const { token, user }: Unwrap<typeof apiUserLogin> = yield call(apiUserLogin, {
data: { token, user },
}: IResultWithStatus<{ token: string; user: IUser }> = yield call(apiUserLogin, {
username, username,
password, password,
}); });
if (error) {
yield put(userSetLoginError(error));
return;
}
yield put(authSetToken(token)); yield put(authSetToken(token));
yield put(authSetUser({ ...user, is_user: true })); yield put(authSetUser({ ...user, is_user: true }));
yield put(authLoggedIn()); yield put(authLoggedIn());
yield put(modalSetShown(false)); yield put(modalSetShown(false));
} catch (e) {
yield put(userSetLoginError(e));
}
} }
function* refreshUser() { function* refreshUser() {
@ -98,23 +86,18 @@ function* refreshUser() {
if (!token) return; if (!token) return;
const { try {
error, const { user }: Unwrap<typeof apiAuthGetUser> = yield call(apiAuthGetUser);
data: { user },
}: IResultWithStatus<{ user: IUser }> = yield call(reqWrapper, apiAuthGetUser);
if (error) { yield put(authSetUser({ ...user, is_user: true }));
} catch (e) {
yield put( yield put(
authSetUser({ authSetUser({
...EMPTY_USER, ...EMPTY_USER,
is_user: false, is_user: false,
}) })
); );
return;
} }
yield put(authSetUser({ ...user, is_user: true }));
} }
function* checkUserSaga({ key }: RehydrateAction) { function* checkUserSaga({ key }: RehydrateAction) {
@ -126,44 +109,43 @@ function* gotPostMessageSaga({ token }: ReturnType<typeof gotAuthPostMessage>) {
yield put(authSetToken(token)); yield put(authSetToken(token));
yield call(refreshUser); yield call(refreshUser);
const { is_shown, dialog }: IModalState = yield select(selectModal); const { is_shown, dialog }: ReturnType<typeof selectModal> = yield select(selectModal);
if (is_shown && dialog === DIALOGS.LOGIN) yield put(modalSetShown(false)); if (is_shown && dialog === DIALOGS.LOGIN) yield put(modalSetShown(false));
} }
function* logoutSaga() { function* logoutSaga() {
yield put(authSetToken(null)); yield put(authSetToken(''));
yield put(authSetUser({ ...EMPTY_USER })); yield put(authSetUser({ ...EMPTY_USER }));
yield put( yield put(
authSetUpdates({ authSetUpdates({
last: null, last: '',
notifications: [], notifications: [],
}) })
); );
} }
function* loadProfile({ username }: ReturnType<typeof authLoadProfile>) { function* loadProfile({ username }: ReturnType<typeof authLoadProfile>): SagaIterator<boolean> {
yield put(authSetProfile({ is_loading: true })); yield put(authSetProfile({ is_loading: true }));
const { try {
error, const { user }: Unwrap<typeof apiAuthGetUserProfile> = yield call(apiAuthGetUserProfile, {
data: { user }, username,
} = yield call(reqWrapper, apiAuthGetUserProfile, { username }); });
if (error || !user) {
return false;
}
yield put(authSetProfile({ is_loading: false, user })); yield put(authSetProfile({ is_loading: false, user }));
yield put(messagesSet({ messages: [] })); yield put(messagesSet({ messages: [] }));
return true; return true;
} catch (error) {
return false;
}
} }
function* openProfile({ username, tab = 'profile' }: ReturnType<typeof authOpenProfile>) { function* openProfile({ username, tab = 'profile' }: ReturnType<typeof authOpenProfile>) {
yield put(modalShowDialog(DIALOGS.PROFILE)); yield put(modalShowDialog(DIALOGS.PROFILE));
yield put(authSetProfile({ tab })); yield put(authSetProfile({ tab }));
const success: boolean = yield call(loadProfile, authLoadProfile(username)); const success: Unwrap<typeof loadProfile> = yield call(loadProfile, authLoadProfile(username));
if (!success) { if (!success) {
return yield put(modalSetShown(false)); return yield put(modalSetShown(false));
@ -171,25 +153,21 @@ function* openProfile({ username, tab = 'profile' }: ReturnType<typeof authOpenP
} }
function* getUpdates() { function* getUpdates() {
try {
const user: ReturnType<typeof selectAuthUser> = yield select(selectAuthUser); const user: ReturnType<typeof selectAuthUser> = 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 modal: ReturnType<typeof selectModal> = yield select(selectModal);
const profile: IAuthState['profile'] = yield select(selectAuthProfile); const profile: ReturnType<typeof selectAuthProfile> = yield select(selectAuthProfile);
const { last, boris_commented_at }: IAuthState['updates'] = yield select(selectAuthUpdates); const { last, boris_commented_at }: IAuthState['updates'] = yield select(selectAuthUpdates);
const exclude_dialogs = const exclude_dialogs =
modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user.id ? profile.user.id : null; modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user?.id ? profile.user.id : 0;
const { error, data }: Unwrap<ReturnType<typeof apiAuthGetUpdates>> = yield call( const data: Unwrap<typeof apiAuthGetUpdates> = yield call(apiAuthGetUpdates, {
reqWrapper, exclude_dialogs,
apiAuthGetUpdates, last: last || user.last_seen_messages,
{ exclude_dialogs, last: last || user.last_seen_messages } });
);
if (error || !data) {
return;
}
if (data.notifications && data.notifications.length) { if (data.notifications && data.notifications.length) {
yield put( yield put(
@ -207,6 +185,7 @@ function* getUpdates() {
}) })
); );
} }
} catch (error) {}
} }
function* startPollingSaga() { function* startPollingSaga() {
@ -219,57 +198,64 @@ function* startPollingSaga() {
function* setLastSeenMessages({ last_seen_messages }: ReturnType<typeof authSetLastSeenMessages>) { function* setLastSeenMessages({ last_seen_messages }: ReturnType<typeof authSetLastSeenMessages>) {
if (!Date.parse(last_seen_messages)) return; 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<typeof authPatchUser>) { function* patchUser(payload: ReturnType<typeof authPatchUser>) {
const me = yield select(selectAuthUser); const me: ReturnType<typeof selectAuthUser> = yield select(selectAuthUser);
const { error, data } = yield call(reqWrapper, apiUpdateUser, { user }); try {
const { user, errors }: Unwrap<typeof apiUpdateUser> = yield call(apiUpdateUser, {
user: payload.user,
});
if (error || !data.user || data.errors) { if (errors && Object.keys(errors).length) {
return yield put(authSetProfile({ patch_errors: data.errors })); yield put(authSetProfile({ patch_errors: errors }));
return;
} }
yield put(authSetUser({ ...me, ...data.user })); yield put(authSetUser({ ...me, ...user }));
yield put(authSetProfile({ user: { ...me, ...data.user }, tab: 'profile' })); yield put(authSetProfile({ user: { ...me, ...user }, tab: 'profile' }));
} catch (error) {
return;
}
} }
function* requestRestoreCode({ field }: ReturnType<typeof authRequestRestoreCode>) { function* requestRestoreCode({ field }: ReturnType<typeof authRequestRestoreCode>) {
if (!field) return; if (!field) return;
yield put(authSetRestore({ error: null, is_loading: true })); try {
const { error, data } = yield call(apiRequestRestoreCode, { field }); yield put(authSetRestore({ error: '', is_loading: true }));
yield call(apiRequestRestoreCode, {
if (data.error || error) { field,
return yield put(authSetRestore({ is_loading: false, error: data.error || error })); });
}
yield put(authSetRestore({ is_loading: false, is_succesfull: true })); yield put(authSetRestore({ is_loading: false, is_succesfull: true }));
} catch (error) {
return yield put(authSetRestore({ is_loading: false, error }));
}
} }
function* showRestoreModal({ code }: ReturnType<typeof authShowRestoreModal>) { function* showRestoreModal({ code }: ReturnType<typeof authShowRestoreModal>) {
try {
if (!code && !code.length) { if (!code && !code.length) {
return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false }));
} }
yield put(authSetRestore({ user: null, is_loading: true })); yield put(authSetRestore({ user: undefined, is_loading: true }));
const { error, data } = yield call(apiCheckRestoreCode, { code }); const data: Unwrap<typeof apiCheckRestoreCode> = 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(authSetRestore({ user: data.user, code, is_loading: false }));
yield put(modalShowDialog(DIALOGS.RESTORE_PASSWORD)); 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));
}
} }
function* restorePassword({ password }: ReturnType<typeof authRestorePassword>) { function* restorePassword({ password }: ReturnType<typeof authRestorePassword>) {
try {
if (!password) return; if (!password) return;
yield put(authSetRestore({ is_loading: true })); yield put(authSetRestore({ is_loading: true }));
@ -279,88 +265,69 @@ function* restorePassword({ password }: ReturnType<typeof authRestorePassword>)
return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false }));
} }
const { error, data } = yield call(apiRestoreCode, { code, password }); const data: Unwrap<typeof apiRestoreCode> = 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(authSetToken(data.token));
yield put(authSetUser(data.user)); yield put(authSetUser(data.user));
yield put(authSetRestore({ is_loading: false, is_succesfull: true, error: null })); yield put(authSetRestore({ is_loading: false, is_succesfull: true, error: '' }));
yield call(refreshUser); yield call(refreshUser);
} catch (error) {
return yield put(authSetRestore({ is_loading: false, error: error || ERRORS.CODE_IS_INVALID }));
}
} }
function* getSocials() { function* getSocials() {
yield put(authSetSocials({ is_loading: true, error: '' }));
try { try {
const { data, error }: Unwrap<ReturnType<typeof apiGetSocials>> = yield call( yield put(authSetSocials({ is_loading: true, error: '' }));
reqWrapper, const data: Unwrap<typeof apiGetSocials> = yield call(apiGetSocials);
apiGetSocials, yield put(authSetSocials({ accounts: data.accounts }));
{} } catch (error) {
); yield put(authSetSocials({ error }));
} finally {
if (error) { yield put(authSetSocials({ is_loading: false }));
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() }));
} }
} }
// TODO: start from here
function* dropSocial({ provider, id }: ReturnType<typeof authDropSocial>) { function* dropSocial({ provider, id }: ReturnType<typeof authDropSocial>) {
try { try {
yield put(authSetSocials({ error: '' })); yield put(authSetSocials({ error: '' }));
const { error }: Unwrap<ReturnType<typeof apiDropSocial>> = yield call( yield call(apiDropSocial, {
reqWrapper, id,
apiDropSocial, provider,
{ id, provider } });
);
if (error) {
throw new Error(error);
}
yield call(getSocials); yield call(getSocials);
} catch (e) { } catch (error) {
yield put(authSetSocials({ error: e.message })); yield put(authSetSocials({ error }));
} }
} }
function* attachSocial({ token }: ReturnType<typeof authAttachSocial>) { function* attachSocial({ token }: ReturnType<typeof authAttachSocial>) {
try {
if (!token) return; if (!token) return;
try {
yield put(authSetSocials({ error: '', is_loading: true })); yield put(authSetSocials({ error: '', is_loading: true }));
const { data, error }: Unwrap<ReturnType<typeof apiAttachSocial>> = yield call( const data: Unwrap<typeof apiAttachSocial> = yield call(apiAttachSocial, {
reqWrapper, token,
apiAttachSocial, });
{ token }
);
if (error) {
throw new Error(error);
}
const { const {
socials: { accounts }, socials: { accounts },
}: ReturnType<typeof selectAuthProfile> = yield select(selectAuthProfile); }: ReturnType<typeof selectAuthProfile> = yield select(selectAuthProfile);
if (accounts.some(it => it.id === data.account.id && it.provider === data.account.provider)) { if (accounts.some(it => it.id === data.account.id && it.provider === data.account.provider)) {
yield put(authSetSocials({ is_loading: false })); return;
} else {
yield put(authSetSocials({ is_loading: false, accounts: [...accounts, data.account] }));
} }
yield put(authSetSocials({ accounts: [...accounts, data.account] }));
} catch (e) { } 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<typeof authLoginWithSocial>) {
try { try {
yield put(userSetLoginError('')); yield put(userSetLoginError(''));
const { const data: Unwrap<typeof apiLoginWithSocial> = yield call(apiLoginWithSocial, {
data, token,
error, });
}: Unwrap<ReturnType<typeof apiLoginWithSocial>> = yield call(apiLoginWithSocial, { token });
// Backend asks us for account registration // Backend asks us for account registration
if (data?.needs_register) { if (data?.needs_register) {
@ -380,18 +346,14 @@ function* loginWithSocial({ token }: ReturnType<typeof authLoginWithSocial>) {
return; return;
} }
if (error) {
throw new Error(error);
}
if (data.token) { if (data.token) {
yield put(authSetToken(data.token)); yield put(authSetToken(data.token));
yield call(refreshUser); yield call(refreshUser);
yield put(modalSetShown(false)); yield put(modalSetShown(false));
return; return;
} }
} catch (e) { } catch (error) {
yield put(userSetLoginError(e.message)); yield put(userSetLoginError(error));
} }
} }
@ -414,23 +376,19 @@ function* authRegisterSocial({ username, password }: ReturnType<typeof authSendR
try { try {
yield put(authSetRegisterSocial({ error: '' })); yield put(authSetRegisterSocial({ error: '' }));
const { token }: Unwrap<ReturnType<typeof selectAuthRegisterSocial>> = yield select( const { token }: Unwrap<typeof selectAuthRegisterSocial> = yield select(
selectAuthRegisterSocial selectAuthRegisterSocial
); );
const { data, error }: Unwrap<ReturnType<typeof apiLoginWithSocial>> = yield call( const data: Unwrap<typeof apiLoginWithSocial> = yield call(apiLoginWithSocial, {
apiLoginWithSocial,
{
token, token,
username, username,
password, password,
} });
);
if (data?.errors) { if (data?.errors) {
yield put(authSetRegisterSocialErrors(data.errors)); yield put(authSetRegisterSocialErrors(data.errors));
} else if (data?.error) { return;
throw new Error(error);
} }
if (data.token) { if (data.token) {
@ -449,6 +407,7 @@ function* authSaga() {
yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga); yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga);
yield takeLatest(AUTH_USER_ACTIONS.LOGOUT, logoutSaga); 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.SEND_LOGIN_REQUEST, sendLoginRequestSaga);
yield takeLatest(AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE, gotPostMessageSaga); yield takeLatest(AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE, gotPostMessageSaga);
yield takeLatest(AUTH_USER_ACTIONS.OPEN_PROFILE, openProfile); yield takeLatest(AUTH_USER_ACTIONS.OPEN_PROFILE, openProfile);

View file

@ -5,7 +5,7 @@ export const selectUser = (state: IState) => state.auth.user;
export const selectToken = (state: IState) => state.auth.token; export const selectToken = (state: IState) => state.auth.token;
export const selectAuthLogin = (state: IState) => state.auth.login; export const selectAuthLogin = (state: IState) => state.auth.login;
export const selectAuthProfile = (state: IState) => state.auth.profile; 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 selectAuthUser = (state: IState) => state.auth.user;
export const selectAuthUpdates = (state: IState) => state.auth.updates; export const selectAuthUpdates = (state: IState) => state.auth.updates;
export const selectAuthRestore = (state: IState) => state.auth.restore; export const selectAuthRestore = (state: IState) => state.auth.restore;

View file

@ -1,13 +1,18 @@
import { IResultWithStatus } from '~/redux/types'; import { IResultWithStatus } from '~/redux/types';
import { HTTP_RESPONSES } from '~/utils/api'; import { HTTP_RESPONSES } from '~/utils/api';
export const userLoginTransform = ({ status, data, error }: IResultWithStatus<any>): IResultWithStatus<any> => { export const userLoginTransform = ({
status,
data,
error,
}: IResultWithStatus<any>): IResultWithStatus<any> => {
switch (true) { 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: 'Пользователь не найден' }; return { status, data, error: 'Пользователь не найден' };
case status === 200: case status === 200:
return { status, data, error: null }; return { status, data, error: '' };
default: default:
return { status, data, error: error || 'Неизвестная ошибка' }; return { status, data, error: error || 'Неизвестная ошибка' };

View file

@ -1,4 +1,4 @@
import { IFile, INotification } from '../types'; import { IFile, INotification, IResultWithStatus } from '../types';
export interface IToken { export interface IToken {
access: string; access: string;
@ -10,8 +10,8 @@ export interface IUser {
username: string; username: string;
email: string; email: string;
role: string; role: string;
photo: IFile; photo?: IFile;
cover: IFile; cover?: IFile;
name: string; name: string;
fullname: string; fullname: string;
description: string; description: string;
@ -53,7 +53,7 @@ export type IAuthState = Readonly<{
tab: 'profile' | 'messages' | 'settings'; tab: 'profile' | 'messages' | 'settings';
is_loading: boolean; is_loading: boolean;
user: IUser; user?: IUser;
patch_errors: Record<string, string>; patch_errors: Record<string, string>;
socials: { socials: {
@ -65,7 +65,7 @@ export type IAuthState = Readonly<{
restore: { restore: {
code: string; code: string;
user: Pick<IUser, 'username' | 'photo'>; user?: Pick<IUser, 'username' | 'photo'>;
is_loading: boolean; is_loading: boolean;
is_succesfull: boolean; is_succesfull: boolean;
error: string; error: string;
@ -81,3 +81,52 @@ export type IAuthState = Readonly<{
is_loading: boolean; 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<IUser> };
export type ApiUpdateUserResult = { user: IUser; errors: Record<Partial<keyof IUser>, 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<string, string>;
needs_register: boolean;
};

View file

@ -8,10 +8,8 @@ function* loadStats() {
yield put(borisSetStats({ is_loading: true })); yield put(borisSetStats({ is_loading: true }));
try { try {
const git: Unwrap<ReturnType<typeof getBorisGitStats>> = yield call(getBorisGitStats); const git: Unwrap<typeof getBorisGitStats> = yield call(getBorisGitStats);
const backend: Unwrap<ReturnType<typeof getBorisBackendStats>> = yield call( const backend: Unwrap<typeof getBorisBackendStats> = yield call(getBorisBackendStats);
getBorisBackendStats
);
yield put(borisSetStats({ git, backend: backend.data, is_loading: false })); yield put(borisSetStats({ git, backend: backend.data, is_loading: false }));
} catch (e) { } catch (e) {

View file

@ -14,7 +14,7 @@ import {
} from './actions'; } from './actions';
import { IResultWithStatus, INode, Unwrap } from '../types'; import { IResultWithStatus, INode, Unwrap } from '../types';
import { selectFlowNodes, selectFlow } from './selectors'; import { selectFlowNodes, selectFlow } from './selectors';
import { reqWrapper } from '../auth/sagas'; import { wrap } from '../auth/sagas';
import { postCellView, getSearchResults } from './api'; import { postCellView, getSearchResults } from './api';
import { IFlowState } from './reducer'; import { IFlowState } from './reducer';
import { uniq } from 'ramda'; import { uniq } from 'ramda';
@ -47,7 +47,7 @@ function* onGetFlow() {
recent: IFlowState['recent']; recent: IFlowState['recent'];
updated: IFlowState['updated']; updated: IFlowState['updated'];
valid: INode['id'][]; valid: INode['id'][];
}> = yield call(reqWrapper, getNodeDiff, { }> = yield call(wrap, getNodeDiff, {
start: new Date().toISOString(), start: new Date().toISOString(),
end: new Date().toISOString(), end: new Date().toISOString(),
with_heroes: true, with_heroes: true,
@ -71,7 +71,7 @@ function* onSetCellView({ id, flow }: ReturnType<typeof flowSetCellView>) {
const nodes = yield select(selectFlowNodes); const nodes = yield select(selectFlowNodes);
yield put(flowSetNodes(nodes.map(node => (node.id === id ? { ...node, flow } : node)))); 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 // TODO: error handling
} }
@ -83,7 +83,7 @@ function* getMore() {
const start = nodes && nodes[0] && nodes[0].created_at; const start = nodes && nodes[0] && nodes[0].created_at;
const end = nodes && nodes[nodes.length - 1] && nodes[nodes.length - 1].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, start,
end, end,
with_heroes: false, with_heroes: false,
@ -124,13 +124,9 @@ function* changeSearch({ search }: ReturnType<typeof flowChangeSearch>) {
yield delay(500); yield delay(500);
const { data, error }: Unwrap<ReturnType<typeof getSearchResults>> = yield call( const { data, error }: Unwrap<typeof getSearchResults> = yield call(wrap, getSearchResults, {
reqWrapper,
getSearchResults,
{
...search, ...search,
} });
);
if (error) { if (error) {
yield put(flowSetSearch({ is_loading: false, results: [], total: 0 })); yield put(flowSetSearch({ is_loading: false, results: [], total: 0 }));
@ -155,11 +151,8 @@ function* loadMoreSearch() {
const { search }: ReturnType<typeof selectFlow> = yield select(selectFlow); const { search }: ReturnType<typeof selectFlow> = yield select(selectFlow);
const { const { result, delay }: { result: Unwrap<typeof getSearchResults>; delay: any } = yield race({
result, result: call(wrap, getSearchResults, {
delay,
}: { result: Unwrap<ReturnType<typeof getSearchResults>>; delay: any } = yield race({
result: call(reqWrapper, getSearchResults, {
...search, ...search,
skip: search.results.length, skip: search.results.length,
}), }),

View file

@ -12,7 +12,7 @@ export interface IMessagesState {
const INITIAL_STATE: IMessagesState = { const INITIAL_STATE: IMessagesState = {
is_loading_messages: true, is_loading_messages: true,
is_sending_messages: false, is_sending_messages: false,
error: null, error: '',
messages: [], messages: [],
}; };

View file

@ -12,7 +12,7 @@ import {
} from '~/redux/messages/api'; } from '~/redux/messages/api';
import { ERRORS } from '~/constants/errors'; import { ERRORS } from '~/constants/errors';
import { IMessageNotification, Unwrap } from '~/redux/types'; import { IMessageNotification, Unwrap } from '~/redux/types';
import { reqWrapper } from '~/redux/auth/sagas'; import { wrap } from '~/redux/auth/sagas';
import { import {
messagesDeleteMessage, messagesDeleteMessage,
messagesGetMessages, messagesGetMessages,
@ -39,11 +39,8 @@ function* getMessages({ username }: ReturnType<typeof messagesGetMessages>) {
}) })
); );
const { const { error, data }: Unwrap<typeof apiMessagesGetUserMessages> = yield call(
error, wrap,
data,
}: Unwrap<ReturnType<typeof apiMessagesGetUserMessages>> = yield call(
reqWrapper,
apiMessagesGetUserMessages, apiMessagesGetUserMessages,
{ username } { username }
); );
@ -82,8 +79,8 @@ function* sendMessage({ message, onSuccess }: ReturnType<typeof messagesSendMess
yield put(messagesSet({ is_sending_messages: true, error: null })); yield put(messagesSet({ is_sending_messages: true, error: null }));
const { error, data }: Unwrap<ReturnType<typeof apiMessagesSendMessage>> = yield call( const { error, data }: Unwrap<typeof apiMessagesSendMessage> = yield call(
reqWrapper, wrap,
apiMessagesSendMessage, apiMessagesSendMessage,
{ {
username, username,
@ -138,8 +135,8 @@ function* deleteMessage({ id, is_locked }: ReturnType<typeof messagesDeleteMessa
yield put(messagesSet({ is_sending_messages: true, error: null })); yield put(messagesSet({ is_sending_messages: true, error: null }));
const { error, data }: Unwrap<ReturnType<typeof apiMessagesDeleteMessage>> = yield call( const { error, data }: Unwrap<typeof apiMessagesDeleteMessage> = yield call(
reqWrapper, wrap,
apiMessagesDeleteMessage, apiMessagesDeleteMessage,
{ {
username, username,
@ -187,11 +184,8 @@ function* refreshMessages({}: ReturnType<typeof messagesRefreshMessages>) {
const after = messages.length > 0 ? messages[0].created_at : undefined; const after = messages.length > 0 ? messages[0].created_at : undefined;
const { const { data, error }: Unwrap<typeof apiMessagesGetUserMessages> = yield call(
data, wrap,
error,
}: Unwrap<ReturnType<typeof apiMessagesGetUserMessages>> = yield call(
reqWrapper,
apiMessagesGetUserMessages, apiMessagesGetUserMessages,
{ username, after } { username, after }
); );

View file

@ -14,7 +14,7 @@ export interface IModalState {
const INITIAL_STATE: IModalState = { const INITIAL_STATE: IModalState = {
is_shown: false, is_shown: false,
dialog: null, dialog: '',
photoswipe: { photoswipe: {
images: [], images: [],
index: 0, index: 0,

View file

@ -2,7 +2,13 @@ import { all, call, delay, put, select, takeLatest, takeLeading } from 'redux-sa
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import { omit } from 'ramda'; 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 { import {
nodeCancelCommentEdit, nodeCancelCommentEdit,
nodeCreate, nodeCreate,
@ -39,7 +45,7 @@ import {
postNodeStar, postNodeStar,
updateNodeTags, updateNodeTags,
} from './api'; } from './api';
import { reqWrapper } from '../auth/sagas'; import { wrap } from '../auth/sagas';
import { flowSetNodes, flowSetUpdated } from '../flow/actions'; import { flowSetNodes, flowSetUpdated } from '../flow/actions';
import { ERRORS } from '~/constants/errors'; import { ERRORS } from '~/constants/errors';
import { modalSetShown, modalShowDialog } from '../modal/actions'; import { modalSetShown, modalShowDialog } from '../modal/actions';
@ -77,7 +83,7 @@ function* onNodeSave({ node }: ReturnType<typeof nodeSave>) {
const { const {
error, error,
data: { errors, node: result }, data: { errors, node: result },
} = yield call(reqWrapper, postNode, { node }); } = yield call(wrap, postNode, { node });
if (errors && Object.values(errors).length > 0) { if (errors && Object.values(errors).length > 0) {
return yield put(nodeSetSaveErrors(errors)); return yield put(nodeSetSaveErrors(errors));
@ -117,15 +123,11 @@ function* onNodeLoadMoreComments() {
comments, comments,
}: ReturnType<typeof selectNode> = yield select(selectNode); }: ReturnType<typeof selectNode> = yield select(selectNode);
const { data, error }: Unwrap<ReturnType<typeof getNodeComments>> = yield call( const { data, error }: Unwrap<typeof getNodeComments> = yield call(wrap, getNodeComments, {
reqWrapper,
getNodeComments,
{
id, id,
take: COMMENTS_DISPLAY, take: COMMENTS_DISPLAY,
skip: comments.length, skip: comments.length,
} });
);
const current: ReturnType<typeof selectNode> = yield select(selectNode); const current: ReturnType<typeof selectNode> = yield select(selectNode);
@ -147,7 +149,7 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType<typeof nodeLoadNode>) {
const { const {
data: { node, error }, data: { node, error },
} = yield call(reqWrapper, getNode, { id }); } = yield call(wrap, getNode, { id });
if (error || !node || !node.id) { if (error || !node || !node.id) {
yield put(push(URLS.ERRORS.NOT_FOUND)); yield put(push(URLS.ERRORS.NOT_FOUND));
@ -166,8 +168,8 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType<typeof nodeLoadNode>) {
data: { related }, data: { related },
}, },
} = yield all({ } = yield all({
comments: call(reqWrapper, getNodeComments, { id, take: COMMENTS_DISPLAY, skip: 0 }), comments: call(wrap, getNodeComments, { id, take: COMMENTS_DISPLAY, skip: 0 }),
related: call(reqWrapper, getNodeRelated, { id }), related: call(wrap, getNodeRelated, { id }),
}); });
yield put( yield put(
@ -190,14 +192,10 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType<typeof nodeLoadNode>) {
} }
function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) { function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
const { data, error }: Unwrap<ReturnType<typeof postNodeComment>> = yield call( const { data, error }: Unwrap<typeof postNodeComment> = yield call(wrap, postNodeComment, {
reqWrapper,
postNodeComment,
{
data: comment, data: comment,
id: nodeId, id: nodeId,
} });
);
if (error || !data.comment) { if (error || !data.comment) {
return callback(error); return callback(error);
@ -237,7 +235,7 @@ function* onUpdateTags({ id, tags }: ReturnType<typeof nodeUpdateTags>) {
const { const {
data: { node }, 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); const { current } = yield select(selectNode);
@ -259,7 +257,7 @@ function* onEditSaga({ id }: ReturnType<typeof nodeEdit>) {
const { const {
data: { node }, data: { node },
error, error,
} = yield call(reqWrapper, getNode, { id }); } = yield call(wrap, getNode, { id });
if (error || !node || !node.type || !NODE_EDITOR_DIALOGS[node.type]) if (error || !node || !node.type || !NODE_EDITOR_DIALOGS[node.type])
return yield put(modalSetShown(false)); return yield put(modalSetShown(false));
@ -282,7 +280,7 @@ function* onLikeSaga({ id }: ReturnType<typeof nodeLike>) {
like_count: is_liked ? Math.max(like_count - 1, 0) : like_count + 1, 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 if (!error || data.is_liked === !is_liked) return; // ok and matches
@ -297,7 +295,7 @@ function* onStarSaga({ id }: ReturnType<typeof nodeLike>) {
yield call(updateNodeEverywhere, { ...current, is_heroic: !is_heroic }); 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 if (!error || data.is_heroic === !is_heroic) return; // ok and matches
@ -315,7 +313,7 @@ function* onLockSaga({ id, is_locked }: ReturnType<typeof nodeLock>) {
deleted_at: is_locked ? new Date().toISOString() : null, 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 }); if (error) return yield call(updateNodeEverywhere, { ...current, deleted_at });
} }
@ -333,7 +331,7 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType<typeof nodeLockComment
) )
); );
yield call(reqWrapper, postNodeLockComment, { current: current.id, id, is_locked }); yield call(wrap, postNodeLockComment, { current: current.id, id, is_locked });
} }
function* onEditCommentSaga({ id }: ReturnType<typeof nodeEditComment>) { function* onEditCommentSaga({ id }: ReturnType<typeof nodeEditComment>) {

View file

@ -57,7 +57,7 @@ function* getYoutubeInfo() {
} }
if (ticker || ids.length > 25) { if (ticker || ids.length > 25) {
const result: Unwrap<ReturnType<typeof getEmbedYoutube>> = yield call(getEmbedYoutube, ids); const result: Unwrap<typeof getEmbedYoutube> = yield call(getEmbedYoutube, ids);
if (!result.error && result.data.items && Object.keys(result.data.items).length) { if (!result.error && result.data.items && Object.keys(result.data.items).length) {
const { youtubes }: ReturnType<typeof selectPlayer> = yield select(selectPlayer); const { youtubes }: ReturnType<typeof selectPlayer> = yield select(selectPlayer);

View file

@ -26,7 +26,7 @@ import playerSaga from '~/redux/player/sagas';
import modal, { IModalState } from '~/redux/modal'; import modal, { IModalState } from '~/redux/modal';
import { modalSaga } from './modal/sagas'; 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 boris, { IBorisState } from './boris/reducer';
import borisSaga from './boris/sagas'; import borisSaga from './boris/sagas';
@ -36,6 +36,9 @@ import messagesSaga from './messages/sagas';
import tag, { ITagState } from './tag'; import tag, { ITagState } from './tag';
import tagSaga from './tag/sagas'; import tagSaga from './tag/sagas';
import { AxiosError } from 'axios';
import { api } from '~/utils/api';
import { assocPath } from 'ramda';
const authPersistConfig: PersistConfig = { const authPersistConfig: PersistConfig = {
key: 'auth', key: 'auth',
@ -116,5 +119,28 @@ export function configureStore(): {
const persistor = persistStore(store); 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 }; return { store, persistor };
} }

View file

@ -1,7 +1,12 @@
import { TAG_ACTIONS } from '~/redux/tag/constants'; import { TAG_ACTIONS } from '~/redux/tag/constants';
import { call, delay, put, select, takeLatest } from 'redux-saga/effects'; import { call, delay, put, select, takeLatest } from 'redux-saga/effects';
import { tagLoadAutocomplete, tagLoadNodes, tagSetAutocomplete, tagSetNodes, } from '~/redux/tag/actions'; import {
import { reqWrapper } from '~/redux/auth/sagas'; tagLoadAutocomplete,
tagLoadNodes,
tagSetAutocomplete,
tagSetNodes,
} from '~/redux/tag/actions';
import { wrap } from '~/redux/auth/sagas';
import { selectTagNodes } from '~/redux/tag/selectors'; import { selectTagNodes } from '~/redux/tag/selectors';
import { getTagAutocomplete, getTagNodes } from '~/redux/tag/api'; import { getTagAutocomplete, getTagNodes } from '~/redux/tag/api';
import { Unwrap } from '~/redux/types'; import { Unwrap } from '~/redux/types';
@ -11,11 +16,11 @@ function* loadTagNodes({ tag }: ReturnType<typeof tagLoadNodes>) {
try { try {
const { list }: ReturnType<typeof selectTagNodes> = yield select(selectTagNodes); const { list }: ReturnType<typeof selectTagNodes> = yield select(selectTagNodes);
const { data, error }: Unwrap<ReturnType<typeof getTagNodes>> = yield call( const { data, error }: Unwrap<typeof getTagNodes> = yield call(wrap, getTagNodes, {
reqWrapper, tag,
getTagNodes, limit: 18,
{ tag, limit: 18, offset: list.length } offset: list.length,
); });
if (error) throw new Error(error); if (error) throw new Error(error);
@ -33,8 +38,8 @@ function* loadAutocomplete({ search, exclude }: ReturnType<typeof tagLoadAutocom
yield put(tagSetAutocomplete({ isLoading: true })); yield put(tagSetAutocomplete({ isLoading: true }));
yield delay(100); yield delay(100);
const { data, error }: Unwrap<ReturnType<typeof getTagAutocomplete>> = yield call( const { data, error }: Unwrap<typeof getTagAutocomplete> = yield call(
reqWrapper, wrap,
getTagAutocomplete, getTagAutocomplete,
{ search, exclude } { search, exclude }
); );

View file

@ -192,7 +192,13 @@ export type INodeNotification = {
export type INotification = IMessageNotification | ICommentNotification; export type INotification = IMessageNotification | ICommentNotification;
export type Unwrap<T> = T extends Promise<infer U> ? U : T; export type Unwrap<T> = T extends (...args: any) => Promise<any>
? T extends (...args: any) => Promise<infer U>
? U
: T
: T extends () => Iterator<any, infer U, any>
? U
: any;
export interface IEmbed { export interface IEmbed {
provider: string; provider: string;

View file

@ -8,7 +8,7 @@ import {
uploadDropStatus, uploadDropStatus,
uploadAddFile, uploadAddFile,
} from './actions'; } from './actions';
import { reqWrapper } from '../auth/sagas'; import { wrap } from '../auth/sagas';
import { createUploader, uploadGetThumb } from '~/utils/uploader'; import { createUploader, uploadGetThumb } from '~/utils/uploader';
import { HTTP_RESPONSES } from '~/utils/api'; import { HTTP_RESPONSES } from '~/utils/api';
import { IFileWithUUID, IFile, IUploadProgressHandler } from '../types'; import { IFileWithUUID, IFile, IUploadProgressHandler } from '../types';
@ -20,7 +20,7 @@ function* uploadCall({
type, type,
onProgress, onProgress,
}: IFileWithUUID & { onProgress: IUploadProgressHandler }) { }: IFileWithUUID & { onProgress: IUploadProgressHandler }) {
return yield call(reqWrapper, postUploadFile, { return yield call(wrap, postUploadFile, {
file, file,
temp_id, temp_id,
type, type,

View file

@ -1,4 +1,4 @@
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import { API } from '~/constants/api'; import { API } from '~/constants/api';
import { store } from '~/redux/store'; import { store } from '~/redux/store';
@ -50,3 +50,5 @@ export const configWithToken = (
...config, ...config,
headers: { ...(config.headers || {}), Authorization: `Bearer ${access}` }, headers: { ...(config.headers || {}), Authorization: `Bearer ${access}` },
}); });
export const cleanResult = <T extends any>(response: AxiosResponse<T>): T => response?.data;

View file

@ -3,13 +3,13 @@ import { IMAGE_MIME_TYPES } from '~/utils/uploader';
const isValidEmail = (email: string): boolean => const isValidEmail = (email: string): boolean =>
!!email && !!email &&
String(email) && !!String(email) &&
!!String(email).match( !!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])?$/ /^[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 => 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 isNonEmpty = (value: string): boolean => !!value && value.trim().length > 0;
const isLikePhone = isNonEmpty; const isLikePhone = isNonEmpty;

View file

@ -11,7 +11,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": false, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "esnext", "module": "esnext",