Refactored use of IndexedDB - still some tests are failing

This commit is contained in:
mhelander 2018-01-25 23:35:56 +02:00
parent faf4c1e658
commit 19a662bf62
2 changed files with 109 additions and 110 deletions

110
sea.js
View File

@ -148,6 +148,55 @@
const Buffer = SafeBuffer
// This is safe class to operate with IndexedDB data - all methods are Promise
function EasyIndexedDB(objectStoreName, dbName = 'GunDB', dbVersion = 1) {
// Private internals, including constructor props
const runTransaction = (fn_) => new Promise((resolve, reject) => {
const open = indexedDB.open(dbName, dbVersion) // Open (or create) the DB
open.onerror = (e) => {
reject(new Error('IndexedDB error:', e))
}
open.onupgradeneeded = () => {
const db = open.result // Create the schema; props === current version
db.createObjectStore(objectStoreName, { keyPath: 'id' })
}
let result
open.onsuccess = () => { // Start a new transaction
const db = open.result
const tx = db.transaction(objectStoreName, 'readwrite')
const store = tx.objectStore(objectStoreName)
tx.oncomplete = () => {
db.close() // Close the db when the transaction is done
resolve(result) // Resolves result returned by action function fn_
}
result = fn_(store)
}
})
Object.assign(this, {
async wipe() { // Wipe IndexedDB completedy!
return runTransaction((store) => {
const act = store.clear()
act.onsuccess = () => {}
})
},
async put(id, props) {
const data = Object.assign({}, props, { id })
return runTransaction((store) => { store.put(data) })
},
async get(id, prop) {
return runTransaction((store) => new Promise((resolve) => {
const getData = store.get(id)
getData.onsuccess = () => {
const { result = {} } = getData
resolve(result[prop])
}
}))
}
})
}
const seaIndexedDb = new EasyIndexedDB('SEA', 'GunDB', 1)
// Encryption parameters
const pbkdf2 = { hash: 'SHA-256', iter: 50000, ks: 64 }
@ -190,7 +239,6 @@
// Practical examples about usage found from ./test/common.js
// These are internal wrappers for ES6 async/await use:
const seaCallOnStorage = async (...props) => SEA._callonstore_(...props)
const seaRead = async (...props) => SEA.read(...props)
const seaWrite = async (...props) => SEA.write(...props)
const seaEnc = async (...props) => SEA.enc(...props)
@ -208,14 +256,6 @@
} catch (e) {} //eslint-disable-line no-empty
return props
}
const seaIndexedDbGetter = async (key, prop) =>
await seaCallOnStorage((store) => new Promise((resolve) => {
const getData = store.get(key)
getData.onsuccess = () => {
const { result = {} } = getData
resolve(result[prop])
}
}))
// This is internal func queries public key(s) for alias.
const querygunaliases = (alias,root) => new Promise((resolve, reject) => {
@ -348,14 +388,8 @@
if (encrypted) {
const auth = await seaWrite(encrypted, key)
await seaCallOnStorage((store) => {
const act = store.clear() // Wipe IndexedDB completedy!
act.onsuccess = () => {}
}) // Then set encrypted auth props
await seaCallOnStorage((store) => {
store.put({ id, auth })
})
await seaIndexedDb.wipe()
await seaIndexedDb.put(id, { auth })
}
return props
@ -365,10 +399,7 @@
}
// Wiping IndexedDB completely when using random PIN
await seaCallOnStorage((store) => {
const act = store.clear()
act.onsuccess = () => {}
})
await seaIndexedDb.wipe()
// And remove sessionStorage data
sessionStorage.removeItem('user')
sessionStorage.removeItem('remember')
@ -431,8 +462,6 @@
}
const readAndDecrypt = async (data, pub, key) =>
parseProps(await seaDec(await seaRead(data, pub), key))
// Returns auth value prop from IndexedDB (encryption key)
const getIndexedDbAuth = async () => await seaIndexedDbGetter(alias, 'auth')
// Already authenticated?
if (root._.user
@ -447,7 +476,7 @@
// Yes, got persisted 'remember'?
if (!remember) {
throw { // And return proof if for matching alias
err: (await getIndexedDbAuth() && authsettings.validity
err: (await seaIndexedDb.get(alias, 'auth') && authsettings.validity
&& 'Missing PIN and alias!') || 'No authentication session found!'
}
}
@ -471,7 +500,7 @@
// No PIN, let's try short-term proof if for matching alias
? await checkRememberData(props)
// Got PIN so get IndexedDB secret if signature is ok
: await checkRememberData(await readAndDecrypt(await getIndexedDbAuth(), pub, pin))
: await checkRememberData(await readAndDecrypt(await seaIndexedDb.get(alias, 'auth'), pub, pin))
}
// got pub, try auth with pin & alias :: or unwrap Storage data...
const args = pin ? { pin, alias } : await readStorageData()
@ -560,15 +589,13 @@
const { pub: id } = p
const importAndStoreKey = async () => {
const key = await importKey(p)
await seaCallOnStorage((store) => {
store.put({ id, key })
})
await seaIndexedDb.put(id, { key })
return key
}
if (Gun.obj.has(p, 'set')) {
return importAndStoreKey() // proof update so overwrite
}
const aesKey = await seaIndexedDbGetter(id, 'key')
const aesKey = await seaIndexedDb.get(id, 'key')
return aesKey ? aesKey : importAndStoreKey()
}
@ -706,7 +733,7 @@
// Delete user data
root.get(`pub/${pub}`).put(null)
// Wipe user data from memory
const { user = { _: {} } } = root._
const { user = { _: {} } } = root._;
// TODO: is this correct way to 'logout' user from Gun.User ?
[ 'alias', 'sea', 'pub' ].map((key) => delete user._[key])
user._.is = user.is = {}
@ -1044,6 +1071,8 @@
}
const SEA = {
// This is easy way to use IndexedDB, all methods are Promises
EasyIndexedDB,
// This is Buffer used in SEA and usable from Gun/SEA application also.
// For documentation see https://nodejs.org/api/buffer.html
Buffer: SafeBuffer,
@ -1304,27 +1333,6 @@
if (cb && typeof cb === 'function') { doIt(cb, () => cb()) } else {
return new Promise(doIt)
}
},
// Internal helper for IndexedDB use - exposed for testing purposes only
_callonstore_(fn_) {
return new Promise((resolve) => {
const open = indexedDB.open('GunDB', 1) // Open (or create) the database; 1 === 'version'
open.onupgradeneeded = () => { // Create the schema; props === current version
const db = open.result
db.createObjectStore('SEA', { keyPath: 'id' })
}
let result
open.onsuccess = () => { // Start a new transaction
const db = open.result
const tx = db.transaction('SEA', 'readwrite')
const store = tx.objectStore('SEA')
tx.oncomplete = () => { // Close the db when the transaction is done
db.close()
resolve(result)
}
result = fn_(store)
}
})
}
}

