diff --git a/client/package.json b/client/package.json
index d6e54b3b..be1c6fae 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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"
}
}
diff --git a/client/src/app/index.js b/client/src/app/index.js
index 7a01f3da..e4bec24b 100644
--- a/client/src/app/index.js
+++ b/client/src/app/index.js
@@ -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);
+
diff --git a/client/src/app/main/dashboard/dashboard-layout.js b/client/src/app/main/dashboard/dashboard-layout.js
index d8bfa4bd..f7e13d1b 100644
--- a/client/src/app/main/dashboard/dashboard-layout.js
+++ b/client/src/app/main/dashboard/dashboard-layout.js
@@ -14,12 +14,12 @@ const DashboardLayout = React.createClass({
},
render() {
- return (
+ return (UserStore.isLoggedIn()) ? (
- );
+ ) : null;
}
});
diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js
index f98981f2..fe8be0c0 100644
--- a/client/src/data/fixtures/user-fixtures.js
+++ b/client/src/data/fixtures/user-fixtures.js
@@ -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
+ }
+ };
+ }
}
];
diff --git a/client/src/lib-app/__mocks__/api-call-mock.js b/client/src/lib-app/__mocks__/api-call-mock.js
index 3fcd2003..f7de46f3 100644
--- a/client/src/lib-app/__mocks__/api-call-mock.js
+++ b/client/src/lib-app/__mocks__/api-call-mock.js
@@ -1,3 +1,5 @@
export default {
- call: stub()
+ call: stub().returns(new Promise(function (resolve) {
+ resolve();
+ }))
};
\ No newline at end of file
diff --git a/client/src/lib-app/__mocks__/session-store-mock.js b/client/src/lib-app/__mocks__/session-store-mock.js
index 5ceed298..c75b4045 100644
--- a/client/src/lib-app/__mocks__/session-store-mock.js
+++ b/client/src/lib-app/__mocks__/session-store-mock.js
@@ -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()
};
\ No newline at end of file
diff --git a/client/src/lib-app/__tests__/session-store-test.js b/client/src/lib-app/__tests__/session-store-test.js
new file mode 100644
index 00000000..883d38a3
--- /dev/null
+++ b/client/src/lib-app/__tests__/session-store-test.js
@@ -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');
+ });
+});
\ No newline at end of file
diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js
index 2db55bb8..89d26ce1 100644
--- a/client/src/lib-app/api-call.js
+++ b/client/src/lib-app/api-call.js
@@ -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);
+ }
+ });
});
}
};
\ No newline at end of file
diff --git a/client/src/lib-app/date.js b/client/src/lib-app/date.js
new file mode 100644
index 00000000..f14beb2b
--- /dev/null
+++ b/client/src/lib-app/date.js
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/client/src/lib-app/session-store.js b/client/src/lib-app/session-store.js
index f6a05b77..8fbce685 100644
--- a/client/src/lib-app/session-store.js
+++ b/client/src/lib-app/session-store.js
@@ -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;
\ No newline at end of file
+export default new SessionStore();
\ No newline at end of file
diff --git a/client/src/stores/__tests__/user-store-test.js b/client/src/stores/__tests__/user-store-test.js
index 5ccc6790..f6fb225c 100644
--- a/client/src/stores/__tests__/user-store-test.js
+++ b/client/src/stores/__tests__/user-store-test.js
@@ -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'
+ }
+ });
+ });
+ });
+ }})
});
diff --git a/client/src/stores/user-store.js b/client/src/stores/user-store.js
index 93118c36..213123cc 100644
--- a/client/src/stores/user-store.js
+++ b/client/src/stores/user-store.js
@@ -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() {
diff --git a/server/controllers/user/check-session.php b/server/controllers/user/check-session.php
new file mode 100644
index 00000000..88cc8fd3
--- /dev/null
+++ b/server/controllers/user/check-session.php
@@ -0,0 +1,20 @@
+ 'any',
+ 'requestData' => []
+ ];
+ }
+
+ public function handler() {
+ $session = Session::getInstance();
+
+ Response::respondSuccess([
+ 'sessionActive' => $session->sessionExists()
+ ]);
+ }
+}