Merged in remember-me-frontend (pull request #25)

Remember me frontend
This commit is contained in:
Ivan Diaz 2016-07-25 21:19:30 -03:00
commit 8fa096f662
13 changed files with 421 additions and 78 deletions

View File

@ -55,6 +55,7 @@
"app-module-path": "^1.0.3",
"classnames": "^2.1.3",
"jquery": "^2.1.4",
"localStorage": "^1.0.3",
"lodash": "^3.10.0",
"messageformat": "^0.2.2",
"react": "^15.0.1",
@ -63,7 +64,6 @@
"react-google-recaptcha": "^0.5.2",
"react-motion": "^0.3.0",
"react-router": "^2.4.0",
"reflux": "^0.4.1",
"sessionstorage": "0.0.1"
"reflux": "^0.4.1"
}
}

View File

@ -1,6 +1,7 @@
import React from 'react';
import {render} from 'react-dom'
import Router from 'react-router';
import UserStore from 'stores/user-store';
import routes from './Routes';
@ -13,4 +14,9 @@ if (noFixtures === 'disabled') {
require('lib-app/fixtures-loader');
}
render(routes, document.getElementById('app'));
let onSessionInit = function () {
render(routes, document.getElementById('app'));
};
UserStore.initSession().then(onSessionInit, onSessionInit);

View File

@ -14,12 +14,12 @@ const DashboardLayout = React.createClass({
},
render() {
return (
return (UserStore.isLoggedIn()) ? (
<div>
<div><DashboardMenu location={this.props.location} /></div>
<div>{this.props.children}</div>
</div>
);
) : null;
}
});

View File

