mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-01-12 23:02:16 +05:30
Merge branch '355-new-accounts-api-migration' into 'develop'
New Accounts API migration See merge request elyby/accounts!4
This commit is contained in:
commit
63de8ed548
@ -1,7 +1,8 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
import type { Account, State as AccountsState } from './reducer';
|
||||||
import { getJwtPayload } from 'functions';
|
import { getJwtPayload } from 'functions';
|
||||||
import { sessionStorage } from 'services/localStorage';
|
import { sessionStorage } from 'services/localStorage';
|
||||||
import authentication from 'services/api/authentication';
|
import { validateToken, requestToken, logout } from 'services/api/authentication';
|
||||||
import { relogin as navigateToLogin } from 'components/auth/actions';
|
import { relogin as navigateToLogin } from 'components/auth/actions';
|
||||||
import { updateUser, setGuest } from 'components/user/actions';
|
import { updateUser, setGuest } from 'components/user/actions';
|
||||||
import { setLocale } from 'components/i18n/actions';
|
import { setLocale } from 'components/i18n/actions';
|
||||||
@ -9,7 +10,6 @@ import { setAccountSwitcher } from 'components/auth/actions';
|
|||||||
import { getActiveAccount } from 'components/accounts/reducer';
|
import { getActiveAccount } from 'components/accounts/reducer';
|
||||||
import logger from 'services/logger';
|
import logger from 'services/logger';
|
||||||
|
|
||||||
import type { Account, State as AccountsState } from './reducer';
|
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
@ -42,75 +42,95 @@ export function authenticate(account: Account | {
|
|||||||
token: string,
|
token: string,
|
||||||
refreshToken: ?string,
|
refreshToken: ?string,
|
||||||
}) {
|
}) {
|
||||||
const {token, refreshToken} = account;
|
const { token, refreshToken } = account;
|
||||||
const email = account.email || null;
|
const email = account.email || null;
|
||||||
|
|
||||||
return (dispatch: Dispatch, getState: () => State): Promise<Account> => {
|
return async (dispatch: Dispatch, getState: () => State): Promise<Account> => {
|
||||||
const accountId: number | null = typeof account.id === 'number' ? account.id : null;
|
let accountId: number;
|
||||||
const knownAccount: ?Account = accountId
|
if (typeof account.id === 'number') {
|
||||||
? getState().accounts.available.find((item) => item.id === accountId)
|
accountId = account.id;
|
||||||
: null;
|
} else {
|
||||||
|
accountId = findAccountIdFromToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
const knownAccount = getState().accounts.available.find((item) => item.id === accountId);
|
||||||
if (knownAccount) {
|
if (knownAccount) {
|
||||||
// this account is already available
|
// this account is already available
|
||||||
// activate it before validation
|
// activate it before validation
|
||||||
dispatch(activate(knownAccount));
|
dispatch(activate(knownAccount));
|
||||||
}
|
}
|
||||||
|
|
||||||
return authentication.validateToken({token, refreshToken})
|
try {
|
||||||
.catch((resp = {}) => {
|
const {
|
||||||
// all the logic to get the valid token was failed,
|
token: newToken,
|
||||||
// looks like we have some problems with token
|
refreshToken: newRefreshToken,
|
||||||
// lets redirect to login page
|
user,
|
||||||
if (typeof email === 'string') {
|
// $FlowFixMe have no idea why it's causes error about missing properties
|
||||||
// TODO: we should somehow try to find email by token
|
} = await validateToken(accountId, token, refreshToken);
|
||||||
dispatch(relogin(email));
|
const { auth } = getState();
|
||||||
}
|
const account: Account = {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
token: newToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
};
|
||||||
|
dispatch(add(account));
|
||||||
|
dispatch(activate(account));
|
||||||
|
dispatch(updateUser({
|
||||||
|
isGuest: false,
|
||||||
|
...user,
|
||||||
|
}));
|
||||||
|
|
||||||
return Promise.reject(resp);
|
// TODO: probably should be moved from here, because it is a side effect
|
||||||
})
|
logger.setUser(user);
|
||||||
.then(({token, refreshToken, user}) => ({
|
|
||||||
user: {
|
|
||||||
isGuest: false,
|
|
||||||
...user
|
|
||||||
},
|
|
||||||
account: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
token,
|
|
||||||
refreshToken
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.then(({user, account}) => {
|
|
||||||
const {auth} = getState();
|
|
||||||
|
|
||||||
dispatch(add(account));
|
if (!newRefreshToken) {
|
||||||
dispatch(activate(account));
|
// mark user as stranger (user does not want us to remember his account)
|
||||||
dispatch(updateUser(user));
|
sessionStorage.setItem(`stranger${account.id}`, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: probably should be moved from here, because it is a side effect
|
if (auth && auth.oauth && auth.oauth.clientId) {
|
||||||
logger.setUser(user);
|
// if we authenticating during oauth, we disable account chooser
|
||||||
|
// because user probably has made his choise now
|
||||||
|
// this may happen, when user registers, logs in or uses account
|
||||||
|
// chooser panel during oauth
|
||||||
|
dispatch(setAccountSwitcher(false));
|
||||||
|
}
|
||||||
|
|
||||||
if (!account.refreshToken) {
|
await dispatch(setLocale(user.lang));
|
||||||
// mark user as stranger (user does not want us to remember his account)
|
|
||||||
sessionStorage.setItem(`stranger${account.id}`, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth && auth.oauth && auth.oauth.clientId) {
|
return account;
|
||||||
// if we authenticating during oauth, we disable account chooser
|
} catch (resp) {
|
||||||
// because user probably has made his choise now
|
// all the logic to get the valid token was failed,
|
||||||
// this may happen, when user registers, logs in or uses account
|
// looks like we have some problems with token
|
||||||
// chooser panel during oauth
|
// lets redirect to login page
|
||||||
dispatch(setAccountSwitcher(false));
|
if (typeof email === 'string') {
|
||||||
}
|
// TODO: we should somehow try to find email by token
|
||||||
|
dispatch(relogin(email));
|
||||||
|
}
|
||||||
|
|
||||||
return dispatch(setLocale(user.lang))
|
throw resp;
|
||||||
.then(() => account);
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findAccountIdFromToken(token: string): number {
|
||||||
|
const encodedPayloads = token.split('.')[1];
|
||||||
|
const { sub, jti }: { sub: string, jti: number } = JSON.parse(atob(encodedPayloads));
|
||||||
|
// sub has the format "ely|{accountId}", so we must trim "ely|" part
|
||||||
|
if (sub) {
|
||||||
|
return parseInt(sub.substr(4), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In older backend versions identity was stored in jti claim. Some users still have such tokens
|
||||||
|
if (jti) {
|
||||||
|
return jti;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('payloads is not contains any identity claim');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the current user's token exp time. Supposed to be used before performing
|
* Checks the current user's token exp time. Supposed to be used before performing
|
||||||
* any api request
|
* any api request
|
||||||
@ -203,8 +223,8 @@ export function requestNewToken() {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return authentication.requestToken(refreshToken)
|
return requestToken(refreshToken)
|
||||||
.then(({ token }) => {
|
.then((token) => {
|
||||||
dispatch(updateToken(token));
|
dispatch(updateToken(token));
|
||||||
})
|
})
|
||||||
.catch((resp) => {
|
.catch((resp) => {
|
||||||
@ -234,7 +254,7 @@ export function revoke(account: Account) {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
// we need to logout user, even in case, when we can
|
// we need to logout user, even in case, when we can
|
||||||
// not authenticate him with new account
|
// not authenticate him with new account
|
||||||
authentication.logout(account)
|
logout(account.token)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// we don't care
|
// we don't care
|
||||||
});
|
});
|
||||||
@ -268,7 +288,7 @@ export function logoutAll() {
|
|||||||
const {accounts: {available}} = getState();
|
const {accounts: {available}} = getState();
|
||||||
|
|
||||||
available.forEach((account) =>
|
available.forEach((account) =>
|
||||||
authentication.logout(account)
|
logout(account.token)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// we don't care
|
// we don't care
|
||||||
})
|
})
|
||||||
@ -303,7 +323,7 @@ export function logoutStrangers() {
|
|||||||
available.filter(isStranger)
|
available.filter(isStranger)
|
||||||
.forEach((account) => {
|
.forEach((account) => {
|
||||||
dispatch(remove(account));
|
dispatch(remove(account));
|
||||||
authentication.logout(account);
|
logout(account.token);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activeAccount && isStranger(activeAccount)) {
|
if (activeAccount && isStranger(activeAccount)) {
|
||||||
|
@ -5,30 +5,33 @@ import { browserHistory } from 'services/history';
|
|||||||
|
|
||||||
import { InternalServerError } from 'services/request';
|
import { InternalServerError } from 'services/request';
|
||||||
import { sessionStorage } from 'services/localStorage';
|
import { sessionStorage } from 'services/localStorage';
|
||||||
import authentication from 'services/api/authentication';
|
import * as authentication from 'services/api/authentication';
|
||||||
import {
|
import {
|
||||||
authenticate,
|
authenticate,
|
||||||
revoke,
|
revoke,
|
||||||
logoutAll,
|
logoutAll,
|
||||||
logoutStrangers
|
logoutStrangers,
|
||||||
} from 'components/accounts/actions';
|
} from 'components/accounts/actions';
|
||||||
import {
|
import {
|
||||||
add, ADD,
|
add, ADD,
|
||||||
activate, ACTIVATE,
|
activate, ACTIVATE,
|
||||||
remove,
|
remove,
|
||||||
reset
|
reset,
|
||||||
} from 'components/accounts/actions/pure-actions';
|
} from 'components/accounts/actions/pure-actions';
|
||||||
import { SET_LOCALE } from 'components/i18n/actions';
|
import { SET_LOCALE } from 'components/i18n/actions';
|
||||||
|
|
||||||
import { updateUser, setUser } from 'components/user/actions';
|
import { updateUser, setUser } from 'components/user/actions';
|
||||||
import { setLogin, setAccountSwitcher } from 'components/auth/actions';
|
import { setLogin, setAccountSwitcher } from 'components/auth/actions';
|
||||||
|
|
||||||
|
const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbHl8MSJ9.pRJ7vakt2eIscjqwG__KhSxKb3qwGsdBBeDbBffJs_I';
|
||||||
|
const legacyToken = 'eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOjF9.cRF-sQNrwWQ94xCb3vWioVdjxAZeefEE7GMGwh7708o';
|
||||||
|
|
||||||
const account = {
|
const account = {
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'username',
|
username: 'username',
|
||||||
email: 'email@test.com',
|
email: 'email@test.com',
|
||||||
token: 'foo',
|
token,
|
||||||
refreshToken: 'bar'
|
refreshToken: 'bar',
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
@ -67,7 +70,7 @@ describe('components/accounts/actions', () => {
|
|||||||
authentication.validateToken.returns(Promise.resolve({
|
authentication.validateToken.returns(Promise.resolve({
|
||||||
token: account.token,
|
token: account.token,
|
||||||
refreshToken: account.refreshToken,
|
refreshToken: account.refreshToken,
|
||||||
user
|
user,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,7 +84,29 @@ describe('components/accounts/actions', () => {
|
|||||||
it('should request user state using token', () =>
|
it('should request user state using token', () =>
|
||||||
authenticate(account)(dispatch, getState).then(() =>
|
authenticate(account)(dispatch, getState).then(() =>
|
||||||
expect(authentication.validateToken, 'to have a call satisfying', [
|
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||||
{token: account.token, refreshToken: account.refreshToken}
|
account.id,
|
||||||
|
account.token,
|
||||||
|
account.refreshToken,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should request user by extracting id from token', () =>
|
||||||
|
authenticate({ token })(dispatch, getState).then(() =>
|
||||||
|
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||||
|
1,
|
||||||
|
token,
|
||||||
|
undefined,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should request user by extracting id from legacy token', () =>
|
||||||
|
authenticate({ token: legacyToken })(dispatch, getState).then(() =>
|
||||||
|
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||||
|
1,
|
||||||
|
legacyToken,
|
||||||
|
undefined,
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -142,7 +167,7 @@ describe('components/accounts/actions', () => {
|
|||||||
it('rejects when 5xx without logouting', () => {
|
it('rejects when 5xx without logouting', () => {
|
||||||
const resp = new InternalServerError(null, {status: 500});
|
const resp = new InternalServerError(null, {status: 500});
|
||||||
|
|
||||||
authentication.validateToken.returns(Promise.reject(resp));
|
authentication.validateToken.rejects(resp);
|
||||||
|
|
||||||
return expect(authenticate(account)(dispatch, getState), 'to be rejected with', resp)
|
return expect(authenticate(account)(dispatch, getState), 'to be rejected with', resp)
|
||||||
.then(() => expect(dispatch, 'to have no calls satisfying', [
|
.then(() => expect(dispatch, 'to have no calls satisfying', [
|
||||||
@ -152,10 +177,10 @@ describe('components/accounts/actions', () => {
|
|||||||
|
|
||||||
it('marks user as stranger, if there is no refreshToken', () => {
|
it('marks user as stranger, if there is no refreshToken', () => {
|
||||||
const expectedKey = `stranger${account.id}`;
|
const expectedKey = `stranger${account.id}`;
|
||||||
authentication.validateToken.returns(Promise.resolve({
|
authentication.validateToken.resolves({
|
||||||
token: account.token,
|
token: account.token,
|
||||||
user
|
user,
|
||||||
}));
|
});
|
||||||
|
|
||||||
sessionStorage.removeItem(expectedKey);
|
sessionStorage.removeItem(expectedKey);
|
||||||
|
|
||||||
@ -247,7 +272,7 @@ describe('components/accounts/actions', () => {
|
|||||||
it('should call logout api method in background', () =>
|
it('should call logout api method in background', () =>
|
||||||
revoke(account)(dispatch, getState).then(() =>
|
revoke(account)(dispatch, getState).then(() =>
|
||||||
expect(authentication.logout, 'to have a call satisfying', [
|
expect(authentication.logout, 'to have a call satisfying', [
|
||||||
account
|
account.token
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -298,7 +323,7 @@ describe('components/accounts/actions', () => {
|
|||||||
it('should call logout api method in background', () =>
|
it('should call logout api method in background', () =>
|
||||||
revoke(account2)(dispatch, getState).then(() =>
|
revoke(account2)(dispatch, getState).then(() =>
|
||||||
expect(authentication.logout, 'to have a call satisfying', [
|
expect(authentication.logout, 'to have a call satisfying', [
|
||||||
account2
|
account2.token
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -325,8 +350,8 @@ describe('components/accounts/actions', () => {
|
|||||||
logoutAll()(dispatch, getState);
|
logoutAll()(dispatch, getState);
|
||||||
|
|
||||||
expect(authentication.logout, 'to have calls satisfying', [
|
expect(authentication.logout, 'to have calls satisfying', [
|
||||||
[account],
|
[account.token],
|
||||||
[account2]
|
[account2.token]
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -395,8 +420,8 @@ describe('components/accounts/actions', () => {
|
|||||||
logoutStrangers()(dispatch, getState);
|
logoutStrangers()(dispatch, getState);
|
||||||
|
|
||||||
expect(authentication.logout, 'to have calls satisfying', [
|
expect(authentication.logout, 'to have calls satisfying', [
|
||||||
[foreignAccount],
|
[foreignAccount.token],
|
||||||
[foreignAccount2]
|
[foreignAccount2.token]
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -458,8 +483,8 @@ describe('components/accounts/actions', () => {
|
|||||||
|
|
||||||
it('logouts all accounts', () => {
|
it('logouts all accounts', () => {
|
||||||
expect(authentication.logout, 'to have calls satisfying', [
|
expect(authentication.logout, 'to have calls satisfying', [
|
||||||
[foreignAccount],
|
[foreignAccount.token],
|
||||||
[foreignAccount2],
|
[foreignAccount2.token],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(dispatch, 'to have a call satisfying', [
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import type { OauthData } from 'services/api/oauth';
|
import type { OauthData } from 'services/api/oauth';
|
||||||
|
import type { OAuthResponse } from 'services/api/authentication';
|
||||||
import { browserHistory } from 'services/history';
|
import { browserHistory } from 'services/history';
|
||||||
import logger from 'services/logger';
|
import logger from 'services/logger';
|
||||||
import localStorage from 'services/localStorage';
|
import localStorage from 'services/localStorage';
|
||||||
@ -8,7 +9,11 @@ import history from 'services/history';
|
|||||||
import { updateUser, acceptRules as userAcceptRules } from 'components/user/actions';
|
import { updateUser, acceptRules as userAcceptRules } from 'components/user/actions';
|
||||||
import { authenticate, logoutAll } from 'components/accounts/actions';
|
import { authenticate, logoutAll } from 'components/accounts/actions';
|
||||||
import { getActiveAccount } from 'components/accounts/reducer';
|
import { getActiveAccount } from 'components/accounts/reducer';
|
||||||
import authentication from 'services/api/authentication';
|
import {
|
||||||
|
login as loginEndpoint,
|
||||||
|
forgotPassword as forgotPasswordEndpoint,
|
||||||
|
recoverPassword as recoverPasswordEndpoint,
|
||||||
|
} from 'services/api/authentication';
|
||||||
import oauth from 'services/api/oauth';
|
import oauth from 'services/api/oauth';
|
||||||
import signup from 'services/api/signup';
|
import signup from 'services/api/signup';
|
||||||
import dispatchBsod from 'components/ui/bsod/dispatchBsod';
|
import dispatchBsod from 'components/ui/bsod/dispatchBsod';
|
||||||
@ -76,9 +81,7 @@ export function login({
|
|||||||
rememberMe?: bool
|
rememberMe?: bool
|
||||||
}) {
|
}) {
|
||||||
return wrapInLoader((dispatch) =>
|
return wrapInLoader((dispatch) =>
|
||||||
authentication.login(
|
loginEndpoint({login, password, totp, rememberMe})
|
||||||
{login, password, totp, rememberMe}
|
|
||||||
)
|
|
||||||
.then(authHandler(dispatch))
|
.then(authHandler(dispatch))
|
||||||
.catch((resp) => {
|
.catch((resp) => {
|
||||||
if (resp.errors) {
|
if (resp.errors) {
|
||||||
@ -120,9 +123,9 @@ export function forgotPassword({
|
|||||||
captcha: string
|
captcha: string
|
||||||
}) {
|
}) {
|
||||||
return wrapInLoader((dispatch, getState) =>
|
return wrapInLoader((dispatch, getState) =>
|
||||||
authentication.forgotPassword({login, captcha})
|
forgotPasswordEndpoint(login, captcha)
|
||||||
.then(({data = {}}) => dispatch(updateUser({
|
.then(({data = {}}) => dispatch(updateUser({
|
||||||
maskedEmail: data.emailMask || getState().user.email
|
maskedEmail: data.emailMask || getState().user.email,
|
||||||
})))
|
})))
|
||||||
.catch(validationErrorsHandler(dispatch))
|
.catch(validationErrorsHandler(dispatch))
|
||||||
);
|
);
|
||||||
@ -138,7 +141,7 @@ export function recoverPassword({
|
|||||||
newRePassword: string
|
newRePassword: string
|
||||||
}) {
|
}) {
|
||||||
return wrapInLoader((dispatch) =>
|
return wrapInLoader((dispatch) =>
|
||||||
authentication.recoverPassword({key, newPassword, newRePassword})
|
recoverPasswordEndpoint(key, newPassword, newRePassword)
|
||||||
.then(authHandler(dispatch))
|
.then(authHandler(dispatch))
|
||||||
.catch(validationErrorsHandler(dispatch, '/forgot-password'))
|
.catch(validationErrorsHandler(dispatch, '/forgot-password'))
|
||||||
);
|
);
|
||||||
@ -544,9 +547,9 @@ function needActivation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function authHandler(dispatch) {
|
function authHandler(dispatch) {
|
||||||
return (resp) => dispatch(authenticate({
|
return (resp: OAuthResponse) => dispatch(authenticate({
|
||||||
token: resp.access_token,
|
token: resp.access_token,
|
||||||
refreshToken: resp.refresh_token
|
refreshToken: resp.refresh_token,
|
||||||
})).then((resp) => {
|
})).then((resp) => {
|
||||||
dispatch(setLogin(null));
|
dispatch(setLogin(null));
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import logger from 'services/logger';
|
import logger from 'services/logger';
|
||||||
import mfa from 'services/api/mfa';
|
import { disable as disableMFA } from 'services/api/mfa';
|
||||||
|
|
||||||
import MfaDisableForm from './disableForm/MfaDisableForm';
|
import MfaDisableForm from './disableForm/MfaDisableForm';
|
||||||
import MfaStatus from './status/MfaStatus';
|
import MfaStatus from './status/MfaStatus';
|
||||||
@ -15,6 +16,10 @@ export default class MfaDisable extends Component<{
|
|||||||
}, {
|
}, {
|
||||||
showForm?: bool
|
showForm?: bool
|
||||||
}> {
|
}> {
|
||||||
|
static contextTypes = {
|
||||||
|
userId: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
state = {};
|
state = {};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -35,7 +40,7 @@ export default class MfaDisable extends Component<{
|
|||||||
() => {
|
() => {
|
||||||
const data = form.serialize();
|
const data = form.serialize();
|
||||||
|
|
||||||
return mfa.disable(data);
|
return disableMFA(this.context.userId, data);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => this.props.onComplete())
|
.then(() => this.props.onComplete())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Button, FormModel } from 'components/ui/form';
|
import { Button, FormModel } from 'components/ui/form';
|
||||||
import styles from 'components/profile/profileForm.scss';
|
import styles from 'components/profile/profileForm.scss';
|
||||||
@ -7,7 +8,7 @@ import Stepper from 'components/ui/stepper';
|
|||||||
import { SlideMotion } from 'components/ui/motion';
|
import { SlideMotion } from 'components/ui/motion';
|
||||||
import { ScrollIntoView } from 'components/ui/scroll';
|
import { ScrollIntoView } from 'components/ui/scroll';
|
||||||
import logger from 'services/logger';
|
import logger from 'services/logger';
|
||||||
import mfa from 'services/api/mfa';
|
import { getSecret, enable as enableMFA } from 'services/api/mfa';
|
||||||
|
|
||||||
import Instructions from './instructions';
|
import Instructions from './instructions';
|
||||||
import KeyForm from './keyForm';
|
import KeyForm from './keyForm';
|
||||||
@ -24,25 +25,31 @@ type Props = {
|
|||||||
confirmationForm: FormModel,
|
confirmationForm: FormModel,
|
||||||
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
||||||
onComplete: Function,
|
onComplete: Function,
|
||||||
step: MfaStep
|
step: MfaStep,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class MfaEnable extends Component<Props, {
|
interface State {
|
||||||
isLoading: bool,
|
isLoading: bool;
|
||||||
activeStep: MfaStep,
|
activeStep: MfaStep;
|
||||||
secret: string,
|
secret: string;
|
||||||
qrCodeSrc: string
|
qrCodeSrc: string;
|
||||||
}> {
|
}
|
||||||
|
|
||||||
|
export default class MfaEnable extends Component<Props, State> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
confirmationForm: new FormModel(),
|
confirmationForm: new FormModel(),
|
||||||
step: 0
|
step: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
userId: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
activeStep: this.props.step,
|
activeStep: this.props.step,
|
||||||
qrCodeSrc: '',
|
qrCodeSrc: '',
|
||||||
secret: ''
|
secret: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
confirmationFormEl: ?Form;
|
confirmationFormEl: ?Form;
|
||||||
@ -124,7 +131,7 @@ export default class MfaEnable extends Component<Props, {
|
|||||||
if (props.step === 1) {
|
if (props.step === 1) {
|
||||||
this.setState({isLoading: true});
|
this.setState({isLoading: true});
|
||||||
|
|
||||||
mfa.getSecret().then((resp) => {
|
getSecret(this.context.userId).then((resp) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
secret: resp.secret,
|
secret: resp.secret,
|
||||||
@ -154,7 +161,7 @@ export default class MfaEnable extends Component<Props, {
|
|||||||
() => {
|
() => {
|
||||||
const data = form.serialize();
|
const data = form.serialize();
|
||||||
|
|
||||||
return mfa.enable(data);
|
return enableMFA(this.context.userId, data.totp, data.password);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => this.props.onComplete())
|
.then(() => this.props.onComplete())
|
||||||
|
@ -16,7 +16,7 @@ export default function Stepper({
|
|||||||
}: {
|
}: {
|
||||||
totalSteps: number,
|
totalSteps: number,
|
||||||
activeStep: number,
|
activeStep: number,
|
||||||
color: Color
|
color?: Color,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.steps, styles[`${color}Steps`])}>
|
<div className={classNames(styles.steps, styles[`${color}Steps`])}>
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import accounts from 'services/api/accounts';
|
// @flow
|
||||||
|
import type { User, State } from './reducer';
|
||||||
|
import {
|
||||||
|
getInfo as getInfoEndpoint,
|
||||||
|
changeLang as changeLangEndpoint,
|
||||||
|
acceptRules as acceptRulesEndpoint,
|
||||||
|
} from 'services/api/accounts';
|
||||||
import { setLocale } from 'components/i18n/actions';
|
import { setLocale } from 'components/i18n/actions';
|
||||||
|
|
||||||
|
type Dispatch = (action: Object) => Promise<*>;
|
||||||
|
|
||||||
export const UPDATE = 'USER_UPDATE';
|
export const UPDATE = 'USER_UPDATE';
|
||||||
/**
|
/**
|
||||||
* Merge data into user's state
|
* Merge data into user's state
|
||||||
@ -8,10 +16,10 @@ export const UPDATE = 'USER_UPDATE';
|
|||||||
* @param {object} payload
|
* @param {object} payload
|
||||||
* @return {object} - action definition
|
* @return {object} - action definition
|
||||||
*/
|
*/
|
||||||
export function updateUser(payload) {
|
export function updateUser(payload: $Shape<User>) { // Temp workaround
|
||||||
return {
|
return {
|
||||||
type: UPDATE,
|
type: UPDATE,
|
||||||
payload
|
payload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,61 +30,69 @@ export const SET = 'USER_SET';
|
|||||||
* @param {User} payload
|
* @param {User} payload
|
||||||
* @return {object} - action definition
|
* @return {object} - action definition
|
||||||
*/
|
*/
|
||||||
export function setUser(payload) {
|
export function setUser(payload: $Shape<User>) {
|
||||||
return {
|
return {
|
||||||
type: SET,
|
type: SET,
|
||||||
payload
|
payload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
||||||
export function changeLang(lang) {
|
export function changeLang(lang: string) {
|
||||||
return (dispatch, getState) => dispatch(setLocale(lang))
|
return (dispatch: Dispatch, getState: () => State) => dispatch(setLocale(lang))
|
||||||
.then((lang) => {
|
.then((lang) => {
|
||||||
const {user: {isGuest, lang: oldLang}} = getState();
|
const { id, isGuest, lang: oldLang } = getState().user;
|
||||||
|
if (oldLang === lang) {
|
||||||
if (oldLang !== lang) {
|
return;
|
||||||
!isGuest && accounts.changeLang(lang);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: CHANGE_LANG,
|
|
||||||
payload: {
|
|
||||||
lang
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
!isGuest && changeLangEndpoint(((id: any): number), lang); // hack to tell Flow that it's defined
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: CHANGE_LANG,
|
||||||
|
payload: {
|
||||||
|
lang,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setGuest() {
|
export function setGuest() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch: Dispatch, getState: () => Object) => {
|
||||||
dispatch(setUser({
|
dispatch(setUser({
|
||||||
lang: getState().user.lang,
|
lang: getState().user.lang,
|
||||||
isGuest: true
|
isGuest: true,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchUserData() {
|
export function fetchUserData() {
|
||||||
return (dispatch) =>
|
return async (dispatch: Dispatch, getState: () => State) => {
|
||||||
accounts.current()
|
// $FlowFixMe
|
||||||
.then((resp) => {
|
const resp = await getInfoEndpoint(getState().user.id);
|
||||||
dispatch(updateUser({
|
dispatch(updateUser({
|
||||||
isGuest: false,
|
isGuest: false,
|
||||||
...resp
|
...resp,
|
||||||
}));
|
}));
|
||||||
|
dispatch(changeLang(resp.lang));
|
||||||
|
|
||||||
return dispatch(changeLang(resp.lang));
|
return resp;
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function acceptRules() {
|
export function acceptRules() {
|
||||||
return (dispatch) =>
|
return (dispatch: Dispatch, getState: () => State) => {
|
||||||
accounts.acceptRules().then((resp) => {
|
const { id } = getState().user;
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('user id is should be set at the moment when this action is called');
|
||||||
|
}
|
||||||
|
|
||||||
|
return acceptRulesEndpoint(id).then((resp) => {
|
||||||
dispatch(updateUser({
|
dispatch(updateUser({
|
||||||
shouldAcceptRules: false
|
shouldAcceptRules: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import sinon from 'sinon';
|
|||||||
|
|
||||||
import refreshTokenMiddleware from 'components/user/middlewares/refreshTokenMiddleware';
|
import refreshTokenMiddleware from 'components/user/middlewares/refreshTokenMiddleware';
|
||||||
import { browserHistory } from 'services/history';
|
import { browserHistory } from 'services/history';
|
||||||
import authentication from 'services/api/authentication';
|
import * as authentication from 'services/api/authentication';
|
||||||
import { InternalServerError } from 'services/request';
|
import { InternalServerError } from 'services/request';
|
||||||
import { updateToken } from 'components/accounts/actions';
|
import { updateToken } from 'components/accounts/actions';
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
authentication.requestToken.returns(Promise.resolve({token: validToken}));
|
authentication.requestToken.returns(Promise.resolve(validToken));
|
||||||
|
|
||||||
return middleware.before(data).then((resp) => {
|
return middleware.before(data).then((resp) => {
|
||||||
expect(resp, 'to satisfy', data);
|
expect(resp, 'to satisfy', data);
|
||||||
@ -126,7 +126,7 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
authentication.requestToken.returns(Promise.resolve({token: validToken}));
|
authentication.requestToken.returns(Promise.resolve(validToken));
|
||||||
|
|
||||||
return middleware.before(data).then(() =>
|
return middleware.before(data).then(() =>
|
||||||
expect(dispatch, 'to have a call satisfying', [
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
@ -251,7 +251,7 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
|
|
||||||
restart = sinon.stub().named('restart');
|
restart = sinon.stub().named('restart');
|
||||||
|
|
||||||
authentication.requestToken.returns(Promise.resolve({token: validToken}));
|
authentication.requestToken.returns(Promise.resolve(validToken));
|
||||||
});
|
});
|
||||||
|
|
||||||
function assertNewTokenRequest() {
|
function assertNewTokenRequest() {
|
||||||
|
@ -18,6 +18,10 @@ export type User = {|
|
|||||||
shouldAcceptRules?: bool,
|
shouldAcceptRules?: bool,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
export type State = {
|
||||||
|
user: User, // TODO: replace with centralized global state
|
||||||
|
};
|
||||||
|
|
||||||
const defaults: User = {
|
const defaults: User = {
|
||||||
id: null,
|
id: null,
|
||||||
uuid: null,
|
uuid: null,
|
||||||
|
@ -1,34 +1,36 @@
|
|||||||
|
// @flow
|
||||||
|
import type { RouterHistory, Match } from 'react-router';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import ChangeEmail from 'components/profile/changeEmail/ChangeEmail';
|
import ChangeEmail from 'components/profile/changeEmail/ChangeEmail';
|
||||||
|
|
||||||
import accounts from 'services/api/accounts';
|
import { requestEmailChange, setNewEmail, confirmNewEmail } from 'services/api/accounts';
|
||||||
|
|
||||||
class ChangeEmailPage extends Component {
|
interface Props {
|
||||||
|
lang: string;
|
||||||
|
email: string;
|
||||||
|
history: RouterHistory;
|
||||||
|
match: {
|
||||||
|
...Match;
|
||||||
|
params: {
|
||||||
|
step: 'step1' | 'step2' | 'step3';
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangeEmailPage extends Component<Props> {
|
||||||
static displayName = 'ChangeEmailPage';
|
static displayName = 'ChangeEmailPage';
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
email: PropTypes.string.isRequired,
|
|
||||||
lang: PropTypes.string.isRequired,
|
|
||||||
history: PropTypes.shape({
|
|
||||||
push: PropTypes.func
|
|
||||||
}).isRequired,
|
|
||||||
match: PropTypes.shape({
|
|
||||||
params: PropTypes.shape({
|
|
||||||
step: PropTypes.oneOf(['step1', 'step2', 'step3']),
|
|
||||||
code: PropTypes.string
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
userId: PropTypes.number.isRequired,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
goToProfile: PropTypes.func.isRequired
|
goToProfile: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const step = this.props.match.params.step;
|
const { step } = this.props.match.params;
|
||||||
|
|
||||||
if (step && !/^step[123]$/.test(step)) {
|
if (step && !/^step[123]$/.test(step)) {
|
||||||
// wrong param value
|
// wrong param value
|
||||||
@ -37,14 +39,14 @@ class ChangeEmailPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {step = 'step1', code} = this.props.match.params;
|
const { step = 'step1', code } = this.props.match.params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangeEmail
|
<ChangeEmail
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
email={this.props.email}
|
email={this.props.email}
|
||||||
lang={this.props.lang}
|
lang={this.props.lang}
|
||||||
step={step.slice(-1) * 1 - 1}
|
step={((step.slice(-1): any): number) * 1 - 1}
|
||||||
onChangeStep={this.onChangeStep}
|
onChangeStep={this.onChangeStep}
|
||||||
code={code}
|
code={code}
|
||||||
/>
|
/>
|
||||||
@ -55,19 +57,20 @@ class ChangeEmailPage extends Component {
|
|||||||
this.props.history.push(`/profile/change-email/step${++step}`);
|
this.props.history.push(`/profile/change-email/step${++step}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = (step, form) => {
|
onSubmit = (step: number, form) => {
|
||||||
return this.context.onSubmit({
|
return this.context.onSubmit({
|
||||||
form,
|
form,
|
||||||
sendData: () => {
|
sendData: () => {
|
||||||
|
const { userId } = this.context;
|
||||||
const data = form.serialize();
|
const data = form.serialize();
|
||||||
|
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 0:
|
case 0:
|
||||||
return accounts.requestEmailChange(data).catch(handleErrors());
|
return requestEmailChange(userId, data.password).catch(handleErrors());
|
||||||
case 1:
|
case 1:
|
||||||
return accounts.setNewEmail(data).catch(handleErrors('/profile/change-email'));
|
return setNewEmail(userId, data.email, data.key).catch(handleErrors('/profile/change-email'));
|
||||||
case 2:
|
case 2:
|
||||||
return accounts.confirmNewEmail(data).catch(handleErrors('/profile/change-email'));
|
return confirmNewEmail(userId, data.key).catch(handleErrors('/profile/change-email'));
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported step ${step}`);
|
throw new Error(`Unsupported step ${step}`);
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
|
// @flow
|
||||||
|
import type { User } from 'components/user';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import accounts from 'services/api/accounts';
|
import { changePassword } from 'services/api/accounts';
|
||||||
import { FormModel } from 'components/ui/form';
|
import { FormModel } from 'components/ui/form';
|
||||||
import ChangePassword from 'components/profile/changePassword/ChangePassword';
|
import ChangePassword from 'components/profile/changePassword/ChangePassword';
|
||||||
|
|
||||||
class ChangePasswordPage extends Component {
|
interface Props {
|
||||||
|
updateUser: (fields: $Shape<User>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangePasswordPage extends Component<Props> {
|
||||||
static displayName = 'ChangePasswordPage';
|
static displayName = 'ChangePasswordPage';
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
updateUser: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
userId: PropTypes.number.isRequired,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
goToProfile: PropTypes.func.isRequired
|
goToProfile: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
form = new FormModel();
|
form = new FormModel();
|
||||||
@ -29,7 +32,7 @@ class ChangePasswordPage extends Component {
|
|||||||
const {form} = this;
|
const {form} = this;
|
||||||
return this.context.onSubmit({
|
return this.context.onSubmit({
|
||||||
form,
|
form,
|
||||||
sendData: () => accounts.changePassword(form.serialize())
|
sendData: () => changePassword(this.context.userId, form.serialize()),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.props.updateUser({
|
this.props.updateUser({
|
||||||
passwordChangedAt: Date.now() / 1000
|
passwordChangedAt: Date.now() / 1000
|
||||||
@ -43,5 +46,5 @@ import { connect } from 'react-redux';
|
|||||||
import { updateUser } from 'components/user/actions';
|
import { updateUser } from 'components/user/actions';
|
||||||
|
|
||||||
export default connect(null, {
|
export default connect(null, {
|
||||||
updateUser
|
updateUser,
|
||||||
})(ChangePasswordPage);
|
})(ChangePasswordPage);
|
||||||
|
@ -1,25 +1,29 @@
|
|||||||
|
// @flow
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import accounts from 'services/api/accounts';
|
import { changeUsername } from 'services/api/accounts';
|
||||||
import { FormModel } from 'components/ui/form';
|
import { FormModel } from 'components/ui/form';
|
||||||
import ChangeUsername from 'components/profile/changeUsername/ChangeUsername';
|
import ChangeUsername from 'components/profile/changeUsername/ChangeUsername';
|
||||||
|
|
||||||
class ChangeUsernamePage extends Component {
|
interface Props {
|
||||||
|
username: string;
|
||||||
|
updateUsername: (username: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangeUsernamePage extends Component<Props> {
|
||||||
static displayName = 'ChangeUsernamePage';
|
static displayName = 'ChangeUsernamePage';
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
updateUsername: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
userId: PropTypes.number.isRequired,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
goToProfile: PropTypes.func.isRequired
|
goToProfile: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
form = new FormModel();
|
form = new FormModel();
|
||||||
|
|
||||||
|
actualUsername: string;
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.actualUsername = this.props.username;
|
this.actualUsername = this.props.username;
|
||||||
}
|
}
|
||||||
@ -43,7 +47,7 @@ class ChangeUsernamePage extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
const {form} = this;
|
const { form } = this;
|
||||||
if (this.actualUsername === this.props.username) {
|
if (this.actualUsername === this.props.username) {
|
||||||
this.context.goToProfile();
|
this.context.goToProfile();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@ -51,7 +55,10 @@ class ChangeUsernamePage extends Component {
|
|||||||
|
|
||||||
return this.context.onSubmit({
|
return this.context.onSubmit({
|
||||||
form,
|
form,
|
||||||
sendData: () => accounts.changeUsername(form.serialize())
|
sendData: () => {
|
||||||
|
const { username, password } = form.serialize();
|
||||||
|
return changeUsername(this.context.userId, username, password);
|
||||||
|
},
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.actualUsername = form.value('username');
|
this.actualUsername = form.value('username');
|
||||||
|
|
||||||
@ -64,7 +71,7 @@ import { connect } from 'react-redux';
|
|||||||
import { updateUser } from 'components/user/actions';
|
import { updateUser } from 'components/user/actions';
|
||||||
|
|
||||||
export default connect((state) => ({
|
export default connect((state) => ({
|
||||||
username: state.user.username
|
username: state.user.username,
|
||||||
}), {
|
}), {
|
||||||
updateUsername: (username) => updateUser({username})
|
updateUsername: (username) => updateUser({username}),
|
||||||
})(ChangeUsernamePage);
|
})(ChangeUsernamePage);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
import type { RouterHistory, Match } from 'react-router';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@ -8,17 +9,18 @@ import type { MfaStep } from 'components/profile/multiFactorAuth';
|
|||||||
import type { FormModel } from 'components/ui/form';
|
import type { FormModel } from 'components/ui/form';
|
||||||
import type { User } from 'components/user';
|
import type { User } from 'components/user';
|
||||||
|
|
||||||
class MultiFactorAuthPage extends Component<{
|
interface Props {
|
||||||
user: User,
|
user: User;
|
||||||
history: {
|
history: RouterHistory;
|
||||||
push: (string) => void
|
|
||||||
},
|
|
||||||
match: {
|
match: {
|
||||||
|
...Match;
|
||||||
params: {
|
params: {
|
||||||
step?: '1' | '2' | '3'
|
step?: '1' | '2' | '3';
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}> {
|
}
|
||||||
|
|
||||||
|
class MultiFactorAuthPage extends Component<Props> {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
goToProfile: PropTypes.func.isRequired
|
goToProfile: PropTypes.func.isRequired
|
||||||
@ -26,7 +28,7 @@ class MultiFactorAuthPage extends Component<{
|
|||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const step = this.props.match.params.step;
|
const step = this.props.match.params.step;
|
||||||
const {user} = this.props;
|
const { user } = this.props;
|
||||||
|
|
||||||
if (step) {
|
if (step) {
|
||||||
if (!/^[1-3]$/.test(step)) {
|
if (!/^[1-3]$/.test(step)) {
|
||||||
@ -42,7 +44,7 @@ class MultiFactorAuthPage extends Component<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {user} = this.props;
|
const { user } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiFactorAuth
|
<MultiFactorAuth
|
||||||
|
@ -20,19 +20,24 @@ import styles from './profile.scss';
|
|||||||
|
|
||||||
import type { FormModel } from 'components/ui/form';
|
import type { FormModel } from 'components/ui/form';
|
||||||
|
|
||||||
class ProfilePage extends Component<{
|
interface Props {
|
||||||
onSubmit: ({form: FormModel, sendData: () => Promise<*>}) => void,
|
userId: number;
|
||||||
fetchUserData: () => Promise<*>
|
onSubmit: ({form: FormModel, sendData: () => Promise<*>}) => void;
|
||||||
}> {
|
fetchUserData: () => Promise<*>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfilePage extends Component<Props> {
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
|
userId: PropTypes.number,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
goToProfile: PropTypes.func
|
goToProfile: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
return {
|
return {
|
||||||
|
userId: this.props.userId,
|
||||||
onSubmit: this.props.onSubmit,
|
onSubmit: this.props.onSubmit,
|
||||||
goToProfile: () => this.props.fetchUserData().then(this.goToProfile)
|
goToProfile: () => this.props.fetchUserData().then(this.goToProfile),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +65,9 @@ class ProfilePage extends Component<{
|
|||||||
goToProfile = () => browserHistory.push('/');
|
goToProfile = () => browserHistory.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(null, {
|
export default connect((state) => ({
|
||||||
|
userId: state.user.id,
|
||||||
|
}), {
|
||||||
fetchUserData,
|
fetchUserData,
|
||||||
onSubmit: ({form, sendData}: {
|
onSubmit: ({form, sendData}: {
|
||||||
form: FormModel,
|
form: FormModel,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import request from 'services/request';
|
import request from 'services/request';
|
||||||
|
|
||||||
type UserResponse = {
|
export type UserResponse = {
|
||||||
elyProfileLink: string,
|
elyProfileLink: string,
|
||||||
email: string,
|
email: string,
|
||||||
hasMojangUsernameCollision: bool,
|
hasMojangUsernameCollision: bool,
|
||||||
@ -16,85 +16,63 @@ type UserResponse = {
|
|||||||
uuid: string,
|
uuid: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export function getInfo(id: number, token?: string): Promise<UserResponse> {
|
||||||
/**
|
return request.get(`/api/v1/accounts/${id}`, {}, {
|
||||||
* @param {object} options
|
token,
|
||||||
* @param {object} [options.token] - an optional token to overwrite headers
|
});
|
||||||
* in middleware and disable token auto-refresh
|
}
|
||||||
*
|
|
||||||
* @return {Promise<UserResponse>}
|
|
||||||
*/
|
|
||||||
current(options: { token?: ?string } = {}): Promise<UserResponse> {
|
|
||||||
return request.get('/api/accounts/current', {}, {
|
|
||||||
token: options.token
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
changePassword({
|
export function changePassword(id: number, {
|
||||||
password = '',
|
password = '',
|
||||||
newPassword = '',
|
newPassword = '',
|
||||||
newRePassword = '',
|
newRePassword = '',
|
||||||
logoutAll = true
|
logoutAll = true,
|
||||||
}: {
|
}: {
|
||||||
password?: string,
|
password?: string,
|
||||||
newPassword?: string,
|
newPassword?: string,
|
||||||
newRePassword?: string,
|
newRePassword?: string,
|
||||||
logoutAll?: bool,
|
logoutAll?: bool,
|
||||||
}) {
|
}): Promise<{ success: bool }> {
|
||||||
return request.post(
|
return request.post(`/api/v1/accounts/${id}/password`, {
|
||||||
'/api/accounts/change-password',
|
password,
|
||||||
{password, newPassword, newRePassword, logoutAll}
|
newPassword,
|
||||||
);
|
newRePassword,
|
||||||
},
|
logoutAll,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
acceptRules() {
|
export function acceptRules(id: number) {
|
||||||
return request.post('/api/accounts/accept-rules');
|
return request.post(`/api/v1/accounts/${id}/rules`);
|
||||||
},
|
}
|
||||||
|
|
||||||
changeUsername({
|
export function changeUsername(id: number, username: ?string, password: ?string) {
|
||||||
username = '',
|
return request.post(`/api/v1/accounts/${id}/username`, {
|
||||||
password = ''
|
username,
|
||||||
}: {
|
password,
|
||||||
username?: string,
|
});
|
||||||
password?: string,
|
}
|
||||||
}) {
|
|
||||||
return request.post(
|
|
||||||
'/api/accounts/change-username',
|
|
||||||
{username, password}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
changeLang(lang: string) {
|
export function changeLang(id: number, lang: string) {
|
||||||
return request.post(
|
return request.post(`/api/v1/accounts/${id}/language`, {
|
||||||
'/api/accounts/change-lang',
|
lang,
|
||||||
{lang}
|
});
|
||||||
);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
requestEmailChange({password = ''}: { password?: string }) {
|
export function requestEmailChange(id: number, password: string) {
|
||||||
return request.post(
|
return request.post(`/api/v1/accounts/${id}/email-verification`, {
|
||||||
'/api/accounts/change-email/initialize',
|
password,
|
||||||
{password}
|
});
|
||||||
);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
setNewEmail({
|
export function setNewEmail(id: number, email: string, key: string) {
|
||||||
email = '',
|
return request.post(`/api/v1/accounts/${id}/new-email-verification`, {
|
||||||
key = ''
|
email,
|
||||||
}: {
|
key,
|
||||||
email?: string,
|
});
|
||||||
key?: string,
|
}
|
||||||
}) {
|
|
||||||
return request.post(
|
|
||||||
'/api/accounts/change-email/submit-new-email',
|
|
||||||
{email, key}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmNewEmail({key}: { key: string }) {
|
export function confirmNewEmail(id: number, key: string) {
|
||||||
return request.post(
|
return request.post(`/api/v1/accounts/${id}/email`, {
|
||||||
'/api/accounts/change-email/confirm-new-email',
|
key,
|
||||||
{key}
|
});
|
||||||
);
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -1,159 +1,155 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
import type { UserResponse } from 'services/api/accounts';
|
||||||
import logger from 'services/logger';
|
import logger from 'services/logger';
|
||||||
import request, { InternalServerError } from 'services/request';
|
import request, { InternalServerError } from 'services/request';
|
||||||
import accounts from 'services/api/accounts';
|
import { getInfo as getInfoEndpoint } from 'services/api/accounts';
|
||||||
|
|
||||||
const authentication = {
|
export interface OAuthResponse {
|
||||||
login({
|
access_token: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
expires_in: number; // count seconds before expire
|
||||||
|
success: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login({
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
totp,
|
||||||
|
rememberMe = false,
|
||||||
|
}: {
|
||||||
|
login: string,
|
||||||
|
password?: string,
|
||||||
|
totp?: string,
|
||||||
|
rememberMe: bool,
|
||||||
|
}): Promise<OAuthResponse> {
|
||||||
|
return request.post('/api/authentication/login', {
|
||||||
login,
|
login,
|
||||||
password,
|
password,
|
||||||
totp,
|
totp,
|
||||||
rememberMe = false
|
rememberMe,
|
||||||
}: {
|
}, { token: null });
|
||||||
login: string,
|
}
|
||||||
password?: string,
|
|
||||||
totp?: string,
|
|
||||||
rememberMe: bool
|
|
||||||
}) {
|
|
||||||
return request.post(
|
|
||||||
'/api/authentication/login',
|
|
||||||
{login, password, totp, rememberMe},
|
|
||||||
{token: null}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} options
|
* @param {string} token - an optional token to overwrite headers
|
||||||
* @param {string} [options.token] - an optional token to overwrite headers
|
* in middleware and disable token auto-refresh
|
||||||
* in middleware and disable token auto-refresh
|
*
|
||||||
*
|
* @return {Promise}
|
||||||
* @return {Promise}
|
*/
|
||||||
*/
|
export function logout(token?: string): Promise<{success: bool}> {
|
||||||
logout(options: ?{
|
return request.post('/api/authentication/logout', {}, {
|
||||||
token: string
|
token,
|
||||||
}) {
|
});
|
||||||
return request.post('/api/authentication/logout', {}, {
|
}
|
||||||
token: options && options.token
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
forgotPassword({
|
export function forgotPassword(login: string, captcha: string): Promise<{
|
||||||
|
success: bool,
|
||||||
|
data: {
|
||||||
|
canRepeatIn: number,
|
||||||
|
emailMask: ?string,
|
||||||
|
repeatFrequency: number,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
[key: string]: string,
|
||||||
|
},
|
||||||
|
}> {
|
||||||
|
return request.post('/api/authentication/forgot-password', {
|
||||||
login,
|
login,
|
||||||
captcha
|
captcha,
|
||||||
}: {
|
}, { token: null });
|
||||||
login: string,
|
}
|
||||||
captcha: string
|
|
||||||
}) {
|
|
||||||
return request.post(
|
|
||||||
'/api/authentication/forgot-password',
|
|
||||||
{login, captcha},
|
|
||||||
{token: null}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
recoverPassword({
|
export function recoverPassword(key: string, newPassword: string, newRePassword: string): Promise<OAuthResponse> {
|
||||||
|
return request.post('/api/authentication/recover-password', {
|
||||||
key,
|
key,
|
||||||
newPassword,
|
newPassword,
|
||||||
newRePassword
|
newRePassword,
|
||||||
}: {
|
}, { token: null });
|
||||||
key: string,
|
}
|
||||||
newPassword: string,
|
|
||||||
newRePassword: string
|
|
||||||
}) {
|
|
||||||
return request.post(
|
|
||||||
'/api/authentication/recover-password',
|
|
||||||
{key, newPassword, newRePassword},
|
|
||||||
{token: null}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves if token is valid
|
* Resolves if token is valid
|
||||||
*
|
*
|
||||||
* @param {object} options
|
* @param {number} id
|
||||||
* @param {string} options.token
|
* @param {string} token
|
||||||
* @param {string} options.refreshToken
|
* @param {string} refreshToken
|
||||||
*
|
*
|
||||||
* @return {Promise} - resolves with options.token or with a new token
|
* @return {Promise} - resolves with options.token or with a new token
|
||||||
* if it was refreshed. As a side effect the response
|
* if it was refreshed. As a side effect the response
|
||||||
* will have a `user` field with current user data
|
* will have a `user` field with current user data
|
||||||
*/
|
*
|
||||||
validateToken({token, refreshToken}: {
|
*/
|
||||||
token: string,
|
export async function validateToken(id: number, token: string, refreshToken: ?string): Promise<{
|
||||||
refreshToken: ?string
|
token: string,
|
||||||
}) {
|
refreshToken: ?string,
|
||||||
return new Promise((resolve) => {
|
user: UserResponse,
|
||||||
if (typeof token !== 'string') {
|
}> {
|
||||||
throw new Error('token must be a string');
|
if (typeof token !== 'string') {
|
||||||
}
|
throw new Error('token must be a string');
|
||||||
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.then(() => accounts.current({token}))
|
|
||||||
.then((user) => ({token, refreshToken, user}))
|
|
||||||
.catch((resp) =>
|
|
||||||
this.handleTokenError(resp, refreshToken)
|
|
||||||
// TODO: use recursion here
|
|
||||||
.then(({token}) =>
|
|
||||||
accounts.current({token})
|
|
||||||
.then((user) => ({token, refreshToken, user}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTokenError(resp: Error | { message: string }, refreshToken: ?string): Promise<{
|
|
||||||
token: string,
|
|
||||||
}> {
|
|
||||||
if (resp instanceof InternalServerError) {
|
|
||||||
// delegate error recovering to the bsod middleware
|
|
||||||
return new Promise(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshToken) {
|
|
||||||
if ([
|
|
||||||
'Token expired',
|
|
||||||
'Incorrect token',
|
|
||||||
'You are requesting with an invalid credential.'
|
|
||||||
].includes(resp.message)) {
|
|
||||||
return authentication.requestToken(refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Unexpected error during token validation', {
|
|
||||||
resp
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(resp);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request new access token using a refreshToken
|
|
||||||
*
|
|
||||||
* @param {string} refreshToken
|
|
||||||
*
|
|
||||||
* @return {Promise} - resolves to {token}
|
|
||||||
*/
|
|
||||||
requestToken(refreshToken: string): Promise<{token: string}> {
|
|
||||||
return request.post(
|
|
||||||
'/api/authentication/refresh-token',
|
|
||||||
{refresh_token: refreshToken}, // eslint-disable-line
|
|
||||||
{token: null}
|
|
||||||
)
|
|
||||||
.then((resp: {access_token: string}) => ({
|
|
||||||
token: resp.access_token
|
|
||||||
}))
|
|
||||||
.catch((resp) => {
|
|
||||||
const errors = resp.errors || {};
|
|
||||||
|
|
||||||
if (errors.refresh_token !== 'error.refresh_token_not_exist') {
|
|
||||||
logger.error('Failed refreshing token: unknown error', {
|
|
||||||
resp
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(resp);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default authentication;
|
let user: UserResponse;
|
||||||
|
try {
|
||||||
|
user = await getInfoEndpoint(id, token);
|
||||||
|
} catch (resp) {
|
||||||
|
token = await handleTokenError(resp, refreshToken);
|
||||||
|
user = await getInfoEndpoint(id, token); // TODO: replace with recursive call
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
refreshToken,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoverableErrors = [
|
||||||
|
'Token expired',
|
||||||
|
'Incorrect token',
|
||||||
|
'You are requesting with an invalid credential.',
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleTokenError(resp: Error | { message: string }, refreshToken: ?string): Promise<string> {
|
||||||
|
if (resp instanceof InternalServerError) {
|
||||||
|
// delegate error recovering to the bsod middleware
|
||||||
|
return new Promise(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshToken) {
|
||||||
|
if (recoverableErrors.includes(resp.message)) {
|
||||||
|
return requestToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error('Unexpected error during token validation', { resp });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request new access token using a refreshToken
|
||||||
|
*
|
||||||
|
* @param {string} refreshToken
|
||||||
|
*
|
||||||
|
* @return {Promise} - resolves to token
|
||||||
|
*/
|
||||||
|
export async function requestToken(refreshToken: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response: OAuthResponse = await request.post('/api/authentication/refresh-token', {
|
||||||
|
refresh_token: refreshToken, // eslint-disable-line camelcase
|
||||||
|
}, {
|
||||||
|
token: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.access_token;
|
||||||
|
} catch (resp) {
|
||||||
|
const errors = resp.errors || {};
|
||||||
|
if (errors.refresh_token !== 'error.refresh_token_not_exist') {
|
||||||
|
logger.error('Failed refreshing token: unknown error', {
|
||||||
|
resp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,11 +1,34 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
import expect from 'unexpected';
|
import expect from 'unexpected';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import request from 'services/request';
|
import request from 'services/request';
|
||||||
import authentication from 'services/api/authentication';
|
import * as authentication from 'services/api/authentication';
|
||||||
import accounts from 'services/api/accounts';
|
import * as accounts from 'services/api/accounts';
|
||||||
|
|
||||||
describe('authentication api', () => {
|
describe('authentication api', () => {
|
||||||
|
let server;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
server = sinon.createFakeServer({
|
||||||
|
autoRespond: true
|
||||||
|
});
|
||||||
|
|
||||||
|
['get', 'post'].forEach((method) => {
|
||||||
|
server[method] = (url, resp = {}, status = 200, headers = {}) => {
|
||||||
|
server.respondWith(method, url, [
|
||||||
|
status,
|
||||||
|
{ 'Content-Type': 'application/json', ...headers },
|
||||||
|
JSON.stringify(resp)
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
server.restore();
|
||||||
|
});
|
||||||
|
|
||||||
describe('#login', () => {
|
describe('#login', () => {
|
||||||
const params = {
|
const params = {
|
||||||
login: 'foo',
|
login: 'foo',
|
||||||
@ -41,50 +64,53 @@ describe('authentication api', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('#validateToken()', () => {
|
describe('#validateToken()', () => {
|
||||||
const validTokens = {token: 'foo', refreshToken: 'bar'};
|
const validToken = 'foo';
|
||||||
const user = {id: 1};
|
const validRefreshToken = 'bar';
|
||||||
|
const user = { id: 1 };
|
||||||
|
const validateTokenArgs = [user.id, validToken, validRefreshToken];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sinon.stub(accounts, 'current');
|
sinon.stub(accounts, 'getInfo');
|
||||||
|
accounts.getInfo.returns(Promise.resolve(user));
|
||||||
accounts.current.returns(Promise.resolve(user));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
accounts.current.restore();
|
accounts.getInfo.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should request accounts.current', () =>
|
it('should request accounts.getInfo', () =>
|
||||||
expect(authentication.validateToken(validTokens), 'to be fulfilled')
|
expect(authentication.validateToken(...validateTokenArgs), 'to be fulfilled')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(accounts.current, 'to have a call satisfying', [
|
expect(accounts.getInfo, 'to have a call satisfying', [
|
||||||
{token: 'foo'}
|
user.id,
|
||||||
|
validToken,
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
it('should resolve with both tokens and user object', () =>
|
it('should resolve with both tokens and user object', () =>
|
||||||
expect(authentication.validateToken(validTokens), 'to be fulfilled with', {
|
expect(authentication.validateToken(...validateTokenArgs), 'to be fulfilled with', {
|
||||||
...validTokens,
|
token: validToken,
|
||||||
user
|
refreshToken: validRefreshToken,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
it('rejects if token has a bad type', () =>
|
it('rejects if token has a bad type', () =>
|
||||||
expect(authentication.validateToken({token: {}}),
|
expect(authentication.validateToken(user.id, {}),
|
||||||
'to be rejected with', 'token must be a string'
|
'to be rejected with', 'token must be a string'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
it('should allow empty refreshToken', () =>
|
it('should allow empty refreshToken', () =>
|
||||||
expect(authentication.validateToken({token: 'foo', refreshToken: null}), 'to be fulfilled')
|
expect(authentication.validateToken(user.id, 'foo', null), 'to be fulfilled')
|
||||||
);
|
);
|
||||||
|
|
||||||
it('rejects if accounts.current request is unexpectedly failed', () => {
|
it('rejects if accounts.getInfo request is unexpectedly failed', () => {
|
||||||
const error = 'Something wrong';
|
const error = 'Something wrong';
|
||||||
accounts.current.returns(Promise.reject(error));
|
accounts.getInfo.returns(Promise.reject(error));
|
||||||
|
|
||||||
return expect(authentication.validateToken(validTokens),
|
return expect(authentication.validateToken(...validateTokenArgs),
|
||||||
'to be rejected with', error
|
'to be rejected with', error
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -95,32 +121,42 @@ describe('authentication api', () => {
|
|||||||
message: 'Token expired',
|
message: 'Token expired',
|
||||||
code: 0,
|
code: 0,
|
||||||
status: 401,
|
status: 401,
|
||||||
type: 'yii\\web\\UnauthorizedHttpException'
|
type: 'yii\\web\\UnauthorizedHttpException',
|
||||||
};
|
};
|
||||||
const newToken = 'baz';
|
const newToken = 'baz';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sinon.stub(authentication, 'requestToken');
|
sinon.stub(authentication, 'requestToken');
|
||||||
|
|
||||||
accounts.current.onCall(0).returns(Promise.reject(expiredResponse));
|
accounts.getInfo.onCall(0).returns(Promise.reject(expiredResponse));
|
||||||
authentication.requestToken.returns(Promise.resolve({token: newToken}));
|
authentication.requestToken.returns(Promise.resolve(newToken));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
authentication.requestToken.restore();
|
authentication.requestToken.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves with new token and user object', () =>
|
it('resolves with new token and user object', async () => {
|
||||||
expect(authentication.validateToken(validTokens),
|
server.post('/api/authentication/refresh-token', {
|
||||||
'to be fulfilled with', {...validTokens, token: newToken, user}
|
access_token: newToken,
|
||||||
)
|
refresh_token: validRefreshToken,
|
||||||
);
|
success: true,
|
||||||
|
expires_in: 50000
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
await expect(authentication.validateToken(...validateTokenArgs),
|
||||||
|
'to be fulfilled with', {token: newToken, refreshToken: validRefreshToken, user}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(server.requests[0].requestBody, 'to equal', `refresh_token=${validRefreshToken}`);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects if token request failed', () => {
|
it('rejects if token request failed', () => {
|
||||||
const error = 'Something wrong';
|
const error = {error: 'Unexpected error example'};
|
||||||
authentication.requestToken.returns(Promise.reject(error));
|
server.post('/api/authentication/refresh-token', error, 500);
|
||||||
|
|
||||||
return expect(authentication.validateToken(validTokens),
|
return expect(authentication.validateToken(...validateTokenArgs),
|
||||||
'to be rejected with', error
|
'to be rejected with', error
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -132,32 +168,35 @@ describe('authentication api', () => {
|
|||||||
message: 'Incorrect token',
|
message: 'Incorrect token',
|
||||||
code: 0,
|
code: 0,
|
||||||
status: 401,
|
status: 401,
|
||||||
type: 'yii\\web\\UnauthorizedHttpException'
|
type: 'yii\\web\\UnauthorizedHttpException',
|
||||||
};
|
};
|
||||||
const newToken = 'baz';
|
const newToken = 'baz';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sinon.stub(authentication, 'requestToken');
|
accounts.getInfo.onCall(0).returns(Promise.reject(expiredResponse));
|
||||||
|
|
||||||
accounts.current.onCall(0).returns(Promise.reject(expiredResponse));
|
|
||||||
authentication.requestToken.returns(Promise.resolve({token: newToken}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
it('resolves with new token and user object', async () => {
|
||||||
authentication.requestToken.restore();
|
server.post('/api/authentication/refresh-token', {
|
||||||
});
|
access_token: newToken,
|
||||||
|
refresh_token: validRefreshToken,
|
||||||
|
success: true,
|
||||||
|
expires_in: 50000
|
||||||
|
});
|
||||||
|
|
||||||
it('resolves with new token and user object', () =>
|
|
||||||
expect(authentication.validateToken(validTokens),
|
await expect(authentication.validateToken(...validateTokenArgs),
|
||||||
'to be fulfilled with', {...validTokens, token: newToken, user}
|
'to be fulfilled with', {token: newToken, refreshToken: validRefreshToken, user}
|
||||||
)
|
);
|
||||||
);
|
|
||||||
|
expect(server.requests[0].requestBody, 'to equal', `refresh_token=${validRefreshToken}`);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects if token request failed', () => {
|
it('rejects if token request failed', () => {
|
||||||
const error = 'Something wrong';
|
const error = {error: 'Unexpected error example'};
|
||||||
authentication.requestToken.returns(Promise.reject(error));
|
server.post('/api/authentication/refresh-token', error, 500);
|
||||||
|
|
||||||
return expect(authentication.validateToken(validTokens),
|
return expect(authentication.validateToken(...validateTokenArgs),
|
||||||
'to be rejected with', error
|
'to be rejected with', error
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -190,7 +229,7 @@ describe('authentication api', () => {
|
|||||||
it('overrides token if provided', () => {
|
it('overrides token if provided', () => {
|
||||||
const token = 'foo';
|
const token = 'foo';
|
||||||
|
|
||||||
authentication.logout({token});
|
authentication.logout(token);
|
||||||
|
|
||||||
expect(request.post, 'to have a call satisfying', [
|
expect(request.post, 'to have a call satisfying', [
|
||||||
'/api/authentication/logout', {}, {token}
|
'/api/authentication/logout', {}, {token}
|
||||||
@ -241,7 +280,7 @@ describe('authentication api', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return expect(authentication.requestToken(refreshToken),
|
return expect(authentication.requestToken(refreshToken),
|
||||||
'to be fulfilled with', {token}
|
'to be fulfilled with', token,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import request from 'services/request';
|
|
||||||
import type { Resp } from 'services/request';
|
import type { Resp } from 'services/request';
|
||||||
|
import request from 'services/request';
|
||||||
|
|
||||||
export default {
|
export function getSecret(id: number): Promise<Resp<{
|
||||||
getSecret(): Promise<Resp<{qr: string, secret: string, uri: string}>> {
|
qr: string,
|
||||||
return request.get('/api/two-factor-auth');
|
secret: string,
|
||||||
},
|
uri: string,
|
||||||
|
}>> {
|
||||||
|
return request.get(`/api/v1/accounts/${id}/two-factor-auth`);
|
||||||
|
}
|
||||||
|
|
||||||
enable(data: {totp: string, password?: string}): Promise<Resp<*>> {
|
export function enable(id: number, totp: string, password?: string): Promise<Resp<*>> {
|
||||||
return request.post('/api/two-factor-auth', {
|
return request.post(`/api/v1/accounts/${id}/two-factor-auth`, {
|
||||||
totp: data.totp,
|
totp,
|
||||||
password: data.password || ''
|
password,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
disable(data: {totp: string, password?: string}): Promise<Resp<*>> {
|
export function disable(id: number, totp: string, password?: string): Promise<Resp<*>> {
|
||||||
return request.delete('/api/two-factor-auth', {
|
return request.delete(`/api/v1/accounts/${id}/two-factor-auth`, {
|
||||||
totp: data.totp,
|
totp,
|
||||||
password: data.password || ''
|
password,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user