Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement infinite sessions #7

Merged
merged 10 commits into from
Aug 8, 2020
5 changes: 3 additions & 2 deletions src/CONFIG.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// TODO: Figure out how to determine prod/dev on mobile, etc.
const IS_IN_PRODUCTION = false;
import {Platform} from 'react-native';

const IS_IN_PRODUCTION = Platform.OS === 'web' ? process.env.NODE_ENV === 'production' : !__DEV__;

export default {
PUSHER: {
Expand Down
11 changes: 11 additions & 0 deletions src/lib/Str.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* globals $, _ */

import Guid from './Guid';

const Str = {
/**
* Returns the proper phrase depending on the count that is passed.
Expand Down Expand Up @@ -49,6 +51,15 @@ const Str = {
nl2br(str) {
return str.replace(/\n/g, '<br />');
},

/**
* Generates a random device login using Guid
*
* @returns {string}
*/
generateDeviceLoginID() {
AndrewGable marked this conversation as resolved.
Show resolved Hide resolved
return `React-Native-Chat-${Guid()}`;
},
};

export default Str;
8 changes: 6 additions & 2 deletions src/page/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ export default class App extends Component {
login: '',
password: '',
// eslint-disable-next-line react/no-unused-state
error: Store.get(STOREKEYS.SESSION, 'error'),
error: null,
};
}

componentDidMount() {
// Listen for changes to our session
Store.subscribe(STOREKEYS.SESSION, this.sessionChanged);
Store.get(STOREKEYS.SESSION, 'error').then(error => this.setState({error}));
}

componentWillUnmount() {
Expand All @@ -54,7 +55,7 @@ export default class App extends Component {
* When the form is submitted, then we trigger our prop callback
*/
submit() {
signIn(this.state.login, this.state.password);
signIn(this.state.login, this.state.password, true);
}

render() {
Expand All @@ -81,6 +82,9 @@ export default class App extends Component {
</View>
<View>
<Button onPress={this.submit} title="Log In" />
{this.state.error && <Text style={{color: 'red'}}>
{this.state.error}
</Text>}
</View>
</SafeAreaView>
</>
Expand Down
93 changes: 60 additions & 33 deletions src/store/actions/SessionActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {request} from '../../lib/Network';
import ROUTES from '../../ROUTES';
import STOREKEYS from '../STOREKEYS';
import CONFIG from '../../CONFIG';
import Str from '../../lib/Str';
import Guid from '../../lib/Guid';

/**
* Amount of time (in ms) after which an authToken is considered expired.
Expand All @@ -12,23 +14,36 @@ import CONFIG from '../../CONFIG';
* @private
* @type {Number}
*/
const AUTH_TOKEN_EXPIRATION_TIME = 1000 * 60;
const AUTH_TOKEN_EXPIRATION_TIME = 1000 * 60 * 90;

/**
* Create login
* @param {string} authToken
* @param {string} login
* @param {string} password
AndrewGable marked this conversation as resolved.
Show resolved Hide resolved
* @returns {Promise}
*/
function createLogin(authToken, login, password) {
request('CreateLogin', {
return request('CreateLogin', {
authToken,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
partnerUserID: login,
partnerUserSecret: password,
}).catch((err) => {
Store.set(STOREKEYS.SESSION, {error: err});
}).then(() => Store.set(STOREKEYS.CREDENTIALS, {login, password}))
.catch(err => Store.set(STOREKEYS.SESSION, {error: err}));
}

/**
* Sets API data in the store when we make a successful "Authenticate"/"CreateLogin" request
* @param {object} data
* @returns {Promise}
*/
function setSuccessfulSignInData(data) {
return Store.multiSet({
[STOREKEYS.SESSION]: data,
[STOREKEYS.APP_REDIRECT_TO]: ROUTES.HOME,
[STOREKEYS.LAST_AUTHENTICATED]: new Date().getTime(),
});
}

Expand All @@ -42,64 +57,76 @@ function createLogin(authToken, login, password) {
*/
function signIn(login, password, useExpensifyLogin = false) {
let authToken;
return Store.multiSet({
[STOREKEYS.CREDENTIALS]: {login, password},
[STOREKEYS.SESSION]: {},
return request('Authenticate', {
useExpensifyLogin,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
partnerUserID: login,
partnerUserSecret: password,
})
.then(() => request('Authenticate', {
useExpensifyLogin,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
partnerUserID: login,
partnerUserSecret: password,
}))
.then((data) => {
authToken = data && data.authToken;

// 404 We need to create a login
if (data.jsonCode === 404 && !useExpensifyLogin) {
return signIn(login, password, true)
.then((newAuthToken) => {
createLogin(newAuthToken, login, password);
});
}

// If we didn't get a 200 response from authenticate, the user needs to sign in again
if (data.jsonCode !== 200) {
// eslint-disable-next-line no-console
console.warn('Did not get a 200 from authenticate, going back to sign in page');
return Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN);
console.debug('Non-200 from authenticate, going back to sign in page');
return Store.multiSet({
[STOREKEYS.CREDENTIALS]: {},
[STOREKEYS.SESSION]: {error: data.message},
[STOREKEYS.APP_REDIRECT_TO]: ROUTES.SIGNIN,
});
}

return Store.multiSet({
[STOREKEYS.SESSION]: data,
[STOREKEYS.APP_REDIRECT_TO]: ROUTES.HOME,
[STOREKEYS.LAST_AUTHENTICATED]: new Date().getTime(),
});
// If Expensify login, it's the users first time logging in and we need to create a login for the user
if (useExpensifyLogin) {
return createLogin(data.authToken, Str.generateDeviceLoginID(), Guid())
.then(() => setSuccessfulSignInData(data));
}

return setSuccessfulSignInData();
})
.then(() => authToken)
.catch((err) => {
console.error(err);
Store.set(STOREKEYS.SESSION, {error: err});
return Store.set(STOREKEYS.SESSION, {error: err.message});
});
}

/**
* Delete login
* @param {string} authToken
* @param {string} login
* @returns {Promise}
*/
function deleteLogin(authToken, login) {
return request('DeleteLogin', {
authToken,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
partnerUserID: login,
}).catch(err => Store.set(STOREKEYS.SESSION, {error: err.message}));
}

/**
* Sign out of our application
*
* @returns {Promise}
*/
async function signOut() {
function signOut() {
return Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN)
.then(Store.clear);
.then(() => Store.multiGet([STOREKEYS.SESSION, STOREKEYS.CREDENTIALS]))
.then(data => deleteLogin(data.session.authToken, data.credentials.login))
.then(Store.clear)
.catch(err => Store.set(STOREKEYS.SESSION, {error: err.message}));
}

/**
* Make sure the authToken we have is OK to use
*
* @returns {Promise}
*/
async function verifyAuthToken() {
function verifyAuthToken() {
return Store.multiGet([STOREKEYS.LAST_AUTHENTICATED, STOREKEYS.CREDENTIALS])
.then(({last_authenticated, credentials}) => {
const haveCredentials = !_.isNull(credentials);
Expand Down