mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-20 07:02:58 +05:30
#104: add AcceptRules auth panel
This commit is contained in:
parent
7ab904d52a
commit
cb1a4b7d55
@ -33,6 +33,7 @@ const contexts = [
|
|||||||
['login', 'password', 'forgotPassword', 'recoverPassword'],
|
['login', 'password', 'forgotPassword', 'recoverPassword'],
|
||||||
['register', 'activation', 'resendActivation'],
|
['register', 'activation', 'resendActivation'],
|
||||||
['changePassword'],
|
['changePassword'],
|
||||||
|
['acceptRules'],
|
||||||
['permissions']
|
['permissions']
|
||||||
];
|
];
|
||||||
|
|
||||||
|
15
src/components/auth/README.md
Normal file
15
src/components/auth/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# How to add new auth panel
|
||||||
|
|
||||||
|
To add new panel you need to:
|
||||||
|
|
||||||
|
* add new state to `services/authFlow` and coresponding test to `tests/services/authFlow`
|
||||||
|
* connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and `services/authFlow/AuthFlow.functional.test` (the last one for some complex flow)
|
||||||
|
* add new actions to `components/auth/actions` and api endpoints to `services/api`
|
||||||
|
* create panel component at `components/auth/[panelId]`
|
||||||
|
* add new context in `components/auth/PanelTransition`
|
||||||
|
* connect component to `routes`
|
||||||
|
* whatever else you need
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
This flow must be simplified
|
6
src/components/auth/acceptRules/AcceptRules.intl.json
Normal file
6
src/components/auth/acceptRules/AcceptRules.intl.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"title": "Accept new rules",
|
||||||
|
"accept": "Accept",
|
||||||
|
"decline": "Decline",
|
||||||
|
"description": "We have updated our {link}. In order to get access to accounts.ely.by service, you need to accept them."
|
||||||
|
}
|
17
src/components/auth/acceptRules/AcceptRules.jsx
Normal file
17
src/components/auth/acceptRules/AcceptRules.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import factory from 'components/auth/factory';
|
||||||
|
|
||||||
|
import Body from './AcceptRulesBody';
|
||||||
|
import messages from './AcceptRules.intl.json';
|
||||||
|
|
||||||
|
export default factory({
|
||||||
|
title: messages.title,
|
||||||
|
body: Body,
|
||||||
|
footer: {
|
||||||
|
color: 'darkBlue',
|
||||||
|
autoFocus: true,
|
||||||
|
label: messages.accept
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
label: messages.decline
|
||||||
|
}
|
||||||
|
});
|
38
src/components/auth/acceptRules/AcceptRulesBody.jsx
Normal file
38
src/components/auth/acceptRules/AcceptRulesBody.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage as Message } from 'react-intl';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
import icons from 'components/ui/icons.scss';
|
||||||
|
import BaseAuthBody from 'components/auth/BaseAuthBody';
|
||||||
|
import registerMessages from 'components/auth/register/Register.intl.json';
|
||||||
|
|
||||||
|
import styles from './acceptRules.scss';
|
||||||
|
import messages from './AcceptRules.intl.json';
|
||||||
|
|
||||||
|
export default class AcceptRulesBody extends BaseAuthBody {
|
||||||
|
static displayName = 'AcceptRulesBody';
|
||||||
|
static panelId = 'acceptRules';
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.renderErrors()}
|
||||||
|
|
||||||
|
<div className={styles.security}>
|
||||||
|
<span className={icons.lock} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={styles.descriptionText}>
|
||||||
|
<Message {...messages.description} values={{
|
||||||
|
link: (
|
||||||
|
<Link to="/rules" target="_blank">
|
||||||
|
<Message {...registerMessages.termsOfService} />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
16
src/components/auth/acceptRules/acceptRules.scss
Normal file
16
src/components/auth/acceptRules/acceptRules.scss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@import '~components/ui/colors.scss';
|
||||||
|
|
||||||
|
.descriptionText {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: вынести иконки такого типа в какую-то внешнюю структуру?
|
||||||
|
.security {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 90px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { routeActions } from 'react-router-redux';
|
import { routeActions } from 'react-router-redux';
|
||||||
|
|
||||||
import { updateUser, logout as logoutUser, changePassword as changeUserPassword, authenticate } from 'components/user/actions';
|
import { updateUser, logout as logoutUser, changePassword as changeUserPassword, acceptRules as userAcceptRules, authenticate } from 'components/user/actions';
|
||||||
import authentication from 'services/api/authentication';
|
import authentication 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';
|
||||||
@ -46,6 +46,13 @@ export function changePassword({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function acceptRules() {
|
||||||
|
return wrapInLoader((dispatch) =>
|
||||||
|
dispatch(userAcceptRules())
|
||||||
|
.catch(validationErrorsHandler(dispatch))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function forgotPassword({
|
export function forgotPassword({
|
||||||
login = ''
|
login = ''
|
||||||
}) {
|
}) {
|
||||||
|
@ -4,7 +4,7 @@ const KEY_USER = 'user';
|
|||||||
|
|
||||||
export default class User {
|
export default class User {
|
||||||
/**
|
/**
|
||||||
* @param {Object|string|undefined} data plain object or jwt token or empty to load from storage
|
* @param {object|string|undefined} data plain object or jwt token or empty to load from storage
|
||||||
*
|
*
|
||||||
* @return {User}
|
* @return {User}
|
||||||
*/
|
*/
|
||||||
@ -30,6 +30,7 @@ export default class User {
|
|||||||
goal: null, // the goal with wich user entered site
|
goal: null, // the goal with wich user entered site
|
||||||
isGuest: true,
|
isGuest: true,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
|
shouldAcceptRules: false, // whether user need to review updated rules
|
||||||
shouldChangePassword: false, // TODO: нужно ещё пробросить причину необходимости смены
|
shouldChangePassword: false, // TODO: нужно ещё пробросить причину необходимости смены
|
||||||
passwordChangedAt: null,
|
passwordChangedAt: null,
|
||||||
hasMojangUsernameCollision: false,
|
hasMojangUsernameCollision: false,
|
||||||
|
@ -89,6 +89,19 @@ export function changePassword({
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function acceptRules() {
|
||||||
|
return (dispatch) =>
|
||||||
|
accounts.acceptRules()
|
||||||
|
.then((resp) => {
|
||||||
|
dispatch(updateUser({
|
||||||
|
shouldAcceptRules: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
let middlewareAdded = false;
|
let middlewareAdded = false;
|
||||||
export function authenticate(token, refreshToken) { // TODO: this action, probably, belongs to components/auth
|
export function authenticate(token, refreshToken) { // TODO: this action, probably, belongs to components/auth
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
@ -21,6 +21,7 @@ import Activation from 'components/auth/activation/Activation';
|
|||||||
import ResendActivation from 'components/auth/resendActivation/ResendActivation';
|
import ResendActivation from 'components/auth/resendActivation/ResendActivation';
|
||||||
import Password from 'components/auth/password/Password';
|
import Password from 'components/auth/password/Password';
|
||||||
import ChangePassword from 'components/auth/changePassword/ChangePassword';
|
import ChangePassword from 'components/auth/changePassword/ChangePassword';
|
||||||
|
import AcceptRules from 'components/auth/acceptRules/AcceptRules';
|
||||||
import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
|
import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
|
||||||
import RecoverPassword from 'components/auth/recoverPassword/RecoverPassword';
|
import RecoverPassword from 'components/auth/recoverPassword/RecoverPassword';
|
||||||
import Finish from 'components/auth/finish/Finish';
|
import Finish from 'components/auth/finish/Finish';
|
||||||
@ -60,6 +61,7 @@ export default function routesFactory(store) {
|
|||||||
<Route path="/resend-activation" components={new ResendActivation()} {...startAuthFlow} />
|
<Route path="/resend-activation" components={new ResendActivation()} {...startAuthFlow} />
|
||||||
<Route path="/oauth/permissions" components={new Permissions()} {...startAuthFlow} />
|
<Route path="/oauth/permissions" components={new Permissions()} {...startAuthFlow} />
|
||||||
<Route path="/oauth/finish" component={Finish} {...startAuthFlow} />
|
<Route path="/oauth/finish" component={Finish} {...startAuthFlow} />
|
||||||
|
<Route path="/accept-rules" components={new AcceptRules()} {...startAuthFlow} />
|
||||||
<Route path="/change-password" components={new ChangePassword()} {...startAuthFlow} />
|
<Route path="/change-password" components={new ChangePassword()} {...startAuthFlow} />
|
||||||
<Route path="/forgot-password" components={new ForgotPassword()} {...startAuthFlow} />
|
<Route path="/forgot-password" components={new ForgotPassword()} {...startAuthFlow} />
|
||||||
<Route path="/recover-password(/:key)" components={new RecoverPassword()} {...startAuthFlow} />
|
<Route path="/recover-password(/:key)" components={new RecoverPassword()} {...startAuthFlow} />
|
||||||
|
@ -17,6 +17,10 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptRules() {
|
||||||
|
return request.post('/api/accounts/accept-rules');
|
||||||
|
},
|
||||||
|
|
||||||
changeUsername({
|
changeUsername({
|
||||||
username = '',
|
username = '',
|
||||||
password = ''
|
password = ''
|
||||||
|
23
src/services/authFlow/AcceptRulesState.js
Normal file
23
src/services/authFlow/AcceptRulesState.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import AbstractState from './AbstractState';
|
||||||
|
import CompleteState from './CompleteState';
|
||||||
|
|
||||||
|
export default class AcceptRulesState extends AbstractState {
|
||||||
|
enter(context) {
|
||||||
|
const {user} = context.getState();
|
||||||
|
|
||||||
|
if (user.shouldAcceptRules) {
|
||||||
|
context.navigate('/accept-rules');
|
||||||
|
} else {
|
||||||
|
context.setState(new CompleteState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(context) {
|
||||||
|
context.run('acceptRules')
|
||||||
|
.then(() => context.setState(new CompleteState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(context) {
|
||||||
|
context.run('logout');
|
||||||
|
}
|
||||||
|
}
|
@ -143,6 +143,7 @@ export default class AuthFlow {
|
|||||||
case '/':
|
case '/':
|
||||||
case '/login':
|
case '/login':
|
||||||
case '/password':
|
case '/password':
|
||||||
|
case '/accept-rules':
|
||||||
case '/change-password':
|
case '/change-password':
|
||||||
case '/oauth/permissions':
|
case '/oauth/permissions':
|
||||||
case '/oauth/finish':
|
case '/oauth/finish':
|
||||||
|
@ -3,6 +3,7 @@ import LoginState from './LoginState';
|
|||||||
import PermissionsState from './PermissionsState';
|
import PermissionsState from './PermissionsState';
|
||||||
import ActivationState from './ActivationState';
|
import ActivationState from './ActivationState';
|
||||||
import ChangePasswordState from './ChangePasswordState';
|
import ChangePasswordState from './ChangePasswordState';
|
||||||
|
import AcceptRulesState from './AcceptRulesState';
|
||||||
import FinishState from './FinishState';
|
import FinishState from './FinishState';
|
||||||
|
|
||||||
export default class CompleteState extends AbstractState {
|
export default class CompleteState extends AbstractState {
|
||||||
@ -19,6 +20,8 @@ export default class CompleteState extends AbstractState {
|
|||||||
context.setState(new LoginState());
|
context.setState(new LoginState());
|
||||||
} else if (!user.isActive) {
|
} else if (!user.isActive) {
|
||||||
context.setState(new ActivationState());
|
context.setState(new ActivationState());
|
||||||
|
} else if (user.shouldAcceptRules) {
|
||||||
|
context.setState(new AcceptRulesState());
|
||||||
} else if (user.shouldChangePassword) {
|
} else if (user.shouldChangePassword) {
|
||||||
context.setState(new ChangePasswordState());
|
context.setState(new ChangePasswordState());
|
||||||
} else if (auth.oauth && auth.oauth.clientId) {
|
} else if (auth.oauth && auth.oauth.clientId) {
|
||||||
|
88
tests/services/authFlow/AcceptRulesState.test.js
Normal file
88
tests/services/authFlow/AcceptRulesState.test.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import AcceptRulesState from 'services/authFlow/AcceptRulesState';
|
||||||
|
import CompleteState from 'services/authFlow/CompleteState';
|
||||||
|
|
||||||
|
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||||
|
|
||||||
|
describe('AcceptRulesState', () => {
|
||||||
|
let state;
|
||||||
|
let context;
|
||||||
|
let mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = new AcceptRulesState();
|
||||||
|
|
||||||
|
const data = bootstrap();
|
||||||
|
context = data.context;
|
||||||
|
mock = data.mock;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#enter', () => {
|
||||||
|
it('should navigate to /accept-rules', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
user: {
|
||||||
|
shouldAcceptRules: true,
|
||||||
|
isGuest: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expectNavigate(mock, '/accept-rules');
|
||||||
|
|
||||||
|
state.enter(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition to complete state if rules accepted', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
user: {
|
||||||
|
shouldAcceptRules: false,
|
||||||
|
isGuest: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expectState(mock, CompleteState);
|
||||||
|
|
||||||
|
state.enter(context);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#resolve', () => {
|
||||||
|
it('should call acceptRules', () => {
|
||||||
|
expectRun(mock, 'acceptRules').returns({then() {}});
|
||||||
|
|
||||||
|
state.resolve(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition to complete state on success', () => {
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
|
||||||
|
mock.expects('run').returns(promise);
|
||||||
|
expectState(mock, CompleteState);
|
||||||
|
|
||||||
|
state.resolve(context);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT transition to complete state on fail', () => {
|
||||||
|
const promise = Promise.reject();
|
||||||
|
|
||||||
|
mock.expects('run').returns(promise);
|
||||||
|
mock.expects('setState').never();
|
||||||
|
|
||||||
|
state.resolve(context);
|
||||||
|
|
||||||
|
return promise.catch(mock.verify.bind(mock));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#reject', () => {
|
||||||
|
it('should logout', () => {
|
||||||
|
expectRun(mock, 'logout');
|
||||||
|
|
||||||
|
state.reject(context);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -5,6 +5,7 @@ import AbstractState from 'services/authFlow/AbstractState';
|
|||||||
|
|
||||||
import OAuthState from 'services/authFlow/OAuthState';
|
import OAuthState from 'services/authFlow/OAuthState';
|
||||||
import RegisterState from 'services/authFlow/RegisterState';
|
import RegisterState from 'services/authFlow/RegisterState';
|
||||||
|
import AcceptRulesState from 'services/authFlow/AcceptRulesState';
|
||||||
import RecoverPasswordState from 'services/authFlow/RecoverPasswordState';
|
import RecoverPasswordState from 'services/authFlow/RecoverPasswordState';
|
||||||
import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
|
import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
|
||||||
import ActivationState from 'services/authFlow/ActivationState';
|
import ActivationState from 'services/authFlow/ActivationState';
|
||||||
@ -178,6 +179,7 @@ describe('AuthFlow', () => {
|
|||||||
'/login': LoginState,
|
'/login': LoginState,
|
||||||
'/password': LoginState,
|
'/password': LoginState,
|
||||||
'/change-password': LoginState,
|
'/change-password': LoginState,
|
||||||
|
'/accept-rules': LoginState,
|
||||||
'/oauth/permissions': LoginState,
|
'/oauth/permissions': LoginState,
|
||||||
'/oauth/finish': LoginState,
|
'/oauth/finish': LoginState,
|
||||||
'/oauth2/v1': OAuthState,
|
'/oauth2/v1': OAuthState,
|
||||||
|
@ -4,6 +4,7 @@ import CompleteState from 'services/authFlow/CompleteState';
|
|||||||
import LoginState from 'services/authFlow/LoginState';
|
import LoginState from 'services/authFlow/LoginState';
|
||||||
import ActivationState from 'services/authFlow/ActivationState';
|
import ActivationState from 'services/authFlow/ActivationState';
|
||||||
import ChangePasswordState from 'services/authFlow/ChangePasswordState';
|
import ChangePasswordState from 'services/authFlow/ChangePasswordState';
|
||||||
|
import AcceptRulesState from 'services/authFlow/AcceptRulesState';
|
||||||
import FinishState from 'services/authFlow/FinishState';
|
import FinishState from 'services/authFlow/FinishState';
|
||||||
import PermissionsState from 'services/authFlow/PermissionsState';
|
import PermissionsState from 'services/authFlow/PermissionsState';
|
||||||
|
|
||||||
@ -96,6 +97,35 @@ describe('CompleteState', () => {
|
|||||||
state.enter(context);
|
state.enter(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should transition to accept-rules if shouldAcceptRules', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
user: {
|
||||||
|
shouldAcceptRules: true,
|
||||||
|
isActive: true,
|
||||||
|
isGuest: false
|
||||||
|
},
|
||||||
|
auth: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expectState(mock, AcceptRulesState);
|
||||||
|
|
||||||
|
state.enter(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition to activation with higher priority than shouldAcceptRules', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
user: {
|
||||||
|
shouldAcceptRules: true,
|
||||||
|
isGuest: false
|
||||||
|
},
|
||||||
|
auth: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expectState(mock, ActivationState);
|
||||||
|
|
||||||
|
state.enter(context);
|
||||||
|
});
|
||||||
|
|
||||||
it('should transition to finish state if code is present', () => {
|
it('should transition to finish state if code is present', () => {
|
||||||
context.getState.returns({
|
context.getState.returns({
|
||||||
user: {
|
user: {
|
||||||
|
Loading…
Reference in New Issue
Block a user