#389: automatically revoke account, when user clicks back during re-login

This commit is contained in:
SleepWalker 2018-02-28 23:58:02 +02:00
parent f1d33bf7ec
commit 9f926e42bc
9 changed files with 103 additions and 23 deletions

View File

@ -29,7 +29,7 @@ type State = {
}, },
}; };
export { updateToken }; export { updateToken, activate };
/** /**
* @param {Account|object} account * @param {Account|object} account

View File

@ -34,6 +34,10 @@ export function getActiveAccount(state: { accounts: State }): ?Account {
return state.accounts.available.find((account) => account.id === accountId); return state.accounts.available.find((account) => account.id === accountId);
} }
export function getAvailableAccounts(state: { accounts: State }): Array<Account> {
return state.accounts.available;
}
export default function accounts( export default function accounts(
state: State = { state: State = {
active: null, active: null,

View File

@ -15,7 +15,12 @@ import { create as createPopup } from 'components/ui/popup/actions';
import ContactForm from 'components/contact/ContactForm'; import ContactForm from 'components/contact/ContactForm';
export { updateUser } from 'components/user/actions'; export { updateUser } from 'components/user/actions';
export { authenticate, logoutAll as logout } from 'components/accounts/actions'; export {
authenticate,
logoutAll as logout,
revoke as removeAccount,
activate as activateAccount
} from 'components/accounts/actions';
import { getCredentials } from './reducer'; import { getCredentials } from './reducer';
/** /**

View File

@ -25,6 +25,8 @@ type Request = {
type ActionId = type ActionId =
| 'updateUser' | 'updateUser'
| 'authenticate' | 'authenticate'
| 'activateAccount'
| 'removeAccount'
| 'logout' | 'logout'
| 'goBack' | 'goBack'
| 'redirect' | 'redirect'

View File

@ -1,6 +1,7 @@
// @flow // @flow
import logger from 'services/logger'; import logger from 'services/logger';
import { getCredentials } from 'components/auth/reducer'; import { getCredentials } from 'components/auth/reducer';
import { getActiveAccount, getAvailableAccounts } from 'components/accounts/reducer';
import AbstractState from './AbstractState'; import AbstractState from './AbstractState';
import ChooseAccountState from './ChooseAccountState'; import ChooseAccountState from './ChooseAccountState';
@ -63,10 +64,24 @@ export default class PasswordState extends AbstractState {
} }
goBack(context: AuthContext) { goBack(context: AuthContext) {
const { isRelogin } = getCredentials(context.getState()); const state = context.getState();
const { isRelogin } = getCredentials(state);
if (isRelogin) { if (isRelogin) {
const availableAccounts = getAvailableAccounts(state);
const accountToRemove = getActiveAccount(state);
if (availableAccounts.length === 1 || !accountToRemove) {
context.run('logout');
context.run('setLogin', null);
context.setState(new LoginState());
} else {
const accountToReplace = availableAccounts.find(({id}) => id !== accountToRemove.id);
context.run('activateAccount', accountToReplace);
context.run('removeAccount', accountToRemove);
context.setState(new ChooseAccountState()); context.setState(new ChooseAccountState());
}
} else { } else {
context.run('setLogin', null); context.run('setLogin', null);
context.setState(new LoginState()); context.setState(new LoginState());

View File

@ -173,6 +173,13 @@ describe('PasswordState', () => {
it('should transition to ChooseAccountState if this is relogin', () => { it('should transition to ChooseAccountState if this is relogin', () => {
context.getState.returns({ context.getState.returns({
accounts: {
active: 1,
available: [
{id: 1},
{id: 2}
]
},
auth: { auth: {
credentials: { credentials: {
login: 'foo', login: 'foo',
@ -181,9 +188,34 @@ describe('PasswordState', () => {
} }
}); });
expectRun(mock, 'activateAccount', { id: 2 });
expectRun(mock, 'removeAccount', { id: 1 });
expectState(mock, ChooseAccountState); expectState(mock, ChooseAccountState);
state.goBack(context); state.goBack(context);
}); });
it('should transition to LoginState if this is relogin and only one account available', () => {
context.getState.returns({
accounts: {
active: 1,
available: [
{id: 1},
]
},
auth: {
credentials: {
login: 'foo',
isRelogin: true,
}
}
});
expectRun(mock, 'logout');
expectRun(mock, 'setLogin', null);
expectState(mock, LoginState);
state.goBack(context);
});
}); });
}); });

View File

@ -7,6 +7,8 @@ import * as actions from 'components/auth/actions';
const availableActions = { const availableActions = {
updateUser: actions.updateUser, updateUser: actions.updateUser,
authenticate: actions.authenticate, authenticate: actions.authenticate,
activateAccount: actions.activateAccount,
removeAccount: actions.removeAccount,
logout: actions.logout, logout: actions.logout,
goBack: actions.goBack, goBack: actions.goBack,
redirect: actions.redirect, redirect: actions.redirect,

View File

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -1,15 +1,15 @@
import { account1, account2 } from '../fixtures/accounts.json'; import { account1, account2 } from '../fixtures/accounts.json';
const multiAccount
= '{"accounts":{"available":[{"id":7,"username":"SleepWalker","email":"danilenkos@auroraglobal.com","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4MDksImV4cCI6MTUxODM3NzQwOSwic3ViIjoiZWx5fDciLCJqdGkiOjM1NDh9.Fv4AbJ0iDbrH3bhbgF0ViJLfYYiwH78deR4fMlMhKrQ","refreshToken":"3gh6ZZ3R9jGeFdp0TmlY7sd0zBxH6Zfq48M86eUAv952RcAKx32RAnjlKkgd6i-MV-RKbjtADIdoRwMUWOYQjEYtwwXPjcQJ"},{"id":102,"username":"test","email":"admin@udf.su","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4NjUsImV4cCI6MTUxODM3NzQ2NSwic3ViIjoiZWx5fDEwMiIsImp0aSI6MzU0OX0.eJEgvXT3leGqBe3tYNGZb0E4WEvWfrLPjcD7eNjyQYO","refreshToken":"Al75SIx-LFOCP7kaqZBVqMVmSljJw9_bdFQGyuM64c6ShP7YsXbkCD8vPOundAwUDfRZqsIbOHUROmAHPB0VBfjLfw96yqxx"}],"active":102},"user":{"id":102,"uuid":"e49cafdc-6e0c-442d-b608-dacdb864ee34","username":"test","token":"","email":"admin@udf.su","maskedEmail":"","avatar":"","lang":"en","isActive":true,"isOtpEnabled":true,"shouldAcceptRules":false,"passwordChangedAt":1478961317,"hasMojangUsernameCollision":true,"isGuest":false,"registeredAt":1478961317,"elyProfileLink":"http://ely.by/u102","originalResponse":{}}}';
const singleAccount
= '{"accounts":{"available":[{"id":102,"username":"test","email":"admin@udf.su","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4NjUsImV4cCI6MTUxODM3NzQ2NSwic3ViIjoiZWx5fDEwMiIsImp0aSI6MzU0OX0.eJEgvXT3leGqBe3tYNGZb0E4WEvWfrLPjcD7eNjyQYO","refreshToken":"Al75SIx-LFOCP7kaqZBVqMVmSljJw9_bdFQGyuM64c6ShP7YsXbkCD8vPOundAwUDfRZqsIbOHUROmAHPB0VBfjLfw96yqxx"}],"active":102},"user":{"id":102,"uuid":"e49cafdc-6e0c-442d-b608-dacdb864ee34","username":"test","token":"","email":"admin@udf.su","maskedEmail":"","avatar":"","lang":"en","isActive":true,"isOtpEnabled":true,"shouldAcceptRules":false,"passwordChangedAt":1478961317,"hasMojangUsernameCollision":true,"isGuest":false,"registeredAt":1478961317,"elyProfileLink":"http://ely.by/u102","originalResponse":{}}}';
describe('when user\'s token and refreshToken are invalid', () => { describe('when user\'s token and refreshToken are invalid', () => {
beforeEach(() => beforeEach(() =>
cy cy
.visit('/') .visit('/')
.then(() => .then(() => localStorage.setItem('redux-storage', multiAccount))
localStorage.setItem(
'redux-storage',
'{"accounts":{"available":[{"id":7,"username":"SleepWalker","email":"danilenkos@auroraglobal.com","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4MDksImV4cCI6MTUxODM3NzQwOSwic3ViIjoiZWx5fDciLCJqdGkiOjM1NDh9.Fv4AbJ0iDbrH3bhbgF0ViJLfYYiwH78deR4fMlMhKrQ","refreshToken":"3gh6ZZ3R9jGeFdp0TmlY7sd0zBxH6Zfq48M86eUAv952RcAKx32RAnjlKkgd6i-MV-RKbjtADIdoRwMUWOYQjEYtwwXPjcQJ"},{"id":102,"username":"test","email":"admin@udf.su","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4NjUsImV4cCI6MTUxODM3NzQ2NSwic3ViIjoiZWx5fDEwMiIsImp0aSI6MzU0OX0.eJEgvXT3leGqBe3tYNGZb0E4WEvWfrLPjcD7eNjyQYO","refreshToken":"Al75SIx-LFOCP7kaqZBVqMVmSljJw9_bdFQGyuM64c6ShP7YsXbkCD8vPOundAwUDfRZqsIbOHUROmAHPB0VBfjLfw96yqxx"}],"active":102},"user":{"id":102,"uuid":"e49cafdc-6e0c-442d-b608-dacdb864ee34","username":"test","token":"","email":"admin@udf.su","maskedEmail":"","avatar":"","lang":"en","isActive":true,"isOtpEnabled":true,"shouldAcceptRules":false,"passwordChangedAt":1478961317,"hasMojangUsernameCollision":true,"isGuest":false,"registeredAt":1478961317,"elyProfileLink":"http://ely.by/u102","originalResponse":{}}}'
)
)
); );
it('should ask for password', () => { it('should ask for password', () => {
@ -31,6 +31,11 @@ describe('when user\'s token and refreshToken are invalid', () => {
cy.url().should('include', '/choose-account'); cy.url().should('include', '/choose-account');
cy
.get('[data-e2e-content]')
.contains(account2.email)
.should('not.exist');
cy cy
.get('[data-e2e-content]') .get('[data-e2e-content]')
.contains(account1.username) .contains(account1.username)
@ -40,14 +45,36 @@ describe('when user\'s token and refreshToken are invalid', () => {
cy.contains('account preferences'); cy.contains('account preferences');
}); });
it('it should redirect to login, when one account and clicking back', () => {
cy
.url()
.should(() => localStorage.setItem('redux-storage', singleAccount));
cy.visit('/');
cy.get('[data-e2e-go-back]').click();
cy.url().should('include', '/login');
cy.get('[data-e2e-toolbar]').contains('Join');
});
it('should allow logout', () => { it('should allow logout', () => {
cy.visit('/'); cy.visit('/');
cy.get('[data-e2e-toolbar]').contains(account2.username).click(); cy
cy.get('[data-e2e-toolbar]').contains('Log out').click(); .get('[data-e2e-toolbar]')
.contains(account2.username)
.click();
cy
.get('[data-e2e-toolbar]')
.contains('Log out')
.click();
cy.contains(account2.email).should('not.exist'); cy.contains(account2.email).should('not.exist');
cy.get('[data-e2e-toolbar]').contains(account2.username).should('not.exist'); cy
.get('[data-e2e-toolbar]')
.contains(account2.username)
.should('not.exist');
}); });
it('should allow enter new login from choose account', () => { it('should allow enter new login from choose account', () => {
@ -98,12 +125,12 @@ describe('when user\'s token and refreshToken are invalid', () => {
cy cy
.get('[data-e2e-content]') .get('[data-e2e-content]')
.contains(account2.username) .contains(account1.username)
.click(); .click();
cy.url().should('include', '/password'); cy.url().should('include', '/password');
cy.get('[name="password"]').type(`${account2.password}{enter}`); cy.get('[name="password"]').type(`${account1.password}{enter}`);
cy.location('pathname', { timeout: 15000 }).should('eq', '/'); cy.location('pathname', { timeout: 15000 }).should('eq', '/');
cy.contains('account preferences'); cy.contains('account preferences');
@ -133,9 +160,7 @@ describe('when user\'s token and refreshToken are invalid', () => {
cy.contains('[type=submit]', 'Log into another account').click(); cy.contains('[type=submit]', 'Log into another account').click();
cy.contains('a', 'Create new account').click(); cy.contains('a', 'Create new account').click();
cy cy.get('@fetch').should('be.calledWith', '/api/options');
.get('@fetch')
.should('be.calledWith', '/api/options');
cy.url().should('contain', '/register'); cy.url().should('contain', '/register');
}); });