First alpha
This commit is contained in:
commit
313f5aee97
4
.gitignore
vendored
Executable file
4
.gitignore
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
.cache/
|
||||||
|
.vscode/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
7
config.json
Executable file
7
config.json
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"secure_file_server": "http://192.168.178.129:3004/",
|
||||||
|
"auth_server": "https://auth.stamm.me",
|
||||||
|
"client_id": "cf699b12-2014-4a61-bb7d-a7e38b197350",
|
||||||
|
"permission": "5c1ed399dc361731340a49d4",
|
||||||
|
"callback_url": "http://localhost:8080"
|
||||||
|
}
|
8857
package-lock.json
generated
Executable file
8857
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
51
package.json
Executable file
51
package.json
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@hibas123/securenotes2",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode=production",
|
||||||
|
"watch": "webpack --mode=development --watch",
|
||||||
|
"start": "webpack-dev-server --mode=production",
|
||||||
|
"start:prod": "webpack-dev-server --mode=production",
|
||||||
|
"start:dev": "webpack-dev-server --mode=development"
|
||||||
|
},
|
||||||
|
"author": "Fabian Stamm <dev@fabianstamm.de>",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@hibas123/secure-file-wrapper": "^2.3.1",
|
||||||
|
"aes-js": "^3.1.2",
|
||||||
|
"crypto-js": "^3.1.9-1",
|
||||||
|
"feather-icons": "^4.10.0",
|
||||||
|
"idb": "^3.0.2",
|
||||||
|
"js-sha256": "^0.9.0",
|
||||||
|
"js-sha512": "^0.8.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"mini.css": "^3.0.1",
|
||||||
|
"secure-file-wrapper": "git+https://git.stamm.me/OpenServer/OSSecureFileWrapper.git",
|
||||||
|
"uuid": "^3.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash.clonedeep": "^4.5.4",
|
||||||
|
"@types/uuid": "^3.4.4",
|
||||||
|
"copy-webpack-plugin": "^4.6.0",
|
||||||
|
"css-loader": "^2.1.0",
|
||||||
|
"file-loader": "^3.0.1",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"mini-css-extract-plugin": "^0.5.0",
|
||||||
|
"node-sass": "^4.10.0",
|
||||||
|
"preact": "^8.3.1",
|
||||||
|
"preact-svg-loader": "^0.2.1",
|
||||||
|
"sass-loader": "^7.1.0",
|
||||||
|
"style-loader": "^0.23.1",
|
||||||
|
"ts-loader": "^5.3.0",
|
||||||
|
"typescript": "^3.2.4",
|
||||||
|
"webpack": "^4.29.0",
|
||||||
|
"webpack-bundle-analyzer": "^3.0.3",
|
||||||
|
"webpack-cli": "^3.1.2",
|
||||||
|
"webpack-dev-server": "^3.1.10",
|
||||||
|
"webpack-visualizer-plugin": "^0.1.11",
|
||||||
|
"workbox-webpack-plugin": "^3.6.3",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
6
public/config.json
Executable file
6
public/config.json
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"auth_server": "https://auth.stamm.me",
|
||||||
|
"client_id": "",
|
||||||
|
"permission": "",
|
||||||
|
"callback_url": ""
|
||||||
|
}
|
1
public/manifest.json
Executable file
1
public/manifest.json
Executable file
@ -0,0 +1 @@
|
|||||||
|
{}
|
7
src/components/AddButton.tsx
Executable file
7
src/components/AddButton.tsx
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
import { h } from "preact";
|
||||||
|
import "./add_button.scss";
|
||||||
|
import Plus from "feather-icons/dist/icons/plus.svg";
|
||||||
|
|
||||||
|
export default function AddButton({ onClick }: { onClick: () => void }) {
|
||||||
|
return <button class="add_button_button circular primary" onClick={() => onClick()}><Plus width={undefined} height={undefined} /></button>
|
||||||
|
}
|
6
src/components/App.tsx
Executable file
6
src/components/App.tsx
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
import { Router } from "./Routing";
|
||||||
|
import { h } from "preact";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return <Router></Router>
|
||||||
|
}
|
0
src/components/ContextMenu.tsx
Executable file
0
src/components/ContextMenu.tsx
Executable file
56
src/components/Routing.tsx
Executable file
56
src/components/Routing.tsx
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import Navigation from '../navigation';
|
||||||
|
import "./routing.scss"
|
||||||
|
|
||||||
|
|
||||||
|
export class Router extends Component<{}, { next?: JSX.Element, current: JSX.Element }> {
|
||||||
|
mounted: HTMLDivElement = undefined;
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
this.state = { current: Navigation.page.page, next: undefined }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
Navigation.pageObservable.subscribe(this.onChange, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
Navigation.pageObservable.unsubscribe(this.onChange)
|
||||||
|
}
|
||||||
|
to = -1;
|
||||||
|
onChange([page]: JSX.Element[]) {
|
||||||
|
this.setState({ next: page, current: this.state.next || this.state.current });
|
||||||
|
if (this.to >= 0) {
|
||||||
|
clearTimeout(this.to)
|
||||||
|
this.to = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let overlay;
|
||||||
|
if (this.state.next) {
|
||||||
|
overlay = <div class="transition_container transition_slidein" key={this.state.next.key} ref={(elm: HTMLDivElement) => {
|
||||||
|
let lst = () => {
|
||||||
|
if (this.state.next)
|
||||||
|
this.setState({ current: this.state.next, next: undefined }, () => {
|
||||||
|
if (this.mounted)
|
||||||
|
this.mounted.scrollTo({ top: 0 })
|
||||||
|
});
|
||||||
|
if (elm)
|
||||||
|
elm.removeEventListener("animationend", lst)
|
||||||
|
}
|
||||||
|
if (elm)
|
||||||
|
elm.addEventListener("animationend", lst)
|
||||||
|
}}>
|
||||||
|
{this.state.next}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
return <div style="overflow:hidden">
|
||||||
|
<div class="transition_container" ref={elm => this.mounted = elm}>
|
||||||
|
{this.state.current}
|
||||||
|
</div>
|
||||||
|
{overlay}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
15
src/components/add_button.scss
Executable file
15
src/components/add_button.scss
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
// .add_button_container {}
|
||||||
|
.add_button_button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
z-index: 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add_button_button>svg {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
25
src/components/demo.tsx
Executable file
25
src/components/demo.tsx
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
import { h } from "preact"
|
||||||
|
import { Page } from "../page";
|
||||||
|
|
||||||
|
export interface DemoProps {
|
||||||
|
state: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DemoPage extends Page<DemoProps, {}> {
|
||||||
|
constructor(props: DemoProps) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let elms = [];
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
elms.push(<li>Hallo {i}</li>);
|
||||||
|
}
|
||||||
|
return <div style={{ background: this.props.state.color, marginTop: "-12px", paddingTop: "12px" }} >
|
||||||
|
<h1>Hallo Welt</h1>
|
||||||
|
<ul>
|
||||||
|
{elms}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
40
src/components/modals/InputModal.tsx
Executable file
40
src/components/modals/InputModal.tsx
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import "./modal.scss"
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
|
||||||
|
|
||||||
|
export class InputModal extends Component<{ title: string, fieldname: string, type: "text" | "password", onResult: (result) => void }, {}> {
|
||||||
|
input: HTMLInputElement;
|
||||||
|
rand: string;
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.rand = Math.random().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.input)
|
||||||
|
this.input.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Modal title={this.props.title} onClose={() => this.props.onResult(null)}>
|
||||||
|
<fieldset style="border:none;">
|
||||||
|
<label for={this.rand}>{this.props.fieldname}</label>
|
||||||
|
<input style="min-width: 85%" autofocus ref={elm => {
|
||||||
|
this.input = elm
|
||||||
|
if (this.input)
|
||||||
|
setTimeout(() => this.input.focus(), 0)
|
||||||
|
}} type={this.props.type} id={this.rand} placeholder={this.props.fieldname} onKeyDown={evt => {
|
||||||
|
if (evt.keyCode === 13) {
|
||||||
|
this.props.onResult(this.input.value)
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<button class="primary" style="display: inline-block;" onClick={() => {
|
||||||
|
this.props.onResult(this.input.value);
|
||||||
|
}}>Enter</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
}
|
9
src/components/modals/LoadingModal.tsx
Executable file
9
src/components/modals/LoadingModal.tsx
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import "./modal.scss"
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
|
||||||
|
export default function LoadingModal() {
|
||||||
|
return <Modal title="Loading" noClose>
|
||||||
|
<div class="spinner primary" style="height: 80px; width: 80px; margin: 3rem auto;"></div>
|
||||||
|
</Modal>
|
||||||
|
}
|
31
src/components/modals/Modal.tsx
Executable file
31
src/components/modals/Modal.tsx
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import "./modal.scss"
|
||||||
|
|
||||||
|
|
||||||
|
export class Modal extends Component<{ title: string, onClose?: () => void, noClose?: boolean }, {}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div class="modal_container" onClick={(evt) => {
|
||||||
|
let path = evt.composedPath();
|
||||||
|
if (!path.find(e => {
|
||||||
|
let res = false;
|
||||||
|
let s = (e as Element);
|
||||||
|
if (s) {
|
||||||
|
if (s.classList) {
|
||||||
|
res = s.classList.contains("card")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}))
|
||||||
|
if (this.props.onClose) this.props.onClose();
|
||||||
|
}}>
|
||||||
|
<div class="card" >
|
||||||
|
<h3 class="section">{this.props.title}</h3>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
39
src/components/modals/YesNoModal.tsx
Executable file
39
src/components/modals/YesNoModal.tsx
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import "./modal.scss"
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
|
||||||
|
|
||||||
|
export class YesNoModal extends Component<{ title: string, onResult: (result: boolean | undefined) => void }, {}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.onKeyDown = this.onKeyDown.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
window.addEventListener("keydown", this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener("keydown", this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(evt: KeyboardEvent) {
|
||||||
|
if (evt.keyCode === 74 || evt.keyCode === 89) this.props.onResult(true)
|
||||||
|
else if (evt.keyCode === 78) this.props.onResult(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Modal title={this.props.title} onClose={() => this.props.onResult(undefined)}>
|
||||||
|
<fieldset style="border:none;">
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<button class="primary" style="display: inline-block;" onClick={() => {
|
||||||
|
this.props.onResult(false);
|
||||||
|
}}>No</button>
|
||||||
|
<button class="primary" style="display: inline-block;" onClick={() => {
|
||||||
|
this.props.onResult(true);
|
||||||
|
}}>Yes</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
}
|
19
src/components/modals/modal.scss
Executable file
19
src/components/modals/modal.scss
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
.modal_container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal_container>.card {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 10%;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
180
src/components/routes/vault/Entry.tsx
Executable file
180
src/components/routes/vault/Entry.tsx
Executable file
@ -0,0 +1,180 @@
|
|||||||
|
import { h, Component } from "preact"
|
||||||
|
import Notes, { IVault, ViewNote, MessageType } from "../../../notes";
|
||||||
|
import Trash from "feather-icons/dist/icons/trash-2.svg"
|
||||||
|
import X from "feather-icons/dist/icons/x.svg"
|
||||||
|
import Save from "feather-icons/dist/icons/save.svg"
|
||||||
|
|
||||||
|
import Navigation from "../../../navigation";
|
||||||
|
import { YesNoModal } from "../../modals/YesNoModal";
|
||||||
|
import LoadingModal from "../../modals/LoadingModal";
|
||||||
|
|
||||||
|
const minRows = 3;
|
||||||
|
export default class EntryComponent extends Component<{ vault: Promise<IVault>, id: string | undefined }, { loading: boolean, title: string, changed: boolean, modal: JSX.Element | undefined }> {
|
||||||
|
old_text: string;
|
||||||
|
text: string = "";
|
||||||
|
vault: IVault;
|
||||||
|
lineHeight: number = 24;
|
||||||
|
note: ViewNote;
|
||||||
|
|
||||||
|
rows: number = minRows;
|
||||||
|
|
||||||
|
skip_save: boolean = false;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { changed: false, title: "", modal: undefined, loading: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private toVault() {
|
||||||
|
history.back()
|
||||||
|
// Navigation.setPage("/vault", { id: this.vault.id }, { entry: "false" }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentWillMount() {
|
||||||
|
try {
|
||||||
|
this.skip_save = false;
|
||||||
|
this.setState({ loading: true })
|
||||||
|
this.vault = await this.props.vault;
|
||||||
|
let note: ViewNote;
|
||||||
|
if (this.props.id)
|
||||||
|
note = await this.vault.getNote(this.props.id)
|
||||||
|
else
|
||||||
|
note = this.vault.newNote();
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
Notes.messageObservableServer.send({ message: "Note not found!", type: MessageType.ERROR });
|
||||||
|
// this.toVault()
|
||||||
|
} else {
|
||||||
|
this.note = note;
|
||||||
|
this.text = note.__value;
|
||||||
|
let rows = this.getRows(this.text);
|
||||||
|
if (rows !== this.rows) {
|
||||||
|
this.rows = rows;
|
||||||
|
}
|
||||||
|
let [title] = this.text.split("\n", 1);
|
||||||
|
this.setState({ loading: false, title })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Notes.sendErrorMessage(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async save() {
|
||||||
|
try {
|
||||||
|
if (this.state.changed) {
|
||||||
|
this.note.__value = this.text;
|
||||||
|
await this.vault.saveNote(this.note);
|
||||||
|
this.setState({ changed: false })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Notes.sendErrorMessage(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async onKeypress(event) {
|
||||||
|
// event = event || window.event;
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (!this.skip_save)
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
strToNr(value: string) {
|
||||||
|
let match = value.match(/\d/g)
|
||||||
|
return Number(match.join(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
getRows(value: string) {
|
||||||
|
const lines = (value.match(/\r?\n/g) || '').length + 1
|
||||||
|
return Math.max(lines + 1, minRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
textAreaChange(evt: KeyboardEvent) {
|
||||||
|
if (evt.keyCode === 17 || evt.keyCode === 27) return; //No character, so not relevant for this function
|
||||||
|
let target = (evt.target as HTMLTextAreaElement)
|
||||||
|
let value = target.value;
|
||||||
|
|
||||||
|
this.text = value;
|
||||||
|
if (!this.state.changed && this.textAreaKeyPress(evt)) this.setState({ changed: true })
|
||||||
|
|
||||||
|
let [title] = value.split("\n", 1);
|
||||||
|
if (title !== this.state.title)
|
||||||
|
this.setState({ title });
|
||||||
|
|
||||||
|
let rows = this.getRows(value);
|
||||||
|
if (rows !== this.rows) {
|
||||||
|
target.rows = rows;
|
||||||
|
this.rows = rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitHandler() {
|
||||||
|
if (this.state.changed) {
|
||||||
|
let modal = <YesNoModal title="Really want to quit?" onResult={res => {
|
||||||
|
if (res === true) {
|
||||||
|
this.skip_save = true;
|
||||||
|
this.toVault();
|
||||||
|
}
|
||||||
|
this.setState({ modal: undefined });
|
||||||
|
}} />
|
||||||
|
this.setState({ modal })
|
||||||
|
} else
|
||||||
|
this.toVault()
|
||||||
|
}
|
||||||
|
|
||||||
|
textAreaKeyPress(evt: KeyboardEvent) {
|
||||||
|
if ((evt.keyCode === 83 || evt.keyCode === 13) && evt.ctrlKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.save();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (evt.keyCode === 27) {
|
||||||
|
event.preventDefault();
|
||||||
|
// this.skip_save = true;
|
||||||
|
// this.toVault()
|
||||||
|
this.exitHandler();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let loading_modal = this.state.loading ? <LoadingModal /> : undefined;
|
||||||
|
|
||||||
|
const save_handler = async () => {
|
||||||
|
await this.save()
|
||||||
|
Navigation.setPage("/vault", { id: this.vault.id }, { entry: "false" }, true);
|
||||||
|
this.toVault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const delete_handler = async () => {
|
||||||
|
await this.vault.deleteNote(this.props.id);
|
||||||
|
this.toVault()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
{loading_modal}
|
||||||
|
{this.state.modal}
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<a class="button header_icon_button" onClick={() => this.exitHandler()}><X height={undefined} width={undefined} /></a>
|
||||||
|
{this.state.changed ? <a class="button header_icon_button" onClick={() => save_handler()}><Save height={undefined} width={undefined} /></a> : undefined}
|
||||||
|
</div>
|
||||||
|
<h1 style="display:inline" class="button header_title">{this.state.title}</h1>
|
||||||
|
<a class="button header_icon_button" onClick={() => delete_handler()}><Trash height={undefined} width={undefined} /></a>
|
||||||
|
</header>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-8 col-lg-6 col-md-offset-2 col-lg-offset-3">
|
||||||
|
<textarea autofocus value={this.text} rows={this.rows} class="doc" style="width:100%;" onKeyDown={evt => this.textAreaKeyPress(evt)} onKeyUp={evt => this.textAreaChange(evt)} ref={elm => {
|
||||||
|
if (elm)
|
||||||
|
setTimeout(() => elm.focus(), 0)
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
57
src/components/routes/vault/EntryList.tsx
Executable file
57
src/components/routes/vault/EntryList.tsx
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
import { h, Component } from "preact"
|
||||||
|
import { IVault, BaseNote } from "../../../notes";
|
||||||
|
import AddButton from "../../AddButton";
|
||||||
|
import Navigation from "../../../navigation";
|
||||||
|
import ArrowLeft from "feather-icons/dist/icons/arrow-left.svg"
|
||||||
|
import MoreVertival from "feather-icons/dist/icons/more-vertical.svg"
|
||||||
|
|
||||||
|
export default class EntryList extends Component<{ vault: Promise<IVault> }, { entries: BaseNote[] }> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = { entries: [] }
|
||||||
|
}
|
||||||
|
vault: IVault;
|
||||||
|
async componentWillMount() {
|
||||||
|
this.vault = await this.props.vault;
|
||||||
|
this.vault.getAllNotes().then(entries => this.setState({ entries }))
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const open_entry = (id: string | null) => {
|
||||||
|
Navigation.setPage("/vault", { id: this.vault.id }, { id, entry: "true" })
|
||||||
|
}
|
||||||
|
|
||||||
|
let elms = this.state.entries.map(entry => {
|
||||||
|
let [first, second] = entry.preview.split("\n", 2);
|
||||||
|
return <div class="vault_vault" onClick={() => {
|
||||||
|
open_entry(entry._id)
|
||||||
|
}}>
|
||||||
|
<span>{first}</span><br />
|
||||||
|
<span>{second}</span>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<a class="button header_icon_button" onClick={() => history.back()}><ArrowLeft height={undefined} width={undefined} /></a>
|
||||||
|
</div>
|
||||||
|
<h1 style="display:inline" class="button header_title" onClick={() => Navigation.setPage("/")}>{this.vault ? this.vault.name : ""}</h1>
|
||||||
|
<a class="button header_icon_button"><MoreVertival height={undefined} width={undefined} /></a>
|
||||||
|
</header>
|
||||||
|
<AddButton onClick={() => open_entry(null)} />
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-8 col-lg-6 col-md-offset-2 col-lg-offset-3">
|
||||||
|
<div class="card fluid">
|
||||||
|
<h1 class="section double-padded">Notes: </h1>
|
||||||
|
<div class="section">
|
||||||
|
{elms}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
37
src/components/routes/vault/Vault.tsx
Executable file
37
src/components/routes/vault/Vault.tsx
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
import { h } from "preact"
|
||||||
|
import { Page } from "../../../page";
|
||||||
|
import Notes, { IVault, BaseNote } from "../../../notes";
|
||||||
|
import Navigation from "../../../navigation";
|
||||||
|
import EntryComponent from "./Entry";
|
||||||
|
import EntryList from "./EntryList";
|
||||||
|
|
||||||
|
import "./vault.scss"
|
||||||
|
|
||||||
|
export interface VaultProps {
|
||||||
|
state: { id: string };
|
||||||
|
hidden: { entry: string, id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class VaultPage extends Page<VaultProps, { entries: BaseNote[] }> {
|
||||||
|
vault: Promise<IVault>
|
||||||
|
constructor(props: VaultProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { entries: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentWillMount() {
|
||||||
|
this.vault = Notes.getVault(this.props.state.id, Notes.getVaultKey(this.props.state.id))
|
||||||
|
|
||||||
|
this.vault.catch(err => {
|
||||||
|
Navigation.setPage("/")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.props.hidden && this.props.hidden.entry === "true") {
|
||||||
|
return <EntryComponent vault={this.vault} id={this.props.hidden.id} />
|
||||||
|
} else {
|
||||||
|
return <EntryList vault={this.vault} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/components/routes/vault/vault.scss
Executable file
27
src/components/routes/vault/vault.scss
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
.vault_vault>span:nth-of-type(1) {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault_vault>span {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault_vault {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-bottom: solid 1px var(--fore-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault_vault:hover {
|
||||||
|
background: var(--nav-hover-back-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault_vault>svg {
|
||||||
|
height: 2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault_vault:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
126
src/components/routes/vaults/Vaults.tsx
Executable file
126
src/components/routes/vaults/Vaults.tsx
Executable file
@ -0,0 +1,126 @@
|
|||||||
|
import { h } from "preact"
|
||||||
|
import { Page } from "../../../page";
|
||||||
|
import Notes, { VaultList } from "../../../notes";
|
||||||
|
import "./vaults.scss"
|
||||||
|
import Lock from "feather-icons/dist/icons/lock.svg";
|
||||||
|
import Unlock from "feather-icons/dist/icons/unlock.svg";
|
||||||
|
import Navigation from "../../../navigation";
|
||||||
|
import { InputModal } from "../../modals/InputModal";
|
||||||
|
import { YesNoModal } from "../../modals/YesNoModal";
|
||||||
|
import AddButton from "../../AddButton";
|
||||||
|
|
||||||
|
export interface VaultsProps {
|
||||||
|
state: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, modal: JSX.Element | undefined }> {
|
||||||
|
constructor(props: VaultsProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { vaults: [], modal: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVaults() {
|
||||||
|
Notes.getVaults().then(vaults => this.setState({ vaults }))
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.updateVaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let elms = this.state.vaults.map(vault => {
|
||||||
|
return <div class="vaults_vault" onClick={() => {
|
||||||
|
const open = () => {
|
||||||
|
Navigation.setPage("/vault", { id: vault.id })
|
||||||
|
}
|
||||||
|
if (vault.encrypted) {
|
||||||
|
let key = Notes.getVaultKey(vault.id);
|
||||||
|
if (key) open()
|
||||||
|
else {
|
||||||
|
let modal = <InputModal title={"Enter password for " + vault.name} type="password" fieldname="Password" onResult={(value) => {
|
||||||
|
if (value === null) this.setState({ modal: undefined });
|
||||||
|
else {
|
||||||
|
let key = Notes.passwordToKey(value);
|
||||||
|
Notes.getVault(vault.id, key).then(() => {
|
||||||
|
let modal = <YesNoModal title="Save permanent?" onResult={(res) => {
|
||||||
|
if (res === undefined)
|
||||||
|
this.setState({ modal: undefined });
|
||||||
|
else {
|
||||||
|
this.setState({ modal: undefined });
|
||||||
|
Notes.saveVaultKey(vault.id, key, res);
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
this.setState({ modal })
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Invalid password!")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
this.setState({ modal })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{vault.encrypted ? <Lock height={undefined} width={undefined} /> : <Unlock height={undefined} width={undefined} />}
|
||||||
|
<span>
|
||||||
|
{vault.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div style={{ marginTop: "-12px", paddingTop: "12px" }} >
|
||||||
|
{this.state.modal}
|
||||||
|
<AddButton onClick={() => {
|
||||||
|
|
||||||
|
let modal = <InputModal title="Enter new name" fieldname="Name" type="text" onResult={(name) => {
|
||||||
|
const create = async (password?: string) => {
|
||||||
|
let key;
|
||||||
|
if (password) {
|
||||||
|
key = Notes.passwordToKey(password)
|
||||||
|
}
|
||||||
|
await Notes.createVault(name, key)
|
||||||
|
this.updateVaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === null) this.setState({ modal: undefined })
|
||||||
|
else {
|
||||||
|
let modal = <YesNoModal title="Encrypt?" onResult={(encrypted) => {
|
||||||
|
if (encrypted === null) this.setState({ modal: undefined })
|
||||||
|
if (encrypted) {
|
||||||
|
let modal = <InputModal title="Enter new password" fieldname="Password" type="password" onResult={(password) => {
|
||||||
|
if (password === null) this.setState({ modal: undefined })
|
||||||
|
else {
|
||||||
|
create(password)
|
||||||
|
this.setState({ modal: undefined });
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
this.setState({ modal })
|
||||||
|
} else {
|
||||||
|
create()
|
||||||
|
this.setState({ modal: undefined });
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
this.setState({ modal })
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
this.setState({ modal })
|
||||||
|
}} />
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-8 col-lg-6 col-md-offset-2 col-lg-offset-3">
|
||||||
|
<div class="card fluid">
|
||||||
|
|
||||||
|
<h1 class="section double-padded">Your vaults:</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
{elms}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
21
src/components/routes/vaults/vaults.scss
Executable file
21
src/components/routes/vaults/vaults.scss
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
.vaults_vault>span {
|
||||||
|
font-size: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaults_vault {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-bottom: solid 1px var(--fore-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaults_vault:hover {
|
||||||
|
background: var(--nav-hover-back-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaults_vault>svg {
|
||||||
|
height: 2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaults_vault:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
24
src/components/routing.scss
Executable file
24
src/components/routing.scss
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
.transition_container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--back-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_slidein {
|
||||||
|
animation-name: slidein;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
z-index: 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slidein {
|
||||||
|
from {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
}
|
55
src/helper/base64.ts
Executable file
55
src/helper/base64.ts
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
|
||||||
|
// Use a lookup table to find the index.
|
||||||
|
var lookup = new Uint8Array(256);
|
||||||
|
for (var i = 0; i < chars.length; i++) {
|
||||||
|
lookup[chars.charCodeAt(i)] = i;
|
||||||
|
}
|
||||||
|
export function encode(arraybuffer: ArrayBuffer | Uint8Array) {
|
||||||
|
var bytes = new Uint8Array(arraybuffer),
|
||||||
|
i, len = bytes.length, base64 = "";
|
||||||
|
|
||||||
|
for (i = 0; i < len; i += 3) {
|
||||||
|
base64 += chars[bytes[i] >> 2];
|
||||||
|
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
||||||
|
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
||||||
|
base64 += chars[bytes[i + 2] & 63];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((len % 3) === 2) {
|
||||||
|
base64 = base64.substring(0, base64.length - 1) + "=";
|
||||||
|
} else if (len % 3 === 1) {
|
||||||
|
base64 = base64.substring(0, base64.length - 2) + "==";
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function decode(base64: string) {
|
||||||
|
var bufferLength = base64.length * 0.75,
|
||||||
|
len = base64.length, i, p = 0,
|
||||||
|
encoded1, encoded2, encoded3, encoded4;
|
||||||
|
|
||||||
|
if (base64[base64.length - 1] === "=") {
|
||||||
|
bufferLength--;
|
||||||
|
if (base64[base64.length - 2] === "=") {
|
||||||
|
bufferLength--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var arraybuffer = new ArrayBuffer(bufferLength),
|
||||||
|
bytes = new Uint8Array(arraybuffer);
|
||||||
|
|
||||||
|
for (i = 0; i < len; i += 4) {
|
||||||
|
encoded1 = lookup[base64.charCodeAt(i)];
|
||||||
|
encoded2 = lookup[base64.charCodeAt(i + 1)];
|
||||||
|
encoded3 = lookup[base64.charCodeAt(i + 2)];
|
||||||
|
encoded4 = lookup[base64.charCodeAt(i + 3)];
|
||||||
|
|
||||||
|
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
||||||
|
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
||||||
|
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
104
src/helper/indexeddb.ts
Executable file
104
src/helper/indexeddb.ts
Executable file
@ -0,0 +1,104 @@
|
|||||||
|
import Lock from "./lock";
|
||||||
|
|
||||||
|
import { DB, openDb, Transaction } from "idb";
|
||||||
|
|
||||||
|
export default class IDB {
|
||||||
|
initLock = new Lock();
|
||||||
|
db: DB;
|
||||||
|
constructor(database: string, private stores: string[]) {
|
||||||
|
let lock = this.initLock.getLock();
|
||||||
|
lock.then(async l => {
|
||||||
|
|
||||||
|
let v = localStorage.getItem(database + "_version");
|
||||||
|
let version = 0;
|
||||||
|
if (v) version = Number(v)
|
||||||
|
let lastStoresS = localStorage.getItem(database + "_stores");
|
||||||
|
if (!lastStoresS) lastStoresS = "";
|
||||||
|
let lastStores = lastStoresS.split(",").filter(e => e !== "");
|
||||||
|
|
||||||
|
if (!stores.every(e => lastStores.indexOf(e) >= 0) || !lastStores.every(e => stores.indexOf(e) >= 0)) version++;
|
||||||
|
|
||||||
|
localStorage.setItem(database + "_version", version.toString());
|
||||||
|
localStorage.setItem(database + "_stores", stores.join(","));
|
||||||
|
|
||||||
|
this.db = await openDb(database, version, db => {
|
||||||
|
console.log("IndexedDB need update")
|
||||||
|
stores.forEach(store => {
|
||||||
|
if (!db.objectStoreNames.contains(store))
|
||||||
|
db.createObjectStore(store);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
console.log("Got DATABASE", this.db)
|
||||||
|
l.release();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(...stores: string[] | { name: string }[]) {
|
||||||
|
if (stores.length < 1) stores = this.stores;
|
||||||
|
let s: string[];
|
||||||
|
if (typeof stores[0] === "string")
|
||||||
|
s = <any>stores
|
||||||
|
else
|
||||||
|
s = (<any>stores).map(e => e.name)
|
||||||
|
|
||||||
|
return this.db.transaction(s, "readwrite")
|
||||||
|
}
|
||||||
|
|
||||||
|
getStore<T = any>(name: string) {
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
transaction: () => {
|
||||||
|
return this.db.transaction(name, "readwrite")
|
||||||
|
},
|
||||||
|
get: async (key: string, transaction?: Transaction): Promise<T> => {
|
||||||
|
(await this.initLock.getLock()).release()
|
||||||
|
return (transaction || this.db.transaction(name))
|
||||||
|
.objectStore(name).get(key);
|
||||||
|
},
|
||||||
|
|
||||||
|
getAll: async (transaction?: Transaction): Promise<T[]> => {
|
||||||
|
(await this.initLock.getLock()).release()
|
||||||
|
return (transaction || this.db.transaction(name))
|
||||||
|
.objectStore(name).getAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
set: async (key: string, val: T, transaction?: Transaction) => {
|
||||||
|
(await this.initLock.getLock()).release()
|
||||||
|
const tx = (transaction || this.db.transaction(name, "readwrite"));
|
||||||
|
tx.objectStore(name).put(val, key);
|
||||||
|
return tx.complete;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async (key: string, transaction?: Transaction) => {
|
||||||
|
(await this.initLock.getLock()).release()
|
||||||
|
const tx = (transaction || this.db.transaction(name, "readwrite"));
|
||||||
|
tx.objectStore(name).delete(key);
|
||||||
|
return tx.complete;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: async (transaction?: Transaction) => {
|
||||||
|
(await this.initLock.getLock()).release()
|
||||||
|
const tx = (transaction || this.db.transaction(name, "readwrite"));
|
||||||
|
tx.objectStore(name).clear();
|
||||||
|
return tx.complete;
|
||||||
|
},
|
||||||
|
|
||||||
|
keys: async (transaction?: Transaction): Promise<string[]> => {
|
||||||
|
(await this.initLock.getLock()).release()
|
||||||
|
const tx = (transaction || this.db.transaction(name));
|
||||||
|
const keys: string[] = [];
|
||||||
|
const store = tx.objectStore(name);
|
||||||
|
|
||||||
|
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
|
||||||
|
// openKeyCursor isn't supported by Safari, so we fall back
|
||||||
|
(store.iterateKeyCursor || store.iterateCursor).call(store, (cursor: any) => {
|
||||||
|
if (!cursor) return;
|
||||||
|
keys.push(cursor.key);
|
||||||
|
cursor.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
return tx.complete.then(() => keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src/helper/lock.ts
Executable file
36
src/helper/lock.ts
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
export type Release = { release: () => void };
|
||||||
|
export default class Lock {
|
||||||
|
private _locked: boolean = false;
|
||||||
|
get locked() {
|
||||||
|
return this._locked;
|
||||||
|
}
|
||||||
|
private toCome: (() => void)[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.release = this.release.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLock(): Promise<Release> {
|
||||||
|
if (!this._locked) return { release: this.lock() };
|
||||||
|
else {
|
||||||
|
return new Promise<Release>((resolve) => {
|
||||||
|
this.toCome.push(() => {
|
||||||
|
resolve({ release: this.lock() });
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lock() {
|
||||||
|
this._locked = true;
|
||||||
|
return this.release;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async release() {
|
||||||
|
if (this.toCome.length > 0) {
|
||||||
|
this.toCome.shift()();
|
||||||
|
} else {
|
||||||
|
this._locked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/helper/observable.ts
Executable file
46
src/helper/observable.ts
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
export type ObserverCallback<T> = (data: T[]) => void;
|
||||||
|
|
||||||
|
export default class Observable<T = any> {
|
||||||
|
private subscriber: { callback: ObserverCallback<T>, one: boolean }[] = [];
|
||||||
|
private events: T[] = [];
|
||||||
|
private timeout = undefined;
|
||||||
|
|
||||||
|
constructor(private collect: boolean = true, private collect_intervall: number = 100) { }
|
||||||
|
|
||||||
|
getPublicApi() {
|
||||||
|
return {
|
||||||
|
subscribe: (callback: ObserverCallback<T>, one: boolean = false) => {
|
||||||
|
let oldcb = this.subscriber.find(e => e.callback === callback);
|
||||||
|
if (oldcb)
|
||||||
|
oldcb.one = one
|
||||||
|
else
|
||||||
|
this.subscriber.push({ callback, one })
|
||||||
|
},
|
||||||
|
unsubscribe: (callback: ObserverCallback<T>) => {
|
||||||
|
let idx = this.subscriber.findIndex(e => e.callback === callback);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.subscriber.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: T) {
|
||||||
|
if (!this.collect)
|
||||||
|
this.subscriber.forEach(e => e.callback([data]));
|
||||||
|
else {
|
||||||
|
this.events.push(data);
|
||||||
|
if (!this.timeout) {
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.subscriber.forEach(cb => {
|
||||||
|
if (cb.one)
|
||||||
|
this.events.forEach(e => cb.callback([e]));
|
||||||
|
else
|
||||||
|
cb.callback(this.events)
|
||||||
|
});
|
||||||
|
this.timeout = 0;
|
||||||
|
}, this.collect_intervall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
src/helper/swipe.tsx
Executable file
77
src/helper/swipe.tsx
Executable file
@ -0,0 +1,77 @@
|
|||||||
|
import { Component, h, cloneElement } from 'preact';
|
||||||
|
|
||||||
|
export default class SwipeRecognizer extends Component<{ onSwipe?: (direction: string) => void }, { swipe: string }> {
|
||||||
|
private tolerance = 100;
|
||||||
|
private gesture = { x: [], y: [], match: '' };
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.base.addEventListener('touchstart', this.capture);
|
||||||
|
this.base.addEventListener('touchmove', this.capture);
|
||||||
|
this.base.addEventListener('touchend', this.compute)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.base.removeEventListener('touchstart', this.capture);
|
||||||
|
this.base.removeEventListener('touchmove', this.capture);
|
||||||
|
this.base.removeEventListener('touchend', this.compute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private capture = (event: TouchEvent) => {
|
||||||
|
// event.preventDefault()
|
||||||
|
this.gesture.x.push(event.touches[0].clientX)
|
||||||
|
this.gesture.y.push(event.touches[0].clientY)
|
||||||
|
};
|
||||||
|
|
||||||
|
private compute = (event: TouchEvent) => {
|
||||||
|
// event.preventDefault();
|
||||||
|
let xStart = this.gesture.x[0];
|
||||||
|
let yStart = this.gesture.y[0];
|
||||||
|
let xEnd = this.gesture.x.pop();
|
||||||
|
let yEnd = this.gesture.y.pop();
|
||||||
|
let xTravel = xEnd - xStart;
|
||||||
|
let yTravel = yEnd - yStart;
|
||||||
|
|
||||||
|
console.log(xTravel, yTravel);
|
||||||
|
let xTravel_b = xTravel < 0 ? xTravel * (-1) : xTravel;
|
||||||
|
let yTravel_b = yTravel < 0 ? yTravel * (-1) : yTravel;
|
||||||
|
console.log(xTravel_b, yTravel_b);
|
||||||
|
if (xTravel_b > yTravel_b) {
|
||||||
|
if (xTravel_b > this.tolerance) {
|
||||||
|
if (xTravel > 0) {
|
||||||
|
this.gesture.match = "right";
|
||||||
|
} else {
|
||||||
|
this.gesture.match = "left";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (yTravel_b > this.tolerance) {
|
||||||
|
if (yTravel > 0) {
|
||||||
|
this.gesture.match = "down";
|
||||||
|
} else {
|
||||||
|
this.gesture.match = "up";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(this.gesture.match);
|
||||||
|
if (this.gesture.match !== '') {
|
||||||
|
this.onSwipe(this.gesture.match);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gesture.x = []
|
||||||
|
this.gesture.y = []
|
||||||
|
this.gesture.match = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
onSwipe = (direction: string) => {
|
||||||
|
if (this.props.onSwipe) {
|
||||||
|
this.props.onSwipe(direction);
|
||||||
|
}
|
||||||
|
this.setState({ swipe: direction });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
render({ children }, state) {
|
||||||
|
return cloneElement(children[0], state);
|
||||||
|
}
|
||||||
|
}
|
44
src/index.html
Executable file
44
src/index.html
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>SecureNotes</title>
|
||||||
|
<meta charset="utf8" />
|
||||||
|
<meta name="Description" content="Notes app">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<link rel="manifest" href="/public/manifest.json">
|
||||||
|
<!-- <link rel="shortcut icon" href="/public/icon-72x72.png"> -->
|
||||||
|
|
||||||
|
<!-- Add to home screen for Safari on iOS -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="eBook">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/public/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/public/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/public/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/public/site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="/public/safari-pinned-tab.svg" color="#3E9AE9">
|
||||||
|
<meta name="msapplication-TileColor" content="#3E9AE9">
|
||||||
|
<meta name="theme-color" content="#3E9AE9">
|
||||||
|
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css"> -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You have to enable JavaScript to use this site!
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script>
|
||||||
|
// Check that service workers are registered
|
||||||
|
// if ('serviceWorker' in navigator) {
|
||||||
|
// // Use the window load event to keep the page load performant
|
||||||
|
// window.addEventListener('load', () => {
|
||||||
|
// navigator.serviceWorker.register('/sw.js');
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
44
src/index.scss
Executable file
44
src/index.scss
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css?family=Roboto');
|
||||||
|
@import "./vars.scss";
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif; // font-family: Verdana, Geneva, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .feather {
|
||||||
|
// width: 24px;
|
||||||
|
// height: 24px;
|
||||||
|
// stroke: currentColor;
|
||||||
|
// stroke-width: 2;
|
||||||
|
// stroke-linecap: round;
|
||||||
|
// stroke-linejoin: round;
|
||||||
|
// fill: none;
|
||||||
|
// }
|
||||||
|
.header_icon_button {
|
||||||
|
height: $header_icon_width;
|
||||||
|
width: $header_icon_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header_title {
|
||||||
|
max-width: calc(100% - #{$header_icon_width} - #{$header_icon_width});
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: calc(1.5 * var(--universal-padding));
|
||||||
|
}
|
43
src/index.tsx
Executable file
43
src/index.tsx
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
import { h, render } from 'preact';
|
||||||
|
import App from './components/App';
|
||||||
|
|
||||||
|
// import "mini.css/src/flavors/mini-dark.scss"
|
||||||
|
import "mini.css/src/flavors/mini-default.scss"
|
||||||
|
import "./index.scss"
|
||||||
|
import Navigation from './navigation';
|
||||||
|
import VaultsPage from './components/routes/vaults/Vaults';
|
||||||
|
import { Page } from './page';
|
||||||
|
|
||||||
|
import Notes from "./notes"
|
||||||
|
import DemoPage from './components/demo';
|
||||||
|
import VaultPage from './components/routes/vault/Vault';
|
||||||
|
console.log(Notes);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Initialize notes provider
|
||||||
|
if (Notes.loginRequired()) {
|
||||||
|
let url = new URL(location.href)
|
||||||
|
let code = url.searchParams.get("code");
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
let err = await Notes.getToken(code)
|
||||||
|
if (err) {
|
||||||
|
alert("Login failed: " + err)
|
||||||
|
Notes.login()
|
||||||
|
} else {
|
||||||
|
window.history.replaceState(null, document.title, "/" + window.location.hash);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Notes.login()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Notes.start();
|
||||||
|
|
||||||
|
Navigation.default = VaultsPage as typeof Page;
|
||||||
|
Navigation.addPage("/vault", VaultPage as typeof Page)
|
||||||
|
Navigation.addPage("/demo", DemoPage as typeof Page)
|
||||||
|
Navigation.start();
|
||||||
|
|
||||||
|
render(<App />, document.body, document.getElementById('app'));
|
||||||
|
})()
|
112
src/navigation.ts
Executable file
112
src/navigation.ts
Executable file
@ -0,0 +1,112 @@
|
|||||||
|
import Observable from "./helper/observable";
|
||||||
|
import { Page, PageProps } from "./page";
|
||||||
|
import { h } from "preact";
|
||||||
|
|
||||||
|
function serializQuery(obj: any) {
|
||||||
|
var str = [];
|
||||||
|
for (var p in obj)
|
||||||
|
if (obj.hasOwnProperty(p)) {
|
||||||
|
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
|
||||||
|
}
|
||||||
|
return str.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseQuery(query: string) {
|
||||||
|
let data: any = {};
|
||||||
|
if (query.startsWith("?")) query = query.slice(1)
|
||||||
|
query.split("&").forEach(e => {
|
||||||
|
let [key, value] = e.split("=");
|
||||||
|
key = decodeURIComponent(key)
|
||||||
|
value = decodeURIComponent(value)
|
||||||
|
data[key] = value
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default class Navigation {
|
||||||
|
private static _pages: Map<string, typeof Page> = new Map();
|
||||||
|
private static _page: { route: string, page: JSX.Element };
|
||||||
|
private static pageObservableServer = new Observable<JSX.Element>(false);
|
||||||
|
public static pageObservable = Navigation.pageObservableServer.getPublicApi();
|
||||||
|
private static _state: { [key: string]: any };
|
||||||
|
private static _hidden_state: { [key: string]: any };
|
||||||
|
|
||||||
|
public static addPage(route: string, comp: typeof Page) {
|
||||||
|
Navigation._pages.set(route, comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public static get state() {
|
||||||
|
// return Navigation._state;
|
||||||
|
// }
|
||||||
|
|
||||||
|
public static set default(comp: typeof Page) {
|
||||||
|
console.log("Set default");
|
||||||
|
Navigation._pages.set("/", comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static set not_found(comp: typeof Page) {
|
||||||
|
Navigation._pages.set("/404", comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setPage(route: string, state?: { [key: string]: string }, hidden?: { [key: string]: string }, replace?: boolean) {
|
||||||
|
let component = Navigation._pages.get(route);
|
||||||
|
if (!component && route !== "/404") {
|
||||||
|
Navigation.setPage("/404", { route, key: "404" })
|
||||||
|
} else {
|
||||||
|
if (!Navigation.page || Navigation.page.route !== route || JSON.stringify(state) !== JSON.stringify(Navigation._state) || JSON.stringify(Navigation._hidden_state) !== JSON.stringify(hidden)) {
|
||||||
|
let s = "";
|
||||||
|
if (state) {
|
||||||
|
s = "?" + serializQuery(state)
|
||||||
|
}
|
||||||
|
let newhash = "#" + route + s;
|
||||||
|
let newkey = newhash + serializQuery(hidden)
|
||||||
|
let whash = window.location.hash;
|
||||||
|
if (!whash || whash === "") whash = "#/"
|
||||||
|
let oldkey = whash + serializQuery(history.state);
|
||||||
|
if (newkey !== oldkey) {
|
||||||
|
if (replace)
|
||||||
|
window.history.replaceState(hidden, document.title, newhash);
|
||||||
|
else
|
||||||
|
window.history.pushState(hidden, document.title, newhash);
|
||||||
|
}
|
||||||
|
let page = h(component as any, { state: state, key: newhash + serializQuery(hidden), hidden: hidden });
|
||||||
|
|
||||||
|
Navigation._state = state;
|
||||||
|
Navigation._hidden_state = hidden;
|
||||||
|
Navigation._page = { page, route };
|
||||||
|
Navigation.pageObservableServer.send(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get page() {
|
||||||
|
return Navigation._page;
|
||||||
|
}
|
||||||
|
|
||||||
|
static onHashChange(hidden_state: { [key: string]: string }) {
|
||||||
|
let hash = window.location.hash.substring(1);
|
||||||
|
if (hash && hash !== "") {
|
||||||
|
let [route, state] = hash.split("?");
|
||||||
|
let s;
|
||||||
|
if (state) {
|
||||||
|
try {
|
||||||
|
s = parseQuery(state)
|
||||||
|
} catch (err) {
|
||||||
|
s = undefined;
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Navigation.setPage(route, s, hidden_state)
|
||||||
|
} else {
|
||||||
|
Navigation.setPage("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static start() {
|
||||||
|
window.addEventListener("popstate", (ev) => {
|
||||||
|
Navigation.onHashChange(ev.state);
|
||||||
|
})
|
||||||
|
Navigation.onHashChange(undefined);
|
||||||
|
}
|
||||||
|
}
|
531
src/notes.ts
Executable file
531
src/notes.ts
Executable file
@ -0,0 +1,531 @@
|
|||||||
|
import Observable from "./helper/observable";
|
||||||
|
import * as config from "../config.json"
|
||||||
|
import Lock from "./helper/lock"
|
||||||
|
|
||||||
|
import SecureFile, { IFile } from "@hibas123/secure-file-wrapper";
|
||||||
|
import * as b64 from "./helper/base64"
|
||||||
|
|
||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(public status: number, public statusText: string) {
|
||||||
|
super(statusText);
|
||||||
|
console.log(statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MessageType {
|
||||||
|
INFO,
|
||||||
|
WARNING,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 * as AES from "crypto-js/aes"
|
||||||
|
import * as aesjs from "aes-js";
|
||||||
|
// import { } from "js-sha512"
|
||||||
|
import { sha256 } from "js-sha256"
|
||||||
|
import clonedeep = require("lodash.clonedeep");
|
||||||
|
import * as uuidv4 from "uuid/v4"
|
||||||
|
import IDB from "./helper/indexeddb";
|
||||||
|
import { Transaction } from "idb";
|
||||||
|
|
||||||
|
console.log(aesjs)
|
||||||
|
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<BaseNote[]>;
|
||||||
|
searchNote(term: string): Promise<BaseNote[]>
|
||||||
|
newNote(): ViewNote;
|
||||||
|
saveNote(note: ViewNote): Promise<void>;
|
||||||
|
getNote(id: string): Promise<ViewNote>;
|
||||||
|
deleteNote(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotesProvider {
|
||||||
|
private notesObservableServer = new Observable<Note>(true)
|
||||||
|
public notesObservable = this.notesObservableServer.getPublicApi()
|
||||||
|
|
||||||
|
public messageObservableServer = new Observable<{ message: string, type: MessageType }>(false)
|
||||||
|
public messageObservable = this.messageObservableServer.getPublicApi()
|
||||||
|
|
||||||
|
public sendErrorMessage(error: Error) {
|
||||||
|
this.messageObservableServer.send({ message: error.message, type: MessageType.ERROR })
|
||||||
|
}
|
||||||
|
|
||||||
|
private database = new IDB("notes", ["notes", "oplog"]);
|
||||||
|
private noteDB = this.database.getStore<DBNote>("notes");
|
||||||
|
private oplogDB = this.database.getStore<{ id: string, logs: OpLog[] }>("oplog");
|
||||||
|
|
||||||
|
private vaultKeys = new Map<string, Uint8Array>();
|
||||||
|
|
||||||
|
|
||||||
|
public apiLock = new Lock();
|
||||||
|
public apiLockRls = this.apiLock.getLock()
|
||||||
|
|
||||||
|
private syncLock = new Lock();
|
||||||
|
|
||||||
|
private _secureFile: SecureFile;
|
||||||
|
|
||||||
|
private generalEncryption: Uint8Array = undefined;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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]) => {
|
||||||
|
let jwt = await this.getJWT();
|
||||||
|
callback(jwt);
|
||||||
|
})
|
||||||
|
|
||||||
|
let key = localStorage.getItem("enc_key");
|
||||||
|
if (key) {
|
||||||
|
this.generalEncryption = b64.decode(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
return this.apiLockRls.then(lock => lock.release()).then(() => { this.sync() })
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.messageObservableServer.send({ message: "offline", type: MessageType.INFO });
|
||||||
|
throw new Error("Need login!")
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sync() {
|
||||||
|
const lock = await this.syncLock.getLock()
|
||||||
|
const log = (...message: any[]) => {
|
||||||
|
console.log("[SYNC]: ", ...message)
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
|
||||||
|
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");
|
||||||
|
await this._secureFile.update(id, op.values.value, b64.encode(op.values.preview), op.date, old);
|
||||||
|
break;
|
||||||
|
case OpLogType.DELETE:
|
||||||
|
log(id, "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");
|
||||||
|
await this._secureFile.create(
|
||||||
|
"",
|
||||||
|
op.values.value,
|
||||||
|
"binary",
|
||||||
|
local.folder,
|
||||||
|
b64.encode(op.values.preview),
|
||||||
|
id,
|
||||||
|
op.date
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
log(id, "LOCAL CREATAE/UPDATE");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
log(id, "DO NOTHING");
|
||||||
|
} else {
|
||||||
|
await create()
|
||||||
|
}
|
||||||
|
} else if (!remote && local && !oplog) { // No local changes, but remote deleted
|
||||||
|
log("LOCAL DELETE");
|
||||||
|
await this.noteDB.delete(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 (local) { //If local changes and remote exist
|
||||||
|
if (old)
|
||||||
|
await create() // Will recreate local entry
|
||||||
|
await apply(old) // Will apply changes to remote
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!local) { //If local ist deleted but remote exists check what is newer
|
||||||
|
// if (old)
|
||||||
|
// await create()
|
||||||
|
// await apply(old)
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
log(id, "DO NOTHING");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.messageObservableServer.send({ message: "Error syncing: " + id, type: MessageType.ERROR })
|
||||||
|
}
|
||||||
|
await this.oplogDB.delete(id);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
log("Finished")
|
||||||
|
lock.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<VaultList> {
|
||||||
|
return this.noteDB.getAll()
|
||||||
|
.then(notes => notes
|
||||||
|
.filter(e => Decoder.decode(e.preview) === "__VAULT__")
|
||||||
|
.map(e => {
|
||||||
|
let value = e.__value;
|
||||||
|
let encrypted = false;
|
||||||
|
if (Decoder.decode(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
|
||||||
|
}, tx),
|
||||||
|
this.noteDB.set(vault._id, vault)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getVault(vault_id: string, key?: Uint8Array): Promise<IVault> {
|
||||||
|
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 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) 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 }, transaction?: Transaction) {
|
||||||
|
let tx = transaction || Notes.oplogDB.transaction();
|
||||||
|
let oplog = await Notes.oplogDB.get(note_id, tx);
|
||||||
|
if (!oplog) oplog = { logs: [], id: note_id };
|
||||||
|
oplog.logs.push({
|
||||||
|
date: new Date(),
|
||||||
|
type,
|
||||||
|
values
|
||||||
|
})
|
||||||
|
await Notes.oplogDB.set(note_id, oplog, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.map<BaseNote>(e => {
|
||||||
|
let new_note = clonedeep(<Note>e) as BaseNote
|
||||||
|
delete (<any>new_note).__value
|
||||||
|
new_note.preview = this.decrypt(e.preview)
|
||||||
|
return new_note;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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>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)
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
Notes.addop(note._id, !old_note ? OpLogType.CREATE : OpLogType.CHANGE, {
|
||||||
|
value: new_note.__value,
|
||||||
|
preview: new_note.preview
|
||||||
|
}, tx),
|
||||||
|
Notes.noteDB.set(note._id, new_note, tx)
|
||||||
|
])
|
||||||
|
lock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNote(id: string): Promise<ViewNote> {
|
||||||
|
let note = await Notes.noteDB.get(id);
|
||||||
|
if (!note) return undefined;
|
||||||
|
let new_note = clonedeep(<Note>note) as ViewNote;
|
||||||
|
new_note.__value = this.decrypt(note.__value);
|
||||||
|
return new_note;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNote(id: string) {
|
||||||
|
let lock = await Notes.syncLock.getLock();
|
||||||
|
let tx = Notes.database.transaction(Notes.oplogDB, Notes.noteDB)
|
||||||
|
await Promise.all([
|
||||||
|
Notes.addop(id, OpLogType.DELETE, null, tx),
|
||||||
|
Notes.noteDB.delete(id, tx)
|
||||||
|
])
|
||||||
|
lock.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Notes = new NotesProvider()
|
||||||
|
export default Notes;
|
||||||
|
|
||||||
|
(<any>window).api = Notes
|
6
src/page.ts
Executable file
6
src/page.ts
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
import { Component } from "preact";
|
||||||
|
|
||||||
|
export interface PageProps {
|
||||||
|
state: any;
|
||||||
|
}
|
||||||
|
export abstract class Page<P extends PageProps, S> extends Component<P, S> { }
|
1
src/test.worker.ts
Executable file
1
src/test.worker.ts
Executable file
@ -0,0 +1 @@
|
|||||||
|
console.log("Hello from worker!")
|
2
src/types.d.ts
vendored
Executable file
2
src/types.d.ts
vendored
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
// import { Component } from "preact";
|
||||||
|
declare module "*.svg";
|
1
src/vars.scss
Executable file
1
src/vars.scss
Executable file
@ -0,0 +1 @@
|
|||||||
|
$header_icon_width: calc(3.1875rem + var(--universal-padding) / 2);
|
23
tsconfig.json
Executable file
23
tsconfig.json
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./public",
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"removeComments": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "h",
|
||||||
|
"lib": [
|
||||||
|
"es2015",
|
||||||
|
"dom",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/types.d.ts",
|
||||||
|
"./src/**/*.tsx",
|
||||||
|
"./src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
BIN
webpack-stats.json
Executable file
BIN
webpack-stats.json
Executable file
Binary file not shown.
86
webpack.config.js
Executable file
86
webpack.config.js
Executable file
@ -0,0 +1,86 @@
|
|||||||
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
const Visualizer = require('webpack-visualizer-plugin');
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devServer: {
|
||||||
|
host: "0.0.0.0", // Defaults to `localhost`
|
||||||
|
open: false, // Open the page in browser,
|
||||||
|
contentBase: path.join(__dirname, 'dist'),
|
||||||
|
disableHostCheck: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CopyWebpackPlugin([{ from: "public", to: "public" }]),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
title: 'SecureNotes',
|
||||||
|
template: "./src/index.html"
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "[name].css",
|
||||||
|
chunkFilename: "[id].css"
|
||||||
|
}),
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: "static",
|
||||||
|
openAnalyzer: false
|
||||||
|
}),
|
||||||
|
new Visualizer(),
|
||||||
|
// new WorkboxPlugin.GenerateSW({
|
||||||
|
// swDest: "sw.js",
|
||||||
|
// importWorkboxFrom: 'local',
|
||||||
|
// clientsClaim: true,
|
||||||
|
// // importsDirectory: "/dist",
|
||||||
|
// directoryIndex: 'index.html',
|
||||||
|
// ignoreUrlParametersMatching: [/./],
|
||||||
|
// runtimeCaching: [{
|
||||||
|
// urlPattern: /images/,
|
||||||
|
// handler: 'cacheFirst',
|
||||||
|
// options: {
|
||||||
|
// cacheName: 'images',
|
||||||
|
// expiration: {
|
||||||
|
// maxEntries: 1000,
|
||||||
|
// //Recheck after 30 Days
|
||||||
|
// maxAgeSeconds: 30 * 24 * 60 * 60
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }, {
|
||||||
|
// urlPattern: /api/,
|
||||||
|
// handler: 'networkOnly'
|
||||||
|
// }]
|
||||||
|
// })
|
||||||
|
],
|
||||||
|
entry: "./src/index.tsx",
|
||||||
|
// Enable sourcemaps for debugging webpack's output.
|
||||||
|
devtool: "eval",
|
||||||
|
resolve: {
|
||||||
|
// Add '.ts' and '.tsx' as resolvable extensions.
|
||||||
|
extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
}, {
|
||||||
|
test: /\.(png|jpg)$/,
|
||||||
|
loader: "file-loader",
|
||||||
|
}, {
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
"style-loader", // creates style nodes from JS strings
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
"css-loader", // translates CSS into CommonJS
|
||||||
|
"sass-loader" // compiles Sass to CSS, using Node Sass by default
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: ['preact-svg-loader'],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user