diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index c6a3a95..626ef59 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -4,69 +4,76 @@ import Refresh from "feather-icons/dist/icons/refresh-cw.svg";
import "./footer.scss";
import Notifications from "../notifications";
-export class Footer extends Component<{}, { synced: boolean, syncing: boolean }> {
- constructor(props) {
- super(props);
- this.state = { synced: false, syncing: false };
- this.onSyncedChange = this.onSyncedChange.bind(this);
- this.onSyncChange = this.onSyncChange.bind(this);
- }
+export class Footer extends Component<
+ {},
+ { synced: boolean; syncing: boolean }
+> {
+ constructor(props) {
+ super(props);
+ this.state = { synced: false, syncing: false };
+ this.onSyncedChange = this.onSyncedChange.bind(this);
+ this.onSyncChange = this.onSyncChange.bind(this);
+ }
- async componentWillMount() {
- Notes.syncedObservable.subscribe(this.onSyncedChange);
- Notes.syncObservable.subscribe(this.onSyncChange);
- this.setState({ synced: await Notes.isSynced() })
- }
+ async componentWillMount() {
+ Notes.syncedObservable.subscribe(this.onSyncedChange);
+ Notes.syncObservable.subscribe(this.onSyncChange);
+ this.setState({ synced: await Notes.isSynced() });
+ }
- componentWillUnmount() {
- Notes.syncedObservable.unsubscribe(this.onSyncedChange);
- Notes.syncObservable.unsubscribe(this.onSyncChange);
- }
+ componentWillUnmount() {
+ Notes.syncedObservable.unsubscribe(this.onSyncedChange);
+ Notes.syncObservable.unsubscribe(this.onSyncChange);
+ }
- onSyncChange(state: boolean) {
- console.log("sync", state);
- this.setState({ syncing: state })
- }
+ onSyncChange(state: boolean) {
+ console.log("sync", state);
+ this.setState({ syncing: state });
+ }
- onSyncedChange(state: boolean) {
- console.log("synced", state);
- this.setState({ synced: state })
- }
+ onSyncedChange(state: boolean) {
+ console.log("synced", state);
+ this.setState({ synced: state });
+ }
- onSyncClick() {
- Notes.sync().then(() => {
- Notifications.sendInfo("Finished Synchronisation");
- })
- }
+ onSyncClick() {
+ Notes.sync().then(() => {
+ Notifications.sendInfo("Finished Synchronisation");
+ });
+ }
- render() {
- let extrac = undefined;
- let color;
- let text;
- if (this.state.syncing) {
- color = "orange";
- text = "syncing";
- extrac = "reloading"
- } else {
- if (this.state.synced) {
- color = "green";
- text = "synced";
- } else {
- color = "red";
- text = "not synced";
- }
- }
- return
+ );
+ }
+}
diff --git a/src/notes.ts b/src/notes.ts
index ef5681b..a1be88b 100644
--- a/src/notes.ts
+++ b/src/notes.ts
@@ -1,595 +1,655 @@
-import SecureFile, { IFile } from "@hibas123/secure-file-wrapper";
-import { Lock, Observable } from "@hibas123/utils";
-import * as aesjs from "aes-js";
-import { IDBPTransaction } from "idb";
-import { sha256 } from "js-sha256";
-import * as uuidv4 from "uuid/v4";
-import * as config from "../config.json";
-import * as b64 from "./helper/base64";
-import IDB from "./helper/indexeddb";
-import Notifications, { MessageType } from "./notifications";
-
-
-export class HttpError extends Error {
- constructor(public status: number, public statusText: string) {
- super(statusText);
- console.log(statusText);
- }
-}
-
-// const newSymbol = Symbol("Symbol for new Notes")
-
-export interface Note {
- _id: string;
- folder: string;
- time: Date;
-}
-
-interface DBNote extends Note {
- __value: Uint8Array;
- preview: Uint8Array;
-}
-
-export interface BaseNote extends Note {
- preview: string;
-}
-
-export interface ViewNote extends BaseNote {
- __value: string;
-}
-
-import clonedeep = require("lodash.clonedeep");
-
-const Encoder = new TextEncoder();
-const Decoder = new TextDecoder();
-
-enum OpLogType {
- CREATE,
- CHANGE,
- DELETE
-}
-
-interface OpLog {
- /**
- * Type of operation
- */
- type: OpLogType;
-
- /**
- * The value
- */
- values: {
- value: Uint8Array,
- preview: Uint8Array
- };
-
- /**
- * Date of change
- */
- date: Date;
-}
-
-export type VaultList = { name: string, encrypted: boolean, id: string }[];
-
-export interface IVault {
- name: string;
- id: string;
- encrypted: boolean;
- getAllNotes(): Promise;
- searchNote(term: string): Promise
- newNote(): ViewNote;
- saveNote(note: ViewNote, date?: Date): Promise;
- getNote(id: string): Promise;
- deleteNote(id: string): Promise;
-}
-
-const awaitTimeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-
-class NotesProvider {
- private notesObservableServer = new Observable()
- public notesObservable = this.notesObservableServer.getPublicApi()
-
- private syncObservableServer = new Observable()
- /**
- * Will send false once finished and true on start
- */
- public syncObservable = this.syncObservableServer.getPublicApi()
-
- private database = new IDB("notes", ["notes", "oplog"]);
- private noteDB = this.database.getStore("notes");
- private oplogDB = this.database.getStore<{ id: string, logs: OpLog[] }>("oplog");
-
- private vaultKeys = new Map();
-
-
- public apiLock = new Lock();
- public apiLockRls = this.apiLock.getLock()
-
- private syncLock = new Lock();
-
- private _secureFile: SecureFile;
-
- private generalEncryption: Uint8Array = undefined;
-
- private syncedObservableServer = new Observable();
- public syncedObservable = this.syncedObservableServer.getPublicApi();
-
- public async isSynced() {
- return (await this.oplogDB.getAll()).length <= 0
- }
-
- private _name;
- public get name() {
- return this._name;
- }
-
- loginRequired() {
- return !localStorage.getItem("refreshtoken") || !this.generalEncryption;
- }
-
- login() {
- window.location.href = `${config.auth_server}/auth?client_id=${config.client_id}&scope=${config.permission}&redirect_uri=${encodeURIComponent(config.callback_url)}&response_type=code`
- }
-
- async getToken(code: string) {
- let req = await fetch(`${config.auth_server}/api/oauth/refresh?grant_type=authorization_code&client_id=${config.client_id}&code=${code}`);
- let res = await req.json();
- if (!res.error) {
- localStorage.setItem("refreshtoken", res.token);
- localStorage.setItem("name", res.profile.name);
- this._name = res.profile.name;
- let kb = this.passwordToKey(res.profile.enc_key);
- localStorage.setItem("enc_key", b64.encode(kb));
- this.generalEncryption = kb
- } else {
- return "Invalid Code"
- }
- }
-
- constructor(public readonly baseurl = "") {
- this._secureFile = new SecureFile(config.secure_file_server);
- this._secureFile.jwtObservable.subscribe(async (callback) => {
- try {
- let jwt = await this.getJWT();
- callback(null, jwt);
- } catch (err) {
- callback(err, null);
- }
- })
-
- let key = localStorage.getItem("enc_key");
- if (key) {
- this.generalEncryption = b64.decode(key)
- }
- this._name = localStorage.getItem("name");
- }
-
- public async start() {
- const next = () => {
- setTimeout(() => {
- this.sync().then(next);
- }, 30000)
- }
- this.syncedObservableServer.send((await this.oplogDB.getAll()).length <= 0);
- let prs = this.apiLockRls.then(lock => lock.release());
- prs.then(() => awaitTimeout(2000)).then(() => this.sync()).then(() => next());
- return prs;
- }
-
- private async getJWT() {
- let lock = await this.apiLock.getLock()
- try {
- console.log("Getting JWT");
- let req = await fetch(config.auth_server + "/api/oauth/jwt?refreshtoken=" + localStorage.getItem("refreshtoken"));
- if (req.status !== 200) {
- Notifications.sendNotification("offline", MessageType.INFO);
- throw new Error("Offline")
- }
- let res = await req.json();
- if (res.error) {
- console.log("Refresh token invalid, forward to login")
- localStorage.removeItem("refreshtoken");
- this.login()
- throw new Error("Need login!")
- } else {
- return res.token
- }
- } finally {
- lock.release()
- }
- }
-
- async Logout() {
- localStorage.removeItem("refreshtoken");
- window.location.reload();
- }
-
- async sync() {
- const lock = await this.syncLock.getLock()
- const log = (...message: any[]) => {
- console.log("[SYNC]: ", ...message)
- }
-
- this.syncObservableServer.send(true);
- log("Start")
- try {
- log("Fetching");
- let [remotes, locals, oplogs] = await Promise.all([this._secureFile.list(), this.noteDB.getAll(), this.oplogDB.getAll()]);
- log("Fetched");
- // Create sync pairs (remote & local)
- log("Building pairs");
- let pairs: { local: DBNote, remote: IFile, id: string, oplog: OpLog[] }[] = [];
- remotes.map(r => {
- let lIdx = locals.findIndex(e => e._id === r._id);
- let l: DBNote = undefined;
- if (lIdx >= 0) {
- l = locals[lIdx];
- locals.splice(lIdx, 1);
- }
-
- let oIdx = oplogs.findIndex(e => e.id === r._id);
- let oplog: OpLog[];
- if (oIdx >= 0) {
- oplog = oplogs[oIdx].logs;
- oplogs.splice(oIdx, 1);
- }
-
- pairs.push({
- remote: r,
- local: l,
- oplog,
- id: r._id
- })
- })
-
- locals.forEach(l => {
- let oIdx = oplogs.findIndex(e => e.id === l._id);
- let oplog: OpLog[] = undefined;
- if (oIdx >= 0) {
- oplog = oplogs[oIdx].logs;
- if (oplog.length <= 0) oplog = undefined;
- oplogs.splice(oIdx, 1);
- }
-
- pairs.push({
- remote: undefined,
- local: l,
- oplog,
- id: l._id
- })
- })
-
- oplogs.forEach(oplog => {
- if (!oplog) return;
- if (oplog.logs.length > 0)
- pairs.push({
- remote: undefined,
- local: undefined,
- oplog: oplog.logs,
- id: oplog.id
- })
- })
-
- log("Pairs builded");
-
- let stats = {
- remote_change: 0,
- remote_delete: 0,
- remote_create: 0,
- local_change: 0,
- local_delete: 0,
- do_nothing: 0,
- error: 0
- }
-
- log("Start inspection");
- for (let { local, remote, oplog, id } of pairs) {
- const apply = async (old = false) => {
- log("Apply OPLOG to", id);
- for (let op of oplog) {
- switch (op.type) {
- case OpLogType.CHANGE:
- log(id, "REMOTE CHANGE");
- stats.remote_change++;
- await this._secureFile.update(id, op.values.value, b64.encode(op.values.preview), op.date, old);
- break;
- case OpLogType.DELETE:
- log(id, "REMOTE DELETE");
- stats.remote_delete++;
- if (old) break; // if the deletion is old, just ignore
- await this._secureFile.delete(id)
- break;
- case OpLogType.CREATE:
- log(id, "REMOTE CREATE");
- stats.remote_create++;
- await this._secureFile.create(
- "",
- op.values.value,
- "binary",
- local.folder,
- b64.encode(op.values.preview),
- id,
- op.date
- );
- break;
- }
- }
- }
-
- const localChange = (id: string) => {
- //TODO implement
- }
-
- const create = async () => {
- log(id, "LOCAL CREATAE/UPDATE");
- stats.local_change++;
- let value = await this._secureFile.get(id);
- let note: DBNote = {
- _id: remote._id,
- folder: remote.folder,
- preview: b64.decode(remote.active.preview),
- time: remote.active.time,
- __value: new Uint8Array(value)
- }
- await this.noteDB.set(id, note);
- localChange(id);
- }
-
- try {
- // log(id, "LRO: ", !!local, !!remote, !!oplog)
- if (remote && !oplog) {
- if (local) {
- let old = remote.active.time.valueOf() > local.time.valueOf();
- if (old)
- await create()
- else {
- stats.do_nothing++;
- log(id, "DO NOTHING");
- }
- } else {
- await create()
- }
- } else if (!remote && local && !oplog) { // No local changes, but remote deleted
- log("LOCAL DELETE");
- stats.local_delete++;
- await this.noteDB.delete(id);
- localChange(id);
- } else if (!remote && oplog) { // Remote does not exist, but oplog, just apply all changes including possible delete
- await apply()
- } else if (remote && oplog) {
- let last = oplog[oplog.length - 1]
- let old = remote.active.time.valueOf() > last.date.valueOf();
-
- if (old)
- await create() // Will recreate local entry
- await apply(old) // Will apply changes to remote
- } else {
- log(id, "DO NOTHING");
- stats.do_nothing++;
- }
- } catch (err) {
- console.error(err);
- stats.error++;
- Notifications.sendNotification("Error syncing: " + id, MessageType.ERROR);
- }
- await this.oplogDB.delete(id);
- }
- log("Stats", stats);
- this.syncedObservableServer.send((await this.oplogDB.getAll()).length <= 0)
- } finally {
- log("Finished")
- lock.release()
- this.syncObservableServer.send(false);
- }
- }
-
- public forgetVaultKey(vault_id: string) {
- this.vaultKeys.delete(vault_id);
- localStorage.removeItem("vault_" + vault_id);
- }
-
- public getVaultKey(vault_id: string) {
- let key = this.vaultKeys.get(vault_id);
- if (!key) {
- let lsk = localStorage.getItem("vault_" + vault_id);
- if (lsk) {
- key = b64.decode(lsk);
- this.vaultKeys.set(vault_id, key);
- }
- }
- return key;
- }
-
- public saveVaultKey(vault_id: string, key: Uint8Array, permanent?: boolean) {
- this.vaultKeys.set(vault_id, key);
- if (permanent) {
- localStorage.setItem("vault_" + vault_id, b64.encode(key));
- }
- }
-
- public getVaults(): Promise {
- return this.noteDB.getAll()
- .then(notes => notes
- .filter(e => Decoder.decode(e.preview) === "__VAULT__")
- .map(e => {
- let value = e.__value;
- let encrypted = false;
- if (this.decrypt(value) !== "__BASELINE__") encrypted = true;
- return { name: e.folder, encrypted, id: e._id }
- }));
- }
-
- public async createVault(name: string, key?: Uint8Array) {
- let vault: DBNote = {
- __value: this.encrypt("__BASELINE__", key),
- _id: uuidv4(),
- folder: name,
- preview: Encoder.encode("__VAULT__"),
- time: new Date()
- }
- let tx = this.database.transaction();
- await Promise.all([
- this.addop(vault._id, OpLogType.CREATE, {
- value: vault.__value,
- preview: vault.preview
- }, vault.time, tx),
- this.noteDB.set(vault._id, vault)
- ]);
- }
-
- public async getVault(vault_id: string, key?: Uint8Array): Promise {
- let vault = await this.noteDB.get(vault_id);
- if (!vault) throw new Error("Vault not found!");
- if (this.decrypt(vault.__value, key) !== "__BASELINE__") throw new Error("Invalid password!");
- return new NotesProvider.Vault(vault, key)
- }
-
- public async deleteVault(vault_id: string) {
- let vault = await this.noteDB.get(vault_id);
- if (!vault) throw new Error("Vault not found!");
- let v = new NotesProvider.Vault(vault);
- await Promise.all((await v.getAllNotes()).map(note => this.delete(note._id)));
- await this.delete(v.id); // This can also delete a vault
- }
-
- public passwordToKey(password: string) {
- return new Uint8Array(sha256.arrayBuffer(password + config.client_id))
- }
-
- private _encrypt(value: Uint8Array, key?: Uint8Array): Uint8Array {
- if (!key) return value;
- var aesCtr = new aesjs.ModeOfOperation.ctr(key);
- var encryptedBytes = aesCtr.encrypt(value);
- return new Uint8Array(encryptedBytes);
- }
-
- private encrypt(value: string, key?: Uint8Array): Uint8Array {
- let msg = this._encrypt(Encoder.encode(value), key)
- return new Uint8Array(this._encrypt(msg, this.generalEncryption))
- }
-
- private _decrypt(value: ArrayBuffer, key?: Uint8Array): Uint8Array {
- if (!key) return new Uint8Array(value);
- var aesCtr = new aesjs.ModeOfOperation.ctr(key);
- var decryptedBytes = aesCtr.decrypt(value);
- return new Uint8Array(decryptedBytes)
- }
-
- private decrypt(value: ArrayBuffer, key?: Uint8Array): string {
- let msg = this._decrypt(value, key)
- return Decoder.decode(this._decrypt(msg, this.generalEncryption))
- }
-
- async addop(note_id: string, type: OpLogType, values: { value: Uint8Array, preview: Uint8Array }, date: Date, transaction?: IDBPTransaction) {
- let tx = transaction || this.oplogDB.transaction();
- let oplog = await this.oplogDB.get(note_id, tx);
- if (!oplog) oplog = { logs: [], id: note_id };
- oplog.logs.push({
- date: date,
- type,
- values
- })
- this.syncedObservableServer.send(false);
- await this.oplogDB.set(note_id, oplog, tx);
- }
-
- async delete(id: string) {
- let lock = await this.syncLock.getLock();
- let tx = this.database.transaction(this.oplogDB, this.noteDB)
- await Promise.all([
- this.addop(id, OpLogType.DELETE, null, new Date(), tx),
- this.noteDB.delete(id, tx)
- ])
- lock.release();
- }
-
- static Vault = class implements IVault {
- id: string;
- name: string;
- encrypted: boolean = false;
- constructor(private vault: Note, private key?: Uint8Array) {
- if (key) this.encrypted = true;
- this.id = vault._id;
- this.name = vault.folder;
- }
-
- private encrypt(data: string) {
- return Notes.encrypt(data, this.key);
- }
-
- private decrypt(data: ArrayBuffer) {
- return Notes.decrypt(data, this.key);
- }
-
- async getAllNotes() {
- return Notes.noteDB.getAll()
- .then(all => all.filter(e => e.folder === this.vault._id)
- .sort(this.sort)
- .map(e => {
- let new_note = clonedeep(e) as BaseNote
- delete (new_note).__value
- new_note.preview = this.decrypt(e.preview)
- return new_note;
- }));
- }
-
- private sort(a: DBNote, b: DBNote) {
- return new Date(b.time).getTime() - new Date(a.time).getTime();
- }
-
- async searchNote(term: string) {
- let all = await this.getAllNotes();
- return all.filter(e => e.preview.indexOf(term) >= 0)
- }
-
- newNote(): ViewNote {
- return {
- _id: uuidv4(),
- folder: this.vault._id,
- time: new Date(),
- __value: "",
- preview: ""
- }
- }
-
- async saveNote(note: ViewNote, date = new Date()) {
- let lock = await Notes.syncLock.getLock();
- const tx = Notes.database.transaction(Notes.noteDB, Notes.oplogDB);
- let old_note = await Notes.noteDB.get(note._id, tx);
-
- let new_note = clonedeep(note) as DBNote;
- new_note.__value = this.encrypt(note.__value)
- let [title, preview] = note.__value.split("\n");
- if (preview) preview = "\n" + preview;
- else preview = ""
- new_note.preview = this.encrypt((title + preview).substr(0, 128))
- new_note.time = date;
-
- await Promise.all([
- Notes.addop(note._id, !old_note ? OpLogType.CREATE : OpLogType.CHANGE, {
- value: new_note.__value,
- preview: new_note.preview
- }, date, tx),
- Notes.noteDB.set(note._id, new_note, tx)
- ])
- lock.release();
- }
-
- async getNote(id: string): Promise {
- let note = await Notes.noteDB.get(id);
- if (!note) return undefined;
- let new_note = clonedeep(note) as ViewNote;
- new_note.__value = this.decrypt(note.__value);
- return new_note;
- }
-
- deleteNote(id: string) {
- return Notes.delete(id);
- }
- }
-}
-
-const Notes = new NotesProvider()
-export default Notes;
-
-(window).api = Notes
\ No newline at end of file
+import SecureFile, { IFile } from "@hibas123/secure-file-wrapper";
+import { Lock, Observable } from "@hibas123/utils";
+import * as aesjs from "aes-js";
+import { IDBPTransaction } from "idb";
+import { sha256 } from "js-sha256";
+import * as uuidv4 from "uuid/v4";
+import * as config from "../config.json";
+import * as b64 from "./helper/base64";
+import IDB from "./helper/indexeddb";
+import Notifications, { MessageType } from "./notifications";
+
+export class HttpError extends Error {
+ constructor(public status: number, public statusText: string) {
+ super(statusText);
+ console.log(statusText);
+ }
+}
+
+// const newSymbol = Symbol("Symbol for new Notes")
+
+export interface Note {
+ _id: string;
+ folder: string;
+ time: Date;
+}
+
+interface DBNote extends Note {
+ __value: Uint8Array;
+ preview: Uint8Array;
+}
+
+export interface BaseNote extends Note {
+ preview: string;
+}
+
+export interface ViewNote extends BaseNote {
+ __value: string;
+}
+
+import clonedeep = require("lodash.clonedeep");
+
+const Encoder = new TextEncoder();
+const Decoder = new TextDecoder();
+
+enum OpLogType {
+ CREATE,
+ CHANGE,
+ DELETE,
+}
+
+interface OpLog {
+ /**
+ * Type of operation
+ */
+ type: OpLogType;
+
+ /**
+ * The value
+ */
+ values: {
+ value: Uint8Array;
+ preview: Uint8Array;
+ };
+
+ /**
+ * Date of change
+ */
+ date: Date;
+}
+
+export type VaultList = { name: string; encrypted: boolean; id: string }[];
+
+export interface IVault {
+ name: string;
+ id: string;
+ encrypted: boolean;
+ getAllNotes(): Promise;
+ searchNote(term: string): Promise;
+ newNote(): ViewNote;
+ saveNote(note: ViewNote, date?: Date): Promise;
+ getNote(id: string): Promise;
+ deleteNote(id: string): Promise;
+}
+
+const awaitTimeout = (ms: number) =>
+ new Promise((resolve) => setTimeout(resolve, ms));
+
+class NotesProvider {
+ private notesObservableServer = new Observable();
+ public notesObservable = this.notesObservableServer.getPublicApi();
+
+ private syncObservableServer = new Observable();
+ /**
+ * Will send false once finished and true on start
+ */
+ public syncObservable = this.syncObservableServer.getPublicApi();
+
+ private database = new IDB("notes", ["notes", "oplog"]);
+ private noteDB = this.database.getStore("notes");
+ private oplogDB = this.database.getStore<{ id: string; logs: OpLog[] }>(
+ "oplog"
+ );
+
+ private vaultKeys = new Map();
+
+ public apiLock = new Lock();
+ public apiLockRls = this.apiLock.getLock();
+
+ private syncLock = new Lock();
+
+ private _secureFile: SecureFile;
+
+ private generalEncryption: Uint8Array = undefined;
+
+ private syncedObservableServer = new Observable();
+ public syncedObservable = this.syncedObservableServer.getPublicApi();
+
+ public async isSynced() {
+ return (await this.oplogDB.getAll()).length <= 0;
+ }
+
+ private _name;
+ public get name() {
+ return this._name;
+ }
+
+ loginRequired() {
+ return !localStorage.getItem("refreshtoken") || !this.generalEncryption;
+ }
+
+ login() {
+ window.location.href = `${config.auth_server}/auth?client_id=${
+ config.client_id
+ }&scope=${config.permission}&redirect_uri=${encodeURIComponent(
+ config.callback_url
+ )}&response_type=code`;
+ }
+
+ async getToken(code: string) {
+ let req = await fetch(
+ `${config.auth_server}/api/oauth/refresh?grant_type=authorization_code&client_id=${config.client_id}&code=${code}`
+ );
+ let res = await req.json();
+ if (!res.error) {
+ localStorage.setItem("refreshtoken", res.token);
+ localStorage.setItem("name", res.profile.name);
+ this._name = res.profile.name;
+ let kb = this.passwordToKey(res.profile.enc_key);
+ localStorage.setItem("enc_key", b64.encode(kb));
+ this.generalEncryption = kb;
+ } else {
+ return "Invalid Code";
+ }
+ }
+
+ constructor(public readonly baseurl = "") {
+ this._secureFile = new SecureFile(config.secure_file_server);
+ this._secureFile.jwtObservable.subscribe(async (callback) => {
+ try {
+ let jwt = await this.getJWT();
+ callback(null, jwt);
+ } catch (err) {
+ callback(err, null);
+ }
+ });
+
+ let key = localStorage.getItem("enc_key");
+ if (key) {
+ this.generalEncryption = b64.decode(key);
+ }
+ this._name = localStorage.getItem("name");
+ }
+
+ public async start() {
+ const next = () => {
+ setTimeout(() => {
+ this.sync().then(next);
+ }, 30000);
+ };
+ this.syncedObservableServer.send(
+ (await this.oplogDB.getAll()).length <= 0
+ );
+ let prs = this.apiLockRls.then((lock) => lock.release());
+ prs.then(() => awaitTimeout(2000))
+ .then(() => this.sync())
+ .then(() => next());
+ return prs;
+ }
+
+ private async getJWT() {
+ let lock = await this.apiLock.getLock();
+ try {
+ console.log("Getting JWT");
+ let req = await fetch(
+ config.auth_server +
+ "/api/oauth/jwt?refreshtoken=" +
+ localStorage.getItem("refreshtoken")
+ );
+ if (req.status !== 200) {
+ Notifications.sendNotification("offline", MessageType.INFO);
+ throw new Error("Offline");
+ }
+ let res = await req.json();
+ if (res.error) {
+ console.log("Refresh token invalid, forward to login");
+ localStorage.removeItem("refreshtoken");
+ this.login();
+ throw new Error("Need login!");
+ } else {
+ return res.token;
+ }
+ } finally {
+ lock.release();
+ }
+ }
+
+ async Logout() {
+ localStorage.removeItem("refreshtoken");
+ window.location.reload();
+ }
+
+ async sync() {
+ const lock = await this.syncLock.getLock();
+ const log = (...message: any[]) => {
+ console.log("[SYNC]: ", ...message);
+ };
+
+ this.syncObservableServer.send(true);
+ log("Start");
+ try {
+ log("Fetching");
+ let [remotes, locals, oplogs] = await Promise.all([
+ this._secureFile.list(),
+ this.noteDB.getAll(),
+ this.oplogDB.getAll(),
+ ]);
+ log("Fetched");
+ // Create sync pairs (remote & local)
+ log("Building pairs");
+ let pairs: {
+ local: DBNote;
+ remote: IFile;
+ id: string;
+ oplog: OpLog[];
+ }[] = [];
+ remotes.map((r) => {
+ let lIdx = locals.findIndex((e) => e._id === r._id);
+ let l: DBNote = undefined;
+ if (lIdx >= 0) {
+ l = locals[lIdx];
+ locals.splice(lIdx, 1);
+ }
+
+ let oIdx = oplogs.findIndex((e) => e.id === r._id);
+ let oplog: OpLog[];
+ if (oIdx >= 0) {
+ oplog = oplogs[oIdx].logs;
+ oplogs.splice(oIdx, 1);
+ }
+
+ pairs.push({
+ remote: r,
+ local: l,
+ oplog,
+ id: r._id,
+ });
+ });
+
+ locals.forEach((l) => {
+ let oIdx = oplogs.findIndex((e) => e.id === l._id);
+ let oplog: OpLog[] = undefined;
+ if (oIdx >= 0) {
+ oplog = oplogs[oIdx].logs;
+ if (oplog.length <= 0) oplog = undefined;
+ oplogs.splice(oIdx, 1);
+ }
+
+ pairs.push({
+ remote: undefined,
+ local: l,
+ oplog,
+ id: l._id,
+ });
+ });
+
+ oplogs.forEach((oplog) => {
+ if (!oplog) return;
+ if (oplog.logs.length > 0)
+ pairs.push({
+ remote: undefined,
+ local: undefined,
+ oplog: oplog.logs,
+ id: oplog.id,
+ });
+ });
+
+ log("Pairs builded");
+
+ let stats = {
+ remote_change: 0,
+ remote_delete: 0,
+ remote_create: 0,
+ local_change: 0,
+ local_delete: 0,
+ do_nothing: 0,
+ error: 0,
+ };
+
+ log("Start inspection");
+ for (let { local, remote, oplog, id } of pairs) {
+ const apply = async (old = false) => {
+ log("Apply OPLOG to", id);
+ for (let op of oplog) {
+ switch (op.type) {
+ case OpLogType.CHANGE:
+ log(id, "REMOTE CHANGE");
+ stats.remote_change++;
+ await this._secureFile.update(
+ id,
+ op.values.value,
+ b64.encode(op.values.preview),
+ op.date,
+ old
+ );
+ break;
+ case OpLogType.DELETE:
+ log(id, "REMOTE DELETE");
+ stats.remote_delete++;
+ if (old) break; // if the deletion is old, just ignore
+ await this._secureFile.delete(id);
+ break;
+ case OpLogType.CREATE:
+ log(id, "REMOTE CREATE");
+ stats.remote_create++;
+ await this._secureFile.create(
+ "",
+ op.values.value,
+ "binary",
+ local.folder,
+ b64.encode(op.values.preview),
+ id,
+ op.date
+ );
+ break;
+ }
+ }
+ };
+
+ const localChange = (id: string) => {
+ //TODO implement
+ };
+
+ const create = async () => {
+ log(id, "LOCAL CREATAE/UPDATE");
+ stats.local_change++;
+ let value = await this._secureFile.get(id);
+ let note: DBNote = {
+ _id: remote._id,
+ folder: remote.folder,
+ preview: b64.decode(remote.active.preview || ""),
+ time: remote.active.time,
+ __value: new Uint8Array(value),
+ };
+ await this.noteDB.set(id, note);
+ localChange(id);
+ };
+
+ try {
+ // log(id, "LRO: ", !!local, !!remote, !!oplog)
+ if (remote && !oplog) {
+ if (local) {
+ let old =
+ remote.active.time.valueOf() > local.time.valueOf();
+ if (old) await create();
+ else {
+ stats.do_nothing++;
+ log(id, "DO NOTHING");
+ }
+ } else {
+ await create();
+ }
+ } else if (!remote && local && !oplog) {
+ // No local changes, but remote deleted
+ log("LOCAL DELETE");
+ stats.local_delete++;
+ await this.noteDB.delete(id);
+ localChange(id);
+ } else if (!remote && oplog) {
+ // Remote does not exist, but oplog, just apply all changes including possible delete
+ await apply();
+ } else if (remote && oplog) {
+ let last = oplog[oplog.length - 1];
+ let old = remote.active.time.valueOf() > last.date.valueOf();
+
+ if (old) await create(); // Will recreate local entry
+ await apply(old); // Will apply changes to remote
+ } else {
+ log(id, "DO NOTHING");
+ stats.do_nothing++;
+ }
+ } catch (err) {
+ console.error(err);
+ stats.error++;
+ Notifications.sendNotification(
+ "Error syncing: " + id,
+ MessageType.ERROR
+ );
+ }
+ await this.oplogDB.delete(id);
+ }
+ log("Stats", stats);
+ this.syncedObservableServer.send(
+ (await this.oplogDB.getAll()).length <= 0
+ );
+ } finally {
+ log("Finished");
+ lock.release();
+ this.syncObservableServer.send(false);
+ }
+ }
+
+ public forgetVaultKey(vault_id: string) {
+ this.vaultKeys.delete(vault_id);
+ localStorage.removeItem("vault_" + vault_id);
+ }
+
+ public getVaultKey(vault_id: string) {
+ let key = this.vaultKeys.get(vault_id);
+ if (!key) {
+ let lsk = localStorage.getItem("vault_" + vault_id);
+ if (lsk) {
+ key = b64.decode(lsk);
+ this.vaultKeys.set(vault_id, key);
+ }
+ }
+ return key;
+ }
+
+ public saveVaultKey(vault_id: string, key: Uint8Array, permanent?: boolean) {
+ this.vaultKeys.set(vault_id, key);
+ if (permanent) {
+ localStorage.setItem("vault_" + vault_id, b64.encode(key));
+ }
+ }
+
+ public getVaults(): Promise {
+ return this.noteDB.getAll().then((notes) =>
+ notes
+ .filter((e) => Decoder.decode(e.preview) === "__VAULT__")
+ .map((e) => {
+ let value = e.__value;
+ let encrypted = false;
+ if (this.decrypt(value) !== "__BASELINE__") encrypted = true;
+ return { name: e.folder, encrypted, id: e._id };
+ })
+ );
+ }
+
+ public async createVault(name: string, key?: Uint8Array) {
+ let vault: DBNote = {
+ __value: this.encrypt("__BASELINE__", key),
+ _id: uuidv4(),
+ folder: name,
+ preview: Encoder.encode("__VAULT__"),
+ time: new Date(),
+ };
+ let tx = this.database.transaction();
+ await Promise.all([
+ this.addop(
+ vault._id,
+ OpLogType.CREATE,
+ {
+ value: vault.__value,
+ preview: vault.preview,
+ },
+ vault.time,
+ tx
+ ),
+ this.noteDB.set(vault._id, vault),
+ ]);
+ }
+
+ public async getVault(vault_id: string, key?: Uint8Array): Promise {
+ let vault = await this.noteDB.get(vault_id);
+ if (!vault) throw new Error("Vault not found!");
+ if (this.decrypt(vault.__value, key) !== "__BASELINE__")
+ throw new Error("Invalid password!");
+ return new NotesProvider.Vault(vault, key);
+ }
+
+ public async deleteVault(vault_id: string) {
+ let vault = await this.noteDB.get(vault_id);
+ if (!vault) throw new Error("Vault not found!");
+ let v = new NotesProvider.Vault(vault);
+ await Promise.all(
+ (await v.getAllNotes()).map((note) => this.delete(note._id))
+ );
+ await this.delete(v.id); // This can also delete a vault
+ }
+
+ public passwordToKey(password: string) {
+ return new Uint8Array(sha256.arrayBuffer(password + config.client_id));
+ }
+
+ private _encrypt(value: Uint8Array, key?: Uint8Array): Uint8Array {
+ if (!key) return value;
+ var aesCtr = new aesjs.ModeOfOperation.ctr(key);
+ var encryptedBytes = aesCtr.encrypt(value);
+ return new Uint8Array(encryptedBytes);
+ }
+
+ private encrypt(value: string, key?: Uint8Array): Uint8Array {
+ let msg = this._encrypt(Encoder.encode(value), key);
+ return new Uint8Array(this._encrypt(msg, this.generalEncryption));
+ }
+
+ private _decrypt(value: ArrayBuffer, key?: Uint8Array): Uint8Array {
+ if (!key) return new Uint8Array(value);
+ var aesCtr = new aesjs.ModeOfOperation.ctr(key);
+ var decryptedBytes = aesCtr.decrypt(value);
+ return new Uint8Array(decryptedBytes);
+ }
+
+ private decrypt(value: ArrayBuffer, key?: Uint8Array): string {
+ let msg = this._decrypt(value, key);
+ return Decoder.decode(this._decrypt(msg, this.generalEncryption));
+ }
+
+ async addop(
+ note_id: string,
+ type: OpLogType,
+ values: { value: Uint8Array; preview: Uint8Array },
+ date: Date,
+ transaction?: IDBPTransaction
+ ) {
+ let tx = transaction || this.oplogDB.transaction();
+ let oplog = await this.oplogDB.get(note_id, tx);
+ if (!oplog) oplog = { logs: [], id: note_id };
+ oplog.logs.push({
+ date: date,
+ type,
+ values,
+ });
+ this.syncedObservableServer.send(false);
+ await this.oplogDB.set(note_id, oplog, tx);
+ }
+
+ async delete(id: string) {
+ let lock = await this.syncLock.getLock();
+ let tx = this.database.transaction(this.oplogDB, this.noteDB);
+ await Promise.all([
+ this.addop(id, OpLogType.DELETE, null, new Date(), tx),
+ this.noteDB.delete(id, tx),
+ ]);
+ lock.release();
+ }
+
+ static Vault = class implements IVault {
+ id: string;
+ name: string;
+ encrypted: boolean = false;
+ constructor(private vault: Note, private key?: Uint8Array) {
+ if (key) this.encrypted = true;
+ this.id = vault._id;
+ this.name = vault.folder;
+ }
+
+ private encrypt(data: string) {
+ return Notes.encrypt(data, this.key);
+ }
+
+ private decrypt(data: ArrayBuffer) {
+ return Notes.decrypt(data, this.key);
+ }
+
+ async getAllNotes() {
+ return Notes.noteDB.getAll().then((all) =>
+ all
+ .filter((e) => e.folder === this.vault._id)
+ .sort(this.sort)
+ .map((e) => {
+ let new_note = clonedeep(e) as BaseNote;
+ delete (new_note).__value;
+ new_note.preview = this.decrypt(e.preview);
+ return new_note;
+ })
+ );
+ }
+
+ private sort(a: DBNote, b: DBNote) {
+ return new Date(b.time).getTime() - new Date(a.time).getTime();
+ }
+
+ async searchNote(term: string) {
+ let all = await this.getAllNotes();
+ return all.filter((e) => e.preview.indexOf(term) >= 0);
+ }
+
+ newNote(): ViewNote {
+ return {
+ _id: uuidv4(),
+ folder: this.vault._id,
+ time: new Date(),
+ __value: "",
+ preview: "",
+ };
+ }
+
+ async saveNote(note: ViewNote, date = new Date()) {
+ let lock = await Notes.syncLock.getLock();
+ const tx = Notes.database.transaction(Notes.noteDB, Notes.oplogDB);
+ let old_note = await Notes.noteDB.get(note._id, tx);
+
+ let new_note = clonedeep(note) as DBNote;
+ new_note.__value = this.encrypt(note.__value);
+ let [title, preview] = note.__value.split("\n");
+ if (preview) preview = "\n" + preview;
+ else preview = "";
+ new_note.preview = this.encrypt((title + preview).substr(0, 128));
+ new_note.time = date;
+
+ await Promise.all([
+ Notes.addop(
+ note._id,
+ !old_note ? OpLogType.CREATE : OpLogType.CHANGE,
+ {
+ value: new_note.__value,
+ preview: new_note.preview,
+ },
+ date,
+ tx
+ ),
+ Notes.noteDB.set(note._id, new_note, tx),
+ ]);
+ lock.release();
+ }
+
+ async getNote(id: string): Promise {
+ let note = await Notes.noteDB.get(id);
+ if (!note) return undefined;
+ let new_note = clonedeep(note) as ViewNote;
+ new_note.__value = this.decrypt(note.__value);
+ return new_note;
+ }
+
+ deleteNote(id: string) {
+ return Notes.delete(id);
+ }
+ };
+}
+
+const Notes = new NotesProvider();
+export default Notes;
+
+(window).api = Notes;