Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

Commit

Permalink
Offline fixes (#1080)
Browse files Browse the repository at this point in the history
* Remove app manifest support

Also update pouchdb to 6.2.0

* Make sure login tests wait until db is loaded to finish.

* Setup testing to use chrome and service worker

* Fixed error when sys admin and not in stand alone mode

* Use worker-pouch to access local db via sw

Also, make sure search works offline

* Move pouch-find-indexes to mixin

* Sync local changes to remote

* Added logic to use background sync to retry remote syncing.

* Change all users to use oauth

Move away from CouchDB sessions so that for offline use we can use
oauth to validate users.

* Remove unneeded logging

* Setup to rely on background sync retrying on failure.

* Make sure logout and login handle offline gracefully.

* Make sure service workers are disabled for Electron.

* Make sure service worker is disabled on Electron tests.

* Try chrome headless

* Try using yarn and chrome beta

* Fix issues logging in with Electron

* Try using chrome beta headless

* pbkdf2 shouldn't need to be specified directly

* Removed unneeded code

* Try to get chrome headless working

* Try upgrading ember-concurrency if that is the issue.

* Fix issues with users DB in standalone (Electron) mode

* Try using regular Google Chrome

* Move back to PhantomJS

* Go back to npm
  • Loading branch information
jkleinsc authored May 18, 2017
1 parent db49ee6 commit 17d2eeb
Show file tree
Hide file tree
Showing 29 changed files with 35,830 additions and 1,100 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ addons:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

cache:
directories:
- $HOME/.npm
Expand Down
111 changes: 55 additions & 56 deletions app/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ const {
} = Ember;

export default Adapter.extend(CheckForErrors, {
config: Ember.inject.service(),
ajax: Ember.inject.service(),
database: Ember.inject.service(),
db: reads('database.mainDB'),
standAlone: reads('config.standAlone'),
usePouchFind: reads('database.usePouchFind'),

_specialQueries: [
'containsValue',
Expand All @@ -29,66 +29,65 @@ export default Adapter.extend(CheckForErrors, {
_esDefaultSize: 25,

_executeContainsSearch(store, type, query) {
let standAlone = get(this, 'standAlone');
if (standAlone) {
let usePouchFind = get(this, 'usePouchFind');
if (usePouchFind) {
return this._executePouchDBFind(store, type, query);
}
return new Ember.RSVP.Promise((resolve, reject) => {
let typeName = this.getRecordTypeName(type);
let searchUrl = `/search/hrdb/${typeName}/_search`;
if (query.containsValue && query.containsValue.value) {
let queryString = '';
query.containsValue.keys.forEach((key) => {
if (!Ember.isEmpty(queryString)) {
queryString = `${queryString} OR `;
}
let queryValue = query.containsValue.value;
switch (key.type) {
case 'contains': {
queryValue = `*${queryValue}*`;
break;
}
case 'fuzzy': {
queryValue = `${queryValue}~`;
break;
}
let typeName = this.getRecordTypeName(type);
let searchUrl = `/search/hrdb/${typeName}/_search`;
if (query.containsValue && query.containsValue.value) {
let queryString = '';
query.containsValue.keys.forEach((key) => {
if (!Ember.isEmpty(queryString)) {
queryString = `${queryString} OR `;
}
let queryValue = query.containsValue.value;
switch (key.type) {
case 'contains': {
queryValue = `*${queryValue}*`;
break;
}
queryString = `${queryString}data.${key.name}:${queryValue}`;
});
let successFn = (results) => {
if (results && results.hits && results.hits.hits) {
let resultDocs = Ember.A(results.hits.hits).map((hit) => {
let mappedResult = hit._source;
mappedResult.id = hit._id;
return mappedResult;
});
let response = {
rows: resultDocs
};
this._handleQueryResponse(response, store, type).then(resolve, reject);
} else if (results.rows) {
this._handleQueryResponse(results, store, type).then(resolve, reject);
} else {
reject('Search results are not valid');
case 'fuzzy': {
queryValue = `${queryValue}~`;
break;
}
};

if (Ember.isEmpty(query.size)) {
query.size = this.get('_esDefaultSize');
}

Ember.$.ajax(searchUrl, {
dataType: 'json',
data: {
q: queryString,
size: this.get('_esDefaultSize')
},
success: successFn
});
} else {
reject('invalid query');
queryString = `${queryString}data.${key.name}:${queryValue}`;
});
let ajax = get(this, 'ajax');
if (Ember.isEmpty(query.size)) {
query.size = this.get('_esDefaultSize');
}
});

return ajax.request(searchUrl, {
dataType: 'json',
data: {
q: queryString,
size: this.get('_esDefaultSize')
}
}).then((results) => {
if (results && results.hits && results.hits.hits) {
let resultDocs = Ember.A(results.hits.hits).map((hit) => {
let mappedResult = hit._source;
mappedResult.id = hit._id;
return mappedResult;
});
let response = {
rows: resultDocs
};
return this._handleQueryResponse(response, store, type);
} else if (results.rows) {
return this._handleQueryResponse(results, store, type);
} else {
throw new Error('Search results are not valid');
}
}).catch(() => {
// Try pouch db find if ajax fails
return this._executePouchDBFind(store, type, query);
});
} else {
throw new Error('invalid query');
}
},

_executePouchDBFind(store, type, query) {
Expand Down
1 change: 0 additions & 1 deletion app/admin/textreplace/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default AbstractIndexRoute.extend({
return store.findAll('text-expansion').then((result) => {
return result.filter((model) => {
let isNew = model.get('isNew');
console.log(`${model.get('from')} ${isNew}`);
return !isNew;
});
});
Expand Down
150 changes: 72 additions & 78 deletions app/authenticators/custom.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Ember from 'ember';
import BaseAuthenticator from 'ember-simple-auth/authenticators/base';
import crypto from 'npm:crypto';
import MapOauthParams from 'hospitalrun/mixins/map-oauth-params';
import OAuthHeaders from 'hospitalrun/mixins/oauth-headers';

const {
computed: {
Expand All @@ -10,69 +12,72 @@ const {
RSVP
} = Ember;

export default BaseAuthenticator.extend({
export default BaseAuthenticator.extend(MapOauthParams, OAuthHeaders, {
ajax: Ember.inject.service(),
config: Ember.inject.service(),
database: Ember.inject.service(),
serverEndpoint: '/db/_session',
useGoogleAuth: false,
serverEndpoint: '/auth/login',

standAlone: alias('config.standAlone'),
usersDB: alias('database.usersDB'),

/**
@method absolutizeExpirationTime
@private
*/
_absolutizeExpirationTime(expiresIn) {
if (!Ember.isEmpty(expiresIn)) {
return new Date((new Date().getTime()) + (expiresIn - 5) * 1000).getTime();
}
},

_checkUser(user) {
_checkUser(user, oauthConfigs) {
return new RSVP.Promise((resolve, reject) => {
this._makeRequest('POST', { name: user.name }, '/chkuser').then((response) => {
let headers = this.getOAuthHeaders(oauthConfigs);
this._makeRequest({ name: user.name }, '/chkuser', headers).then((response) => {
if (response.error) {
reject(response);
}
user.displayName = response.displayName;
user.role = response.role;
user.prefix = response.prefix;
resolve(user);
}, () => {
}).catch(() => {
// If chkuser fails, user is probably offline; resolve with currently stored credentials
resolve(user);
});
});
},

_getPromise(type, data) {
return new RSVP.Promise(function(resolve, reject) {
this._makeRequest(type, data).then(function(response) {
Ember.run(function() {
resolve(response);
});
}, function(xhr) {
Ember.run(function() {
reject(xhr.responseJSON || xhr.responseText);
});
});
}.bind(this));
_finishAuth(user, oauthConfigs) {
let config = this.get('config');
let database = this.get('database');
config.setCurrentUser(user);
return database.setup().then(() => {
user.oauthConfigs = oauthConfigs;
return user;
});
},

_makeRequest(type, data, url) {
_makeRequest(data, url, headers, method) {
if (!url) {
url = this.serverEndpoint;
}
return Ember.$.ajax({
url,
type,
let ajax = get(this, 'ajax');
let params = {
type: 'POST',
data,
dataType: 'json',
contentType: 'application/x-www-form-urlencoded',
xhrFields: {
withCredentials: true
}
};
if (method) {
params.type = method;
}
if (headers) {
params.headers = headers;
}

return ajax.request(url, params);
},

_saveOAuthConfigs(params) {
let config = get(this, 'config');
let oauthConfigs = this.mapOauthParams(params);
return config.saveOauthConfigs(oauthConfigs).then(() => {
return oauthConfigs;
});
},

Expand All @@ -88,75 +93,56 @@ export default BaseAuthenticator.extend({
return this._authenticateStandAlone(credentials);
}
if (credentials.google_auth) {
this.useGoogleAuth = true;
let sessionCredentials = {
google_auth: true,
consumer_key: credentials.params.k,
consumer_secret: credentials.params.s1,
token: credentials.params.t,
token_secret: credentials.params.s2,
name: credentials.params.i
};
return new RSVP.Promise((resolve, reject) => {
this._checkUser(sessionCredentials).then((user) => {
resolve(user);
this.get('config').setCurrentUser(user.name);
}, reject);
return this._saveOAuthConfigs(credentials.params).then((oauthConfigs) => {
return this._checkUser({ name: credentials.params.i }, oauthConfigs).then((user) => {
return this._finishAuth(user, oauthConfigs);
});
});
}

return new Ember.RSVP.Promise((resolve, reject) => {
let username = credentials.identification;
if (typeof username === 'string' && username) {
username = username.trim();
let username = this._getUserName(credentials);
let data = { name: username, password: credentials.password };
return this._makeRequest(data).then((user) => {
if (user.error) {
throw new Error(user.errorResult || 'Unauthorized user');
}
let data = { name: username, password: credentials.password };
this._makeRequest('POST', data).then((response) => {
response.name = data.name;
response.expires_at = this._absolutizeExpirationTime(600);
this._checkUser(response).then((user) => {
this.get('config').setCurrentUser(user.name);
let database = this.get('database');
database.setup({}).then(() => {
resolve(user);
}, reject);
}, reject);
}, function(xhr) {
reject(xhr.responseJSON || xhr.responseText);
let userInfo = {
displayName: user.displayName,
prefix: user.prefix,
role: user.role
};
userInfo.name = username;

return this._saveOAuthConfigs(user).then((oauthConfigs) => {
return this._finishAuth(userInfo, oauthConfigs);
});
});
},

invalidate() {
invalidate(data) {
let standAlone = get(this, 'standAlone');
if (this.useGoogleAuth || standAlone) {
return RSVP.resolve();
} else {
return this._getPromise('DELETE');
// Ping the remote db to make sure we still have connectivity before logging off.
let headers = this.getOAuthHeaders(data.oauthConfigs);
let remoteDBUrl = get(this, 'database').getRemoteDBUrl();
return this._makeRequest({}, remoteDBUrl, headers, 'GET');
}
},

restore(data) {
if (window.ELECTRON) { // config service has not been setup yet, so config.standAlone not available yet
return RSVP.resolve(data);
}
return new RSVP.Promise((resolve, reject) => {
let now = (new Date()).getTime();
if (!Ember.isEmpty(data.expires_at) && data.expires_at < now) {
reject();
} else {
if (data.google_auth) {
this.useGoogleAuth = true;
}
this._checkUser(data).then(resolve, reject);
}
});
return this._checkUser(data, data.oauthConfigs);
},

_authenticateStandAlone(credentials) {
let usersDB = get(this, 'usersDB');
return new RSVP.Promise((resolve, reject) => {
usersDB.get(`org.couchdb.user:${credentials.identification}`).then((user) => {
let username = this._getUserName(credentials);
usersDB.get(`org.couchdb.user:${username}`).then((user) => {
let { salt, iterations, derived_key } = user;
let { password } = credentials;
this._checkPassword(password, salt, iterations, derived_key, (error, isCorrectPassword) => {
Expand All @@ -167,7 +153,7 @@ export default BaseAuthenticator.extend({
reject(new Error('UNAUTHORIZED'));
}
user.role = this._getPrimaryRole(user);
resolve(user);
this._finishAuth(user, {}).then(resolve, reject);
});
}, reject);
});
Expand All @@ -193,6 +179,14 @@ export default BaseAuthenticator.extend({
});
}
return primaryRole;
},

_getUserName(credentials) {
let username = credentials.identification;
if (typeof username === 'string' && username) {
username = username.trim();
}
return username;
}

});
Loading

0 comments on commit 17d2eeb

Please sign in to comment.