View File

@ -24,23 +24,14 @@ var root;
}
}(this));
if(typeof Buffer === 'undefined'){
var Buffer = require('buffer').Buffer;
}
const Buffer = Gun.SEA.Buffer
const seaIndexedDb = new Gun.SEA.EasyIndexedDB('SEA', 'GunDB', 1)
const checkIndexedDB = (key, prop, resolve_) => {
const doIt = (resolve, reject) => {
try {
Gun.SEA._callonstore_((store) => new Promise((reslv) => {
const getData = store.get(key)
getData.onsuccess = () => {
reslv(getData.result && getData.result[prop])
}
})).then(resolve)
} catch (e) {
reject(e)
}
}
const doIt = (resolve, reject) => seaIndexedDb.get(key, prop)
.then(resolve).catch(reject)
if (resolve_) {
doIt(resolve_, (e) => { throw e })
} else {
@ -48,12 +39,15 @@ const checkIndexedDB = (key, prop, resolve_) => {
}
}
function setIndexedDB(key, prop, resolve_){
Gun.SEA._callonstore_(function(store){
store.put({id: key, auth: prop});
}, function(){
resolve_();
});
const setIndexedDB = (key, auth, resolve_) => {
const doIt = (resolve, reject) => seaIndexedDb.put(key, { auth })
.then(resolve).catch(reject)
if (resolve_) {
doIt(resolve_, (e) => { throw e })
} else {
return new Promise(doIt)
}
}
Gun.SEA && describe('SEA', function(){
@ -282,7 +276,7 @@ Gun().user && describe('Gun', function(){
var user = gun.user();
Gun.log.off = true; // Supress all console logging
var throwOutUser = function(wipeStorageData){
const throwOutUser = (wipeStorageData, done) => {
// Get rid of authenticated Gun user
var user = gun.back(-1)._.user;
// TODO: is this correct way to 'logout' user from Gun.User ?
@ -295,20 +289,20 @@ Gun().user && describe('Gun', function(){
// ... and persisted session
sessionStorage.removeItem('remember');
sessionStorage.removeItem('alias');
Gun.SEA._callonstore_(function(store) {
var act = store.clear(); // Wipes whole IndexedDB
act.onsuccess = function(){};
});
if (typeof done === 'function') {
seaIndexedDb.wipe().then(done)
return
} else {
return seaIndexedDb.wipe()
}
}
};
return Promise.resolve()
}
['callback', 'Promise'].forEach(function(type){
describe(type+':', function(){
beforeEach(function(done){
// Simulate browser reload
throwOutUser(true);
done();
});
// Simulate browser reload
beforeEach((done) => { throwOutUser(true, done) })
describe('create', function(){
@ -677,7 +671,7 @@ Gun().user && describe('Gun', function(){
expect(usr).to.have.key('_');
expect(usr._).to.have.keys(['pub', 'sea']);
// ... to validate 'remember' data
pin = pin && new Buffer(pin, 'utf8').toString('base64');
pin = pin && Buffer.from(pin, 'utf8').toString('base64');
return !pin ? Promise.resolve(sessionStorage.getItem('remember'))
: new Promise(function(resolve){
checkIndexedDB(usr._.alias, 'auth', resolve);
@ -784,7 +778,7 @@ Gun().user && describe('Gun', function(){
user.recall(12 * 60).then(
doCheck(function(ack){
expect(ack).to.have.key('err');
expect(ack.err.toLowerCase().indexOf('no authentication')).to.not.be(-1);
expect(ack.err.toLowerCase().indexOf('no session')).to.not.be(-1);
checkIndexedDB(alias+type, 'auth', function(auth){
expect((typeof auth !== 'undefined' && auth !== null && auth !== ''))
.to.not.eql(true);
@ -847,12 +841,13 @@ Gun().user && describe('Gun', function(){
});
it('valid session bootstrap using alias & PIN', function(done){
let sRemember
user.recall(12 * 60).then(function(){
return user.auth(alias+type, pass+' new', {pin: 'PIN'});
}).then(doCheck(function(ack){
// Let's save remember props
var sUser = root.sessionStorage.getItem('user');
var sRemember = root.sessionStorage.getItem('remember');
sRemember = root.sessionStorage.getItem('remember')
var iAuth = ack.auth;
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){
@ -888,6 +883,7 @@ Gun().user && describe('Gun', function(){
expect(props.err.toLowerCase()
.indexOf('missing pin')).not.to.be(-1);
}catch(e){ done(e); return }
root.sessionStorage.setItem('remember', sRemember)
// Ok, time to try auth with alias & PIN
return user.auth(alias+type, undefined, {pin: 'PIN'});
});
@ -962,21 +958,18 @@ Gun().user && describe('Gun', function(){
var ret = Object.assign({}, props, {iat: props.iat - 65 - props.exp});
return ret;
}, pin);
})).then(function(){
// Simulate browser reload
throwOutUser();
user.recall(60).then(function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.keys([ 'pub', 'sea' ]);
expect(ack).to.have.key('err');
expect(ack.err).to.not.be(undefined);
expect(ack.err).to.not.be('');
expect(ack.err.toLowerCase()
.indexOf('no authentication session')).not.to.be(-1);
done();
}).catch(done);
}).catch(done);
})).then(() => throwOutUser(false)) // Simulate browser reload
.then(() => user.recall(60).then((ack) => {
expect(ack).to.not.be(undefined)
expect(ack).to.not.be('')
expect(ack).to.not.have.keys([ 'pub', 'sea' ])
expect(ack).to.have.key('err')
expect(ack.err).to.not.be(undefined)
expect(ack.err).to.not.be('')
expect(ack.err.toLowerCase()
.indexOf('no session')).not.to.be(-1)
done()
})).catch(done)
});
it('changed password', function(done){
@ -1010,14 +1003,12 @@ Gun().user && describe('Gun', function(){
return user.auth(alias+type, pass+' new', {newpass: pass, pin: pin})
.then(function(usr){ expect(usr).to.not.have.key('err') });
}).then(function(){
return user.leave().then(function(ack){
try{
expect(ack).to.have.key('ok');
}catch(e){ done(e); return }
throwOutUser();
});
}).then(function(){
}).then(() => user.leave().then((ack) => {
try {
expect(ack).to.have.key('ok')
} catch (e) { done(e); return }
return throwOutUser(false)
})).then(function(){
// Simulate browser reload
// Call back pre-update remember...
root.sessionStorage.setItem('user', sUser);
@ -1034,7 +1025,7 @@ Gun().user && describe('Gun', function(){
expect(props.err).to.not.be(undefined);
expect(props.err).to.not.be('');
expect(props.err.toLowerCase()
.indexOf('no authentication session')).not.to.be(-1);
.indexOf('no session')).not.to.be(-1);
done();
}).catch(done);
}).catch(done);