@ -5,19 +5,21 @@ module.exports = [
response: function (data) {
let response;
if (data.password === 'invalid') {
response = {
status: 'fail',
message: 'Invalid Credientals'
};
} else {
if (data.password === 'valid' || (data.rememberToken === 'aa41efe0a1b3eeb9bf303e4561ff8392' && data.userId === 12)) {
response = {
status: 'success',
data: {
'userId': 12,
'token': 'cc6b4921e6733d6aafe284ec0d7be57e'
'token': 'cc6b4921e6733d6aafe284ec0d7be57e',
'rememberToken': (data.remember) ? 'aa41efe0a1b3eeb9bf303e4561ff8392' : null,
'rememberExpiration': (data.remember) ? 2018 : 0
}
};
} else {
response = {
status: 'fail',
message: 'Invalid Credientals'
};
}
return response;
@ -25,12 +27,24 @@ module.exports = [
},
{
path: 'user/logout',
time: 1000,
time: 100,
response: function () {
return {
status: 'success',
data: {}
};
}
},
{
path: 'user/check-session',
time: 100,
response: function () {
return {
status: 'success',
data: {
sessionActive: true
}
};
}
}
];

View File

@ -1,3 +1,5 @@
export default {
call: stub()
call: stub().returns(new Promise(function (resolve) {
resolve();
}))
};

View File

@ -1,6 +1,10 @@
export default {
createSession: stub(),
getSessionData: stub().returns({}),
clearRememberData: stub(),
storeRememberData: stub(),
getRememberData: stub(),
isRememberDataExpired: stub().returns(false),
isLoggedIn: stub().returns(false),
closeSession: stub()
};

View File

@ -0,0 +1,110 @@
const LocalStorageMock = {
getItem: stub(),
setItem: stub(),
removeItem: stub()
};
const date = { getCurrentDate: stub().returns(20160505)};
const sessionStore = requireUnit('lib-app/session-store', {
'localStorage': LocalStorageMock,
'lib-app/date': date
});
describe('sessionStore library', function () {
beforeEach(function () {
LocalStorageMock.getItem = stub();
LocalStorageMock.setItem = stub();
LocalStorageMock.removeItem = stub();
});
it('should get, set and remove items from LocalStorage', function () {
sessionStore.getItem('SOME_KEY');
expect(LocalStorageMock.getItem).to.have.been.calledWith('SOME_KEY');
sessionStore.setItem('SOME_KEY', 'SOME_VALUE');
expect(LocalStorageMock.setItem).to.have.been.calledWith('SOME_KEY', 'SOME_VALUE');
sessionStore.removeItem('SOME_KEY');
expect(LocalStorageMock.removeItem).to.have.been.calledWith('SOME_KEY');
});
it('should create session correctly', function () {
sessionStore.createSession(14, 'TOKEN');
expect(LocalStorageMock.setItem).to.have.been.calledWith('userId', 14);
expect(LocalStorageMock.setItem).to.have.been.calledWith('token', 'TOKEN');
});
it('should return session data', function () {
LocalStorageMock.getItem = function (key) {
if (key === 'userId') return 'USER_ID';
if (key === 'token') return 'TOKEN';
};
let sessionData = sessionStore.getSessionData();
expect(sessionData.userId).to.equal('USER_ID');
expect(sessionData.token).to.equal('TOKEN');
LocalStorageMock.getItem = stub().returns('ITEM');
});
it('should inform if it is logged in', function () {
LocalStorageMock.getItem = stub().returns('TOKEN');
expect(sessionStore.isLoggedIn()).to.equal(true);
LocalStorageMock.getItem = stub().returns(null);
expect(sessionStore.isLoggedIn()).to.equal(false);
});
it('should clear session data if session is closed', function () {
sessionStore.closeSession();
expect(LocalStorageMock.removeItem).to.have.been.calledWith('userId');
expect(LocalStorageMock.removeItem).to.have.been.calledWith('token');
});
it('should store remember data', function () {
sessionStore.storeRememberData({
token: 'SOME_TOKEN',
userId: 12,
expiration: 20160623
});
expect(LocalStorageMock.setItem).to.have.been.calledWith('rememberData-token', 'SOME_TOKEN');
expect(LocalStorageMock.setItem).to.have.been.calledWith('rememberData-userId', 12);
expect(LocalStorageMock.setItem).to.have.been.calledWith('rememberData-expiration', 20160623);
});
it('should inform if remember expired', function () {
LocalStorageMock.getItem = (key) => (key === 'rememberData-expiration') ? 20160505 : null;
date.getCurrentDate.returns(20160506);
expect(sessionStore.isRememberDataExpired()).to.equal(true);
LocalStorageMock.getItem = (key) => (key === 'rememberData-expiration') ? 20160505 : null;
date.getCurrentDate.returns(20160503);
expect(sessionStore.isRememberDataExpired()).to.equal(false);
});
it('should return all remember data', function () {
LocalStorageMock.getItem = function (key) {
if (key === 'rememberData-userId') return 'USER_ID';
if (key === 'rememberData-token') return 'TOKEN';
if (key === 'rememberData-expiration') return 'EXPIRATION';
};
let rememberData = sessionStore.getRememberData();
expect(rememberData.userId).to.equal('USER_ID');
expect(rememberData.token).to.equal('TOKEN');
expect(rememberData.expiration).to.equal('EXPIRATION');
LocalStorageMock.getItem = stub().returns('ITEM');
});
it('should clear remember data', function () {
sessionStore.clearRememberData();
expect(LocalStorageMock.removeItem).to.have.been.calledWith('rememberData-userId');
expect(LocalStorageMock.removeItem).to.have.been.calledWith('rememberData-token');
expect(LocalStorageMock.removeItem).to.have.been.calledWith('rememberData-expiration');
});
});

View File

@ -9,13 +9,17 @@ function processData (data) {
}
module.exports = {
call: function ({path, data, onSuccess, onFail}) {
APIUtils.post(root + path, processData(data)).then(function (result) {
if (result.status === 'success') {
onSuccess && onSuccess(result);
} else {
onFail && onFail(result);
}
call: function ({path, data}) {
return new Promise(function (resolve, reject) {
APIUtils.post(root + path, processData(data)).then(function (result) {
console.log(result);
if (result.status === 'success') {
resolve(result);
} else if (reject) {
reject(result);
}
});
});
}
};

View File

@ -0,0 +1,10 @@
export default {
getCurrentDate() {
let date = new Date();
let yyyy = date.getFullYear().toString();
let mm = (date.getMonth()+1).toString(); // getMonth() is zero-based
let dd = date.getDate().toString();
return (yyyy + (mm[1]?mm:"0"+mm[0]) + (dd[1]?dd:"0"+dd[0])) * 1;
}
}

View File

@ -1,35 +1,73 @@
import SessionStorage from 'sessionstorage';
import LocalStorage from 'localStorage';
import date from 'lib-app/date';
class SessionStore {
static initialize() {
if (!SessionStorage.getItem('language')) {
SessionStorage.setItem('language', 'english');
constructor() {
this.storage = LocalStorage;
if (!this.getItem('language')) {
this.setItem('language', 'english');
}
}
static createSession(userId, token) {
SessionStorage.setItem('userId', userId);
SessionStorage.setItem('token', token);
createSession(userId, token) {
this.setItem('userId', userId);
this.setItem('token', token);
}
static getSessionData() {
getSessionData() {
return {
userId: SessionStorage.getItem('userId'),
token: SessionStorage.getItem('token')
userId: this.getItem('userId'),
token: this.getItem('token')
};
}
static isLoggedIn() {
return !!SessionStorage.getItem('userId');
isLoggedIn() {
return !!this.getItem('token');
}
static closeSession() {
SessionStorage.removeItem('userId');
SessionStorage.removeItem('token');
closeSession() {
this.removeItem('userId');
this.removeItem('token');
}
storeRememberData({token, userId, expiration}) {
this.setItem('rememberData-token', token);
this.setItem('rememberData-userId', userId);
this.setItem('rememberData-expiration', expiration);
}
isRememberDataExpired() {
let rememberData = this.getRememberData();
return rememberData.expiration < date.getCurrentDate();
}
getRememberData() {
return {
token: this.getItem('rememberData-token'),
userId: this.getItem('rememberData-userId'),
expiration: this.getItem('rememberData-expiration')
};
}
clearRememberData() {
this.removeItem('rememberData-token');
this.removeItem('rememberData-userId');
this.removeItem('rememberData-expiration');
}
getItem(key) {
return this.storage.getItem(key);
}
setItem(key, value) {
return this.storage.setItem(key, value);
}
removeItem(key) {
this.storage.removeItem(key);
}
}
SessionStore.initialize();
export default SessionStore;
export default new SessionStore();

View File

@ -16,6 +16,13 @@ const UserStore = requireUnit('stores/user-store', {
});
describe('UserStore', function () {
it ('should inform is the user is logged based on SessionStores\' info', function () {
SessionStore.isLoggedIn.returns(true);
expect(UserStore.isLoggedIn()).to.equal(true);
SessionStore.isLoggedIn.returns(false);
expect(UserStore.isLoggedIn()).to.equal(false);
});
describe('when login user', function () {
it('should call /user/login api path', function () {
let mockLoginData = {email: 'mock', password: 'mock'};
@ -23,9 +30,7 @@ describe('UserStore', function () {
UserStore.loginUser(mockLoginData);
expect(API.call).to.have.been.calledWith({
path: 'user/login',
data: mockLoginData,
onSuccess: sinon.match.func,
onFail: sinon.match.func
data: mockLoginData
});
});
@ -42,10 +47,13 @@ describe('UserStore', function () {
spy(UserStore, 'trigger');
CommonActions.logged.reset();
SessionStore.createSession.reset();
API.call = ({onSuccess}) => {onSuccess(mockSuccessData)};
API.call.returns({
then: (resolve) => {resolve(mockSuccessData)}
});
UserStore.loginUser(mockLoginData);
expect(SessionStore.storeRememberData).to.have.not.been.called;
expect(SessionStore.createSession).to.have.been.calledWith(12, 'RANDOM_TOKEN');
expect(UserStore.trigger).to.have.been.calledWith('LOGIN_SUCCESS');
expect(CommonActions.logged).to.have.been.called;
@ -54,38 +62,68 @@ describe('UserStore', function () {
it('should trigger fail event if login fails', function () {
let mockLoginData = {email: 'mock', password: 'mock'};
let mockSuccessData = {
status: 'success',
data: {
userId: 12,
token: 'RANDOM_TOKEN'
}
};
spy(UserStore, 'trigger');
API.call = ({onFail}) => {onFail(mockSuccessData)};
API.call.returns({
then: (resolve, reject) => {reject()}
});
UserStore.loginUser(mockLoginData);
expect(UserStore.trigger).to.have.been.calledWith('LOGIN_FAIL');
UserStore.trigger.restore();
});
it('should store remember data if remember is true', function () {
let mockLoginData = {email: 'mock', password: 'mock', remember: true};
let mockSuccessData = {
status: 'success',
data: {
userId: 12,
token: 'RANDOM_TOKEN',
rememberToken: 'RANDOM_TOKEN_2',
rememberExpiration: 20150822
}
};
spy(UserStore, 'trigger');
CommonActions.logged.reset();
SessionStore.createSession.reset();
API.call.returns({
then: (resolve) => {resolve(mockSuccessData)}
});
UserStore.loginUser(mockLoginData);
expect(SessionStore.storeRememberData).to.have.been.calledWith({
token: 'RANDOM_TOKEN_2',
userId: 12,
expiration: 20150822
});
expect(SessionStore.createSession).to.have.been.calledWith(12, 'RANDOM_TOKEN');
expect(UserStore.trigger).to.have.been.calledWith('LOGIN_SUCCESS');
expect(CommonActions.logged).to.have.been.called;
UserStore.trigger.restore();
});
});
describe('when login out', function () {
it('should call /user/logout api path', function () {
API.call = stub();
API.call = stub().returns({
then: (resolve) => {resolve()}
});
UserStore.logoutUser();
expect(API.call).to.have.been.calledWith({
path: 'user/logout',
onSuccess: sinon.match.func
path: 'user/logout'
});
});
it('should delete session, trigger LOGOUT event and inform common action of logout', function () {
API.call = ({onSuccess}) => {onSuccess()};
API.call = stub().returns({
then: (resolve) => {resolve()}
});
spy(UserStore, 'trigger');
UserStore.logoutUser();
@ -96,10 +134,69 @@ describe('UserStore', function () {
})
});
it ('should inform is the user is logged based on SessionStores\' info', function () {
SessionStore.isLoggedIn.returns(true);
expect(UserStore.isLoggedIn()).to.equal(true);
SessionStore.isLoggedIn.returns(false);
expect(UserStore.isLoggedIn()).to.equal(false);
});
describe('when calling initSession', function () {{
it('should check if session is active in the API', function () {
let mockSuccessData = {
status: 'success',
data: {
sessionActive: true
}
};
API.call = stub().returns({
then: (resolve) => {resolve(mockSuccessData)}
});
UserStore.initSession();
expect(API.call).to.have.been.calledWith({
path: 'user/check-session',
data: {}
});
});
describe('and no session is active', function () {
beforeEach(function() {
let mockSuccessData = {
status: 'success',
data: {
sessionActive: false
}
};
API.call = stub().returns({
then: (resolve) => {resolve(mockSuccessData)}
});
});
it('should log out and delete remember data if expired', function () {
SessionStore.isRememberDataExpired.returns(true);
SessionStore.clearRememberData.reset();
UserStore.initSession();
expect(SessionStore.clearRememberData).to.have.been.called;
expect(SessionStore.closeSession).to.have.been.called;
expect(CommonActions.loggedOut).to.have.been.called;
});
it('should login with remember data', function () {
SessionStore.isRememberDataExpired.returns(false);
SessionStore.getRememberData.returns({
userId: 'REMEMBER_USER_ID',
token: 'REMEMBER_TOKEN',
expiration: 20160721
});
UserStore.initSession();
expect(API.call).to.have.been.calledWithMatch({
path: 'user/login',
data: {
userId: 'REMEMBER_USER_ID',
rememberToken: 'REMEMBER_TOKEN'
}
});
});
});
}})
});

View File

@ -1,6 +1,6 @@
const Reflux = require('reflux');
const API = require('lib-app/api-call');
const SessionStore = require('lib-app/session-store');
const sessionStore = require('lib-app/session-store');
const UserActions = require('actions/user-actions');
const CommonActions = require('actions/common-actions');
@ -14,35 +14,73 @@ const UserStore = Reflux.createStore({
this.listenTo(UserActions.login, this.loginUser);
this.listenTo(UserActions.logout, this.logoutUser);
},
initSession() {
return API.call({
path: 'user/check-session',
data: {}
}).then(this.tryLoginIfSessionIsInactive);
},
tryLoginIfSessionIsInactive(result) {
if (!result.data.sessionActive) {
if (sessionStore.isRememberDataExpired()) {
return this.logoutUser();
} else {
return this.loginWithRememberData();
}
}
},
loginUser(loginData) {
API.call({
let onSuccessLogin = (loginData.remember) ? this.handleLoginSuccessWithRemember : this.handleLoginSuccess;
let onFailedLogin = (loginData.isAutomatic) ? null : this.handleLoginFail;
return API.call({
path: 'user/login',
data: loginData,
onSuccess: this.handleLoginSuccess,
onFail: this.handleLoginFail
});
data: loginData
}).then(onSuccessLogin, onFailedLogin);
},
logoutUser() {
API.call({
path: 'user/logout',
onSuccess: function () {
SessionStore.closeSession();
this.trigger('LOGOUT');
CommonActions.loggedOut();
}.bind(this)
return API.call({
path: 'user/logout'
}).then(() => {
sessionStore.closeSession();
sessionStore.clearRememberData();
CommonActions.loggedOut();
this.trigger('LOGOUT');
});
},
isLoggedIn() {
return SessionStore.isLoggedIn();
return sessionStore.isLoggedIn();
},
loginWithRememberData() {
let rememberData = sessionStore.getRememberData();
return this.loginUser({
userId: rememberData.userId,
rememberToken: rememberData.token,
isAutomatic: true
});
},
handleLoginSuccessWithRemember(result) {
sessionStore.storeRememberData({
token: result.data.rememberToken,
userId: result.data.userId,
expiration: result.data.rememberExpiration
});
this.handleLoginSuccess(result)
},
handleLoginSuccess(result) {
SessionStore.createSession(result.data.userId, result.data.token);
this.trigger('LOGIN_SUCCESS');
sessionStore.createSession(result.data.userId, result.data.token);
CommonActions.logged();
this.trigger('LOGIN_SUCCESS');
},
handleLoginFail() {

View File

@ -0,0 +1,20 @@
<?php
class CheckSessionController extends Controller {
const PATH = '/check-session';
public function validations() {
return [
'permission' => 'any',
'requestData' => []
];
}
public function handler() {
$session = Session::getInstance();
Response::respondSuccess([
'sessionActive' => $session->sessionExists()
]);
}
}