feat: impl account password changing

This commit is contained in:
thecodrr
2020-12-11 20:19:28 +05:00
parent 34b6bb477c
commit a83b2355d2
5 changed files with 106 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ import Constants from "../utils/constants";
import { EV } from "../common"; import { EV } from "../common";
import Settings from "./settings"; import Settings from "./settings";
import Migrations from "./migrations"; import Migrations from "./migrations";
import Outbox from "./outbox";
/** /**
* @type {EventSource} * @type {EventSource}
@@ -59,6 +60,7 @@ class Database {
this.backup = new Backup(this); this.backup = new Backup(this);
this.settings = new Settings(this); this.settings = new Settings(this);
this.migrations = new Migrations(this); this.migrations = new Migrations(this);
this.outbox = new Outbox(this);
// collections // collections
/** @type {Notes} */ /** @type {Notes} */
@@ -76,6 +78,7 @@ class Database {
await this.settings.init(); await this.settings.init();
await this.user.sync(); await this.user.sync();
await this.outbox.init();
await this.migrations.init(); await this.migrations.init();
await this.migrations.migrate(); await this.migrations.migrate();
@@ -87,10 +90,10 @@ class Database {
this.evtSource.close(); this.evtSource.close();
} }
if (!user) return; if (!user || !user.accessToken) {
if (!user.accessToken) {
user = await this.user.get(); user = await this.user.get();
} }
if (!user) return;
this.evtSource = new NNEventSource(`${Constants.HOST}/events`, { this.evtSource = new NNEventSource(`${Constants.HOST}/events`, {
headers: { Authorization: `Bearer ${user.accessToken}` }, headers: { Authorization: `Bearer ${user.accessToken}` },
@@ -123,6 +126,10 @@ class Database {
await this.user.logout(); await this.user.logout();
EV.publish("user:deleted"); EV.publish("user:deleted");
break; break;
case "userPasswordChanged":
await this.user.logout();
EV.publish("user:passwordChanged");
break;
case "sync": case "sync":
await this.syncer.eventMerge(data); await this.syncer.eventMerge(data);
EV.publish("db:refresh"); EV.publish("db:refresh");
@@ -141,8 +148,8 @@ class Database {
}, 15 * 1000); }, 15 * 1000);
} }
sync(full = true) { sync(full = true, force = false) {
return this.syncer.start(full); return this.syncer.start(full, force);
} }
host(host) { host(host) {

View File

@@ -0,0 +1,43 @@
class Outbox {
/**
*
* @param {import("./index").default} db
*/
constructor(db) {
this._db = db;
this.outbox = {};
}
async init() {
this.outbox = (await this._db.context.read("outbox")) || {};
for (var id in this.outbox) {
const data = this.outbox[id];
switch (id) {
case "changePassword":
const key = await this._db.user.key();
const { username } = await this._db.user.get();
await this._db.context.deriveCryptoKey(`_uk_@${username}`, {
password: data.newPassword,
salt: key.salt,
});
await this._db.sync(false, true);
await this.delete(id);
break;
}
}
}
async add(id, data, action) {
this.outbox[id] = data;
await this._db.context.write("outbox", this.outbox);
await action();
await this.delete(id);
}
delete(id) {
delete this.outbox[id];
return this._db.context.write("outbox", this.outbox);
}
}
export default Outbox;

View File

@@ -12,11 +12,26 @@ class Collector {
*/ */
constructor(db) { constructor(db) {
this._db = db; this._db = db;
this._map = async (i) => {
const item = { ...i };
// in case of resolved delta
delete item.resolved;
// turn the migrated flag off so we don't keep syncing this item repeated
delete item.migrated;
return {
id: item.id,
v: CURRENT_DATABASE_VERSION,
...(await this._serialize(item)),
};
};
} }
async collect(lastSyncedTimestamp) { async collect(lastSyncedTimestamp, force) {
this._lastSyncedTimestamp = lastSyncedTimestamp; this._lastSyncedTimestamp = lastSyncedTimestamp;
this.key = await this._db.user.key(); this.key = await this._db.user.key();
this.force = force;
return { return {
notes: await this._collect(this._db.notes.raw), notes: await this._collect(this._db.notes.raw),
notebooks: await this._collect(this._db.notebooks.raw), notebooks: await this._collect(this._db.notebooks.raw),
@@ -35,24 +50,15 @@ class Collector {
} }
_collect(array) { _collect(array) {
if (this.force) {
return Promise.all(tfun.map(this._map)(array));
}
return Promise.all( return Promise.all(
tfun tfun
.filter( .filter(
(item) => item.dateEdited > this._lastSyncedTimestamp || item.migrated (item) => item.dateEdited > this._lastSyncedTimestamp || item.migrated
) )
.map(async (i) => { .map(this._map)(array)
const item = { ...i };
// in case of resolved delta
delete item.resolved;
// turn the migrated flag off so we don't keep syncing this item repeated
delete item.migrated;
return {
id: item.id,
v: CURRENT_DATABASE_VERSION,
...(await this._serialize(item)),
};
})(array)
); );
} }
} }

View File

@@ -65,13 +65,13 @@ export default class Sync {
return { user, lastSynced, token }; return { user, lastSynced, token };
} }
async start(full) { async start(full, force) {
let { lastSynced, token } = await this._performChecks(); let { lastSynced, token } = await this._performChecks();
if (full) var serverResponse = await this._fetch(lastSynced, token); if (full) var serverResponse = await this._fetch(lastSynced, token);
// we prepare local data before merging so we always have correct data // we prepare local data before merging so we always have correct data
const data = await this._collector.collect(lastSynced); const data = await this._collector.collect(lastSynced, force);
if (full) { if (full) {
// merge the server response // merge the server response

View File

@@ -28,8 +28,11 @@ export default class User {
"GET" "GET"
); );
} catch (e) { } catch (e) {
if (e.message.includes("not authorized")) await this.logout(); if (e.message.includes("not authorized")) {
else throw e; return await this.logout(
"You were logged out. Either your session expired or your account was deleted. Please try logging in again."
);
} else throw e;
} }
await this.set({ await this.set({
@@ -95,11 +98,11 @@ export default class User {
EV.publish("user:tokenRefreshed", user); EV.publish("user:tokenRefreshed", user);
} }
async logout() { async logout(reason) {
await this._context.clear(); await this._context.clear();
// propogate event // propogate event
EV.publish("user:loggedOut"); EV.publish("user:loggedOut", reason);
} }
async signup(username, email, password) { async signup(username, email, password) {
@@ -126,6 +129,28 @@ export default class User {
} }
} }
async changePassword(oldPassword, newPassword) {
let response = await authRequest.call(
this,
"users/password",
{ old_password: oldPassword, new_password: newPassword },
true,
"PATCH"
);
if (response.success) {
await this._db.outbox.add("changePassword", { newPassword }, async () => {
const key = await this.key();
const { username } = await this.get();
await this._context.deriveCryptoKey(`_uk_@${username}`, {
password: newPassword,
salt: key.salt,
});
await this._db.sync(false, true);
});
return true;
}
}
async _postLogin(password, remember, response) { async _postLogin(password, remember, response) {
await this._context.deriveCryptoKey(`_uk_@${response.payload.username}`, { await this._context.deriveCryptoKey(`_uk_@${response.payload.username}`, {
password, password,
@@ -173,9 +198,8 @@ async function authRequest(endpoint, data, auth = false, method = "POST") {
return result; return result;
} }
let json = await response.json();
let error = let error =
json.error || (await response.text()) ||
`Request failed with status code: ${response.status} ${response.statusText}.`; `Request failed with status code: ${response.status} ${response.statusText}.`;
throw new Error(error); throw new Error(error);
} }