aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Powers <markppowers0@gmail.com>2019-12-26 20:28:52 -0600
committerMark Powers <markppowers0@gmail.com>2019-12-26 20:28:52 -0600
commit60f66450078b0a3a46d4ab373645c2546aab28f4 (patch)
tree94bc6f8146ca5f861396c454218e6960aa46a45e
initial commit
-rw-r--r--.gitignore65
-rw-r--r--README.md26
-rw-r--r--package-lock.json693
-rw-r--r--package.json21
-rw-r--r--src/create-user.js101
-rw-r--r--src/index.html154
-rw-r--r--src/index.js93
-rw-r--r--src/login.html40
-rw-r--r--src/main.js182
-rw-r--r--src/server.js161
-rw-r--r--src/styles.css72
11 files changed, 1608 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e72625d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,65 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# next.js build output
+.next
+
+config.json
+uploads/
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a5f2d47
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# marks.database
+A database frontend to manage movies, books, todos, and recipes/pantry
+
+I've had a local database I've run where I keep track of movies and books I've seen or read.
+It's fairly clunky to use however. I've thought about using php myadmin or something similar,
+but that isn't much easier really. This project is fairly specific, but its a frontend to
+manage the tables that I need. It also includes a recipe manager, which is fairly complex. It
+can keep track of your pantry too, allowing you to move things between owned and a shopping list.
+
+# Installation
+Download the repository, run `npm install`, set up a mysql server with a user and a database,
+fill out a `config.json` file as specified below, and then `npm run run`.
+
+`config.json` should be of the following form
+```
+{
+ "database": {
+ "host": "<DB_HOST>",
+ "user": "<DB_USER>",
+ "database": "<DB_NAME>",
+ "password": "<PASSWORD>"
+ },
+ "port": <PORT_TO_RUN_WEBSERVER_ON>
+}
+
+```
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..8d36346
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,693 @@
+{
+ "name": "marksdatabase",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@types/node": {
+ "version": "12.7.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
+ "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ=="
+ },
+ "accepts": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+ "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+ "requires": {
+ "mime-types": "~2.1.18",
+ "negotiator": "0.6.1"
+ }
+ },
+ "any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "bluebird": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz",
+ "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg=="
+ },
+ "body-parser": {
+ "version": "1.18.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+ "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "requires": {
+ "bytes": "3.0.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "~1.6.3",
+ "iconv-lite": "0.4.23",
+ "on-finished": "~2.3.0",
+ "qs": "6.5.2",
+ "raw-body": "2.3.3",
+ "type-is": "~1.6.16"
+ }
+ },
+ "buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+ },
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ },
+ "cls-bluebird": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz",
+ "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=",
+ "requires": {
+ "is-bluebird": "^1.0.2",
+ "shimmer": "^1.1.0"
+ }
+ },
+ "content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ },
+ "cookie-parser": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz",
+ "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==",
+ "requires": {
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6"
+ }
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "denque": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.0.tgz",
+ "integrity": "sha512-gh513ac7aiKrAgjiIBWZG0EASyDF9p4JMWwKA8YU5s9figrL5SRNEMT6FDynsegakuhWd1wVqTvqvqAoDxw7wQ=="
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "dottie": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz",
+ "integrity": "sha512-ch5OQgvGDK2u8pSZeSYAQaV/lczImd7pMJ7BcEPXmnFVjy4yJIzP6CsODJUTH8mg1tyH1Z2abOiuJO3DjZ/GBw=="
+ },
+ "ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
+ "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+ "requires": {
+ "accepts": "~1.3.5",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.18.3",
+ "content-disposition": "0.5.2",
+ "content-type": "~1.0.4",
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.1.1",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.4",
+ "qs": "6.5.2",
+ "range-parser": "~1.2.0",
+ "safe-buffer": "5.1.2",
+ "send": "0.16.2",
+ "serve-static": "1.13.2",
+ "setprototypeof": "1.1.0",
+ "statuses": "~1.4.0",
+ "type-is": "~1.6.16",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+ "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "statuses": "~1.4.0",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "requires": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+ "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "inflection": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz",
+ "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY="
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "ipaddr.js": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+ "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
+ },
+ "is-bluebird": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz",
+ "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI="
+ },
+ "is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ="
+ },
+ "jsonwebtoken": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
+ "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
+ "requires": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^5.6.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "requires": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "requires": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ },
+ "lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+ },
+ "lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+ },
+ "lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
+ },
+ "lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
+ },
+ "lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+ },
+ "lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
+ },
+ "lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+ },
+ "long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+ },
+ "lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+ },
+ "mime-db": {
+ "version": "1.38.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
+ "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg=="
+ },
+ "mime-types": {
+ "version": "2.1.22",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
+ "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
+ "requires": {
+ "mime-db": "~1.38.0"
+ }
+ },
+ "moment": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+ "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
+ },
+ "moment-timezone": {
+ "version": "0.5.26",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz",
+ "integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==",
+ "requires": {
+ "moment": ">= 2.9.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "mysql2": {
+ "version": "1.6.5",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-1.6.5.tgz",
+ "integrity": "sha512-zedaOOyb3msuuZcJJnxIX/EGOpmljDG7B+UevRH5lqcv+yhy9eCwkArBz8/AO+/rlY3/oCsOdG8R5oD6k0hNfg==",
+ "requires": {
+ "denque": "^1.4.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.4.24",
+ "long": "^4.0.0",
+ "lru-cache": "^4.1.3",
+ "named-placeholders": "^1.1.2",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.1"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ }
+ }
+ },
+ "named-placeholders": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz",
+ "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==",
+ "requires": {
+ "lru-cache": "^4.1.3"
+ }
+ },
+ "negotiator": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "proxy-addr": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
+ "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.8.0"
+ }
+ },
+ "pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
+ },
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ },
+ "range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+ },
+ "raw-body": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+ "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+ "requires": {
+ "bytes": "3.0.0",
+ "http-errors": "1.6.3",
+ "iconv-lite": "0.4.23",
+ "unpipe": "1.0.0"
+ }
+ },
+ "readline-sync": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
+ "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
+ },
+ "retry-as-promised": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz",
+ "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==",
+ "requires": {
+ "any-promise": "^1.3.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "semver": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
+ },
+ "send": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+ "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.6.2",
+ "mime": "1.4.1",
+ "ms": "2.0.0",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.0",
+ "statuses": "~1.4.0"
+ }
+ },
+ "seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4="
+ },
+ "sequelize": {
+ "version": "5.19.5",
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.19.5.tgz",
+ "integrity": "sha512-NdICIFJ0KEjLGrlbnVC3K74tPursZ9yjY5112uCiAxiXK2LXyXP1ePgATEdMJDdeaR5hmP4//qFInTfrnoGe/w==",
+ "requires": {
+ "bluebird": "^3.5.0",
+ "cls-bluebird": "^2.1.0",
+ "debug": "^4.1.1",
+ "dottie": "^2.0.0",
+ "inflection": "1.12.0",
+ "lodash": "^4.17.15",
+ "moment": "^2.24.0",
+ "moment-timezone": "^0.5.21",
+ "retry-as-promised": "^3.2.0",
+ "semver": "^6.3.0",
+ "sequelize-pool": "^2.3.0",
+ "toposort-class": "^1.0.1",
+ "uuid": "^3.3.3",
+ "validator": "^10.11.0",
+ "wkx": "^0.4.8"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+ }
+ }
+ },
+ "sequelize-pool": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz",
+ "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA=="
+ },
+ "serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.2",
+ "send": "0.16.2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ },
+ "shimmer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="
+ },
+ "sqlstring": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
+ "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A="
+ },
+ "statuses": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+ "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ },
+ "toposort-class": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
+ "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg="
+ },
+ "type-is": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+ "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.18"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "uuid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
+ "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
+ },
+ "validator": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz",
+ "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw=="
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "wkx": {
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz",
+ "integrity": "sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4bc399b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "marksdatabase",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "run": "nodejs src/"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "body-parser": "^1.18.3",
+ "cookie-parser": "^1.4.4",
+ "express": "^4.16.4",
+ "jsonwebtoken": "^8.5.1",
+ "mysql2": "^1.6.5",
+ "readline-sync": "^1.4.10",
+ "sequelize": "^5.19.5"
+ }
+}
diff --git a/src/create-user.js b/src/create-user.js
new file mode 100644
index 0000000..899d441
--- /dev/null
+++ b/src/create-user.js
@@ -0,0 +1,101 @@
+const server = require('./server');
+const Sequelize = require('sequelize');
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+const config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json')));
+
+const dbCreds = config.database;
+
+const database = new Sequelize(dbCreds.database, dbCreds.user, dbCreds.password, {
+ logging(str) {
+ console.debug(`DB:${str}`);
+ },
+ dialectOptions: {
+ charset: 'utf8mb4',
+ multipleStatements: true,
+ },
+ // host: dbCreds.host,
+ dialect: 'mysql',
+ pool: {
+ max: 5,
+ min: 0,
+ idle: 10000,
+ },
+});
+
+database.authenticate().then(() => {
+ console.debug(`database connection successful: ${dbCreds.database}`);
+}, (e) => console.log(e));
+
+async function sync(alter, force, callback) {
+ await database.sync({ alter, force, logging: console.log });
+}
+
+function setUpModels() {
+ const models = {
+ "transaction": database.define('transaction', {
+ when: {
+ type: Sequelize.DATE,
+ allowNull: false,
+ },
+ amount: {
+ type: Sequelize.DECIMAL,
+ allowNull: false,
+ },
+ where: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ category: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ subcategory: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ }),
+ "users": database.define('user', {
+ username: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ password: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ salt: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },}),
+ }
+ return models;
+}
+
+const models = setUpModels();
+sync();
+
+function hashWithSalt(password, salt){
+ var hash = crypto.createHmac('sha512', salt);
+ hash.update(password);
+ return hash.digest("base64");
+};
+
+const readline = require('readline-sync');
+let username = readline.question("New username: ");
+let password = readline.question("New password: ");
+let salt = crypto.randomBytes(32).toString("Base64");
+console.log("Salt", salt);
+let hash = hashWithSalt(password, salt)
+console.log("Hash", hash);
+var newUser ={
+ "username": username,
+ "password": hash,
+ "salt": salt
+}
+models.users.create(newUser).then(e =>{
+ console.log("done")
+ console.log(e);
+})
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..0703c8b
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,154 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Mark's Budget</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="shortcut icon" href="/favicon.ico">
+ <!-- <script src="https://cdn.jsdelivr.net/npm/vue"></script> -->
+ <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+ <script src="main.js"></script>
+ <link rel="stylesheet" type="text/css" href="styles.css">
+</head>
+
+<body>
+ <h1>Mark's Budget</h1>
+ <div id="data">
+ <div>
+ <button v-bind:class="{ bold: activeTab == 0 }" v-on:click="setTab(0)">Ledger</button>
+ <button v-bind:class="{ bold: activeTab == 1 }" v-on:click="setTab(1)">Summary</button>
+ </div>
+
+ <!-- Ledger -->
+ <div v-if="activeTab == 0">
+ <div class="newItem">
+ <input v-model="m.when" placeholder="Date" type="date">
+ <input v-model="m.where" placeholder="Where">
+ <input v-model="m.amount" placeholder="Amount" type="number" step="0.01">
+ <input v-model="m.category" placeholder="Category">
+ <input v-model="m.subcategory" placeholder="Subcategory">
+ <button v-on:click="post(m, '/transaction')">Add</button>
+ </div>
+ <table class="table">
+ <tr class="table-header">
+ <th class="table-index"></th>
+ <th>Date</th>
+ <th>Where</th>
+ <th>Amount</th>
+ <th>Category</th>
+ <th>Subcategory</th>
+ <th></th>
+ </tr>
+ <tr v-for="(transaction, i) in transactions">
+ <td class="table-index">{{i+1}}</td>
+ <td>{{transaction.when.substring(0,10)}}</td>
+ <td>{{transaction.where}}</td>
+ <td>{{transaction.amount}}</td>
+ <td>{{transaction.category}}</td>
+ <td>{{transaction.subcategory}}</td>
+ <td>
+ <button v-on:click="prepareEntryEdit(transaction)">&#9881;</button>
+ <button v-on:click="remove(transaction)">X</button>
+ </td>
+ </tr>
+
+ </table>
+ </div v-if="activeTab == 0">
+
+ <table v-if="activeTab == 10">
+ <tr>
+ <th></th>
+ <th>Date</th>
+ <th>Where</th>
+ <th>Amount</th>
+ <th>Category</th>
+ <th>Subcategory</th>
+
+ </tr>
+ <tr>
+ <td></td>
+ <td>
+ <input v-model="em.when" placeholder="Date" type="date">
+ </td>
+ <td>
+ <input v-model="em.where" placeholder="Where">
+ </td>
+ <td>
+ <input v-model="em.amount" placeholder="Amount" type="number" step="0.01">
+ </td>
+ <td>
+ <input v-model="em.category" placeholder="Category">
+ </td>
+ <td>
+ <input v-model="em.subcategory" placeholder="Subcategory">
+ </td>
+ <td><button v-on:click="updateMany(em);activeTab=0;">Update</button></td>
+ </tr>
+ </table>
+
+ <!-- Summary -->
+ <div v-if="activeTab == 1">
+ <div class="summary-panel">
+ <h2>Weekly</h2>
+ <table class="table">
+ <tr>
+ <th>Year</th>
+ <th>Week</th>
+ <th>In</th>
+ <th>Out</th>
+ <th>Net</th>
+ </tr>
+ <tr v-for="(data, i) in summary.week">
+ <td>{{data.y}}</td>
+ <td>{{data.w}}</td>
+ <td>{{data.in}}</td>
+ <td>{{data.out}}</td>
+ <td v-bind:class="{'net-negative': data.negative, 'net-positive':data.positive}">{{data.net}}
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div class="summary-panel">
+ <h2>Monthly</h2>
+ <table class="table">
+ <tr>
+ <th>Year</th>
+ <th>Month</th>
+ <th>In</th>
+ <th>Out</th>
+ <th>Net</th>
+ </tr>
+ <tr v-for="(data, i) in summary.month">
+ <td>{{data.y}}</td>
+ <td>{{data.m}}</td>
+ <td>{{data.in}}</td>
+ <td>{{data.out}}</td>
+ <td v-bind:class="{'net-negative': data.negative, 'net-positive':data.positive}">{{data.net}}
+ </td v-bind:class="{'net-negative': data.negative, 'net-positive':data.positive}">
+ </tr>
+ </table>
+ </div>
+ <div class="summary-panel">
+ <h2>Yearly</h2>
+ <table class="table">
+ <tr>
+ <th>Year</th>
+ <th>In</th>
+ <th>Out</th>
+ <th>Net</th>
+ </tr>
+ <tr v-for="(data, i) in summary.year">
+ <td>{{data.y}}</td>
+ <td>{{data.in}}</td>
+ <td>{{data.out}}</td>
+ <td v-bind:class="{'net-negative': data.negative, 'net-positive':data.positive}">{{data.net}}
+ </td v-bind:class="{'net-negative': data.negative, 'net-positive':data.positive}">
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..01de277
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,93 @@
+const server = require('./server');
+const Sequelize = require('sequelize');
+const fs = require('fs');
+const path = require('path');
+const jwt = require('jsonwebtoken');
+
+
+const config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json')));
+
+const dbCreds = config.database;
+const secret = config.jwt_secret;
+
+const jwtFunctions = {
+ sign: function (message) {
+ return jwt.sign({ value: message }, secret);
+ },
+ verify: function (token) {
+ return jwt.verify(token, secret).value;
+ }
+}
+
+const database = new Sequelize(dbCreds.database, dbCreds.user, dbCreds.password, {
+ logging(str) {
+ console.debug(`DB:${str}`);
+ },
+ dialectOptions: {
+ charset: 'utf8mb4',
+ multipleStatements: true,
+ },
+ // host: dbCreds.host,
+ dialect: 'mysql',
+ pool: {
+ max: 5,
+ min: 0,
+ idle: 10000,
+ },
+});
+
+database.authenticate().then(() => {
+ console.debug(`database connection successful: ${dbCreds.database}`);
+}, (e) => console.log(e));
+
+async function sync(alter, force, callback) {
+ await database.sync({ alter, force, logging: console.log });
+}
+
+function setUpModels() {
+ const models = {
+ "transaction": database.define('transaction', {
+ when: {
+ type: Sequelize.DATE,
+ allowNull: false,
+ },
+ amount: {
+ type: Sequelize.DECIMAL,
+ allowNull: false,
+ },
+ where: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ category: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ subcategory: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ }),
+ "users": database.define('user', {
+ username: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ password: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ salt: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },}),
+ }
+ return models;
+}
+
+const models = setUpModels();
+sync();
+
+server.setUpRoutes(models, jwtFunctions, database);
+server.listen(config.port);
+
diff --git a/src/login.html b/src/login.html
new file mode 100644
index 0000000..d1dbe1d
--- /dev/null
+++ b/src/login.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Mark's Database - Login</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="stylesheet" type="text/css" href="/css/styles.css">
+ <script>
+ function sendPost(){
+ let username = document.getElementById('username').value;
+ let password = document.getElementById('password').value;
+ fetch(new Request("/login", {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({'username':username, 'password':password})
+ }))
+ .then((response) => {
+ console.log(response);
+ window.location = "/";
+ });
+ }
+ </script>
+</head>
+
+<body>
+ <div>
+ <h1>Login</h1>
+ <div class="form">
+ <input type="text" placeholder="Enter Username" name="username" id="username" required>
+ <input type="password" placeholder="Enter Password" name="password" id="password" required>
+ <button onclick="sendPost()">Log in</button>
+ </div>
+ </div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..6dbc3c0
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,182 @@
+window.onload = function () {
+ var transactionData = new Vue({
+ el: '#data',
+ data: {
+ activeTab: 0,
+ transactions: [],
+ summary: {},
+ selTodoType: "all",
+ },
+ methods: {
+ setTab: function (value) {
+ this.activeTab = value;
+ },
+ clearData: function () {
+ this.m = {
+ when: new Date().toLocaleDateString(),
+ where: "",
+ amount: "",
+ category: "",
+ subcategory: "",
+ }
+ this.em = {
+ when: new Date().toLocaleDateString(),
+ where: "",
+ amount: "",
+ category: "",
+ subcategory: "",
+ }
+ },
+ requestThenUpdate: function (request) {
+ fetch(request)
+ .then(response => response.json())
+ .then(response => this.transactions = response);
+ },
+ post: function (obj, path) {
+ console.log(obj);
+ console.log(path);
+ this.requestThenUpdate(new Request(path, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(obj)
+ }));
+ this.clearData();
+ },
+ remove: function (obj) {
+ if (confirm(`Delete transaction?`)) {
+ this.requestThenUpdate(new Request("/transaction", {
+ method: 'delete',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(obj)
+ }))
+ }
+ },
+ prepareEntryEdit: function(transaction){
+ this.em.id=transaction.id;
+ this.em.where=transaction.where;
+ this.em.when=transaction.when;
+ this.em.amount=transaction.amount;
+ this.em.category=transaction.category;
+ this.em.subcategory=transaction.subcategory;
+ this.activeTab=10;
+ },
+ updateMany: function (obj) {
+ update = {}
+ update = obj;
+ this.requestThenUpdate(new Request("/transaction", {
+ method: 'put',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ id: obj.id, update: update })
+ }))
+ },
+ },
+ created() {
+ this.clearData();
+ fetch(new Request(`/transaction`)).then(response => response.json())
+ .then(response => this.transactions = response);
+ fetch(new Request(`/summary`)).then(response => response.json())
+ .then(response => {
+ var findOrCreateWeek = function(t, el){
+ var item = t.summary.week.find( el2 => {
+ return el.y == el2.y && el.w == el2.w
+ })
+ if(!item){
+ item = {y : el.y, w : el.w, in: 0, out: 0, net: 0}
+ t.summary.week.push(item);
+ }
+ return item
+ }
+ var findOrCreateMonth = function(t, el){
+ var item = t.summary.month.find( el2 => {
+ return el.y == el2.y && el.m == el2.m
+ })
+ if(!item){
+ item = {y : el.y, m : el.m, in: 0, out: 0, net: 0}
+ t.summary.month.push(item);
+ }
+ return item
+ }
+ var findOrCreateYear = function(t, el){
+ var item = t.summary.year.find( el2 => {
+ return el.y == el2.y
+ })
+ if(!item){
+ item = {y : el.y, in: 0, out: 0, net: 0}
+ t.summary.year.push(item);
+ }
+ return item
+ }
+
+ this.summary.week = [];
+ this.summary.month = [];
+ this.summary.year = [];
+
+ response.week.in.forEach(el => {
+ findOrCreateWeek(this, el).in = Math.abs(el.s)
+ })
+ response.week.out.forEach(el => {
+ findOrCreateWeek(this, el).out = Math.abs(el.s)
+ })
+ response.week.net.forEach(el => {
+ var item = findOrCreateWeek(this, el);
+ item.net = el.s
+ // Note we flip these since income is negative
+ item.negative = el.s > 0
+ item.positive = el.s < 0
+ })
+
+ response.month.in.forEach(el => {
+ findOrCreateMonth(this, el).in = Math.abs(el.s)
+ })
+ response.month.out.forEach(el => {
+ findOrCreateMonth(this, el).out = Math.abs(el.s)
+ })
+ response.month.net.forEach(el => {
+ var item = findOrCreateMonth(this, el);
+ item.net = el.s
+ // Note we flip these since income is negative
+ item.negative = el.s > 0
+ item.positive = el.s < 0
+ })
+
+ response.year.in.forEach(el => {
+ findOrCreateYear(this, el).in = Math.abs(el.s)
+ })
+ response.year.out.forEach(el => {
+ findOrCreateYear(this, el).out = Math.abs(el.s)
+ })
+ response.year.net.forEach(el => {
+ var item = findOrCreateYear(this, el);
+ item.net = el.s
+ // Note we flip these since income is negative
+ item.negative = el.s > 0
+ item.positive = el.s < 0
+ })
+
+ this.summary.week.sort(function(a, b){
+ if ( a.y == b.y ){ return a.w - b.w; }
+ return a.y-b.y;
+ })
+ this.summary.month.sort(function(a, b){
+ if ( a.y == b.y ){ return a.m - b.m; }
+ return a.y-b.y;
+ })
+ this.summary.year.sort(function(a, b){
+ return a.y-b.y;
+ })
+ });
+ },
+ computed: {
+
+ }
+ });
+} \ No newline at end of file
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 0000000..7251a63
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,161 @@
+const express = require('express');
+const bodyParser = require('body-parser');
+const cookieParser = require('cookie-parser');
+//const request = require('request');
+const crypto = require('crypto');
+
+const path = require('path');
+const fs = require('fs');
+const config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json')));
+
+const server = express();
+server.use(cookieParser())
+server.use(bodyParser.json());
+//server.use(bodyParser.urlencoded({ extended: true }));
+
+function listen(port) {
+ server.listen(port, () => console.info(`Listening on port ${port}!`));
+}
+
+function hashWithSalt(password, salt){
+ var hash = crypto.createHmac('sha512', salt);
+ hash.update(password);
+ return hash.digest("base64");
+};
+
+function setUpRoutes(models, jwtFunctions, database) {
+ // Authentication routine
+ server.use(function (req, res, next) {
+ if (!req.path.toLowerCase().startsWith("/login")) {
+ let cookie = req.cookies.authorization
+ if (!cookie) {
+ console.debug("Redirecting to login - no cookie")
+ res.redirect('/login');
+ return;
+ }
+ try {
+ const decryptedUserId = jwtFunctions.verify(cookie);
+ models.users.findOne({ where: { username: decryptedUserId } }).then((user, error) => {
+ if (user) {
+ res.locals.user = user.get({ plain: true });
+ } else {
+ console.debug("Redirecting to login - invalid cookie")
+ res.redirect('/login');
+ return;
+ }
+ });
+ } catch (e) {
+ res.status(400).send(e.message);
+ }
+ }
+ next();
+ })
+
+ // Route logging
+ server.use(function (req, res, next) {
+ console.debug(new Date(), req.method, req.originalUrl);
+ next()
+ })
+
+ server.get('/', (req, res) => res.sendFile(__dirname + "/index.html"))
+ server.get('/login', (req, res) => res.sendFile(__dirname + "/login.html"))
+ server.get('/styles.css', (req, res) => res.sendFile(__dirname + "/styles.css"))
+ server.get('/main.js', (req, res) => res.sendFile(__dirname + "/main.js"))
+
+ server.post('/login', async (req, res, next) => {
+ const user = await models.users.findOne({ where: { username: req.body.username} })
+ const hash = hashWithSalt(req.body.password, user.salt)
+ if (user.password == hash) {
+ const token = jwtFunctions.sign(user.username);
+ res.cookie('authorization', token, { expires: new Date(Date.now() + (1000 * 60 * 60 * 24 * 30)) });
+ console.debug("Redirecting to page - logged in")
+ res.redirect('/');
+ } else {
+ console.debug("Redirecting to login - invalid login")
+ res.redirect('/login');
+ }
+ })
+
+ server.get(`/transaction`, async (req, res, next) => {
+ try {
+ var result = await database.query("SELECT * FROM transactions ORDER BY `when` DESC", { type: database.QueryTypes.SELECT })
+ res.status(200).send(result);
+ next();
+ } catch (e) {
+ console.log(e)
+ res.status(400).send(e.message);
+ }
+ })
+ server.post(`/transaction`, async (req, res, next) => {
+ try {
+ let item = req.body;
+ console.log(item);
+ await models.transaction.create(item);
+ var result = await database.query("SELECT * FROM transactions ORDER BY `when` DESC", { type: database.QueryTypes.SELECT })
+ res.status(200).send(result);
+ } catch (e) {
+ console.log(e);
+ res.status(400).send(e.message);
+ }
+ })
+ server.delete(`/transaction`, async (req, res, next) => {
+ try {
+ let id = req.body.id;
+ console.log(`Deleting ${id}`);
+ await models.transaction.destroy({ where: { id: id } });
+ var result = await database.query("SELECT * FROM transactions ORDER BY `when` DESC", { type: database.QueryTypes.SELECT })
+ res.status(200).send(result);
+ } catch (e) {
+ console.log(e);
+ res.status(400).send(e.message);
+ }
+ })
+ server.put(`/transaction`, async (req, res, next) => {
+ try {
+ let id = req.body.id;
+ let update = req.body.update;
+ console.log(`Updating ${id}`);
+ var toUpdate = await models.transaction.findOne({ where: { id: id } });
+ console.log(toUpdate)
+ console.log(update)
+ await toUpdate.update(update);
+ var result = await database.query("SELECT * FROM transactions ORDER BY `when` DESC", { type: database.QueryTypes.SELECT })
+ res.status(200).send(result);
+ } catch (e) {
+ console.log(e);
+ res.status(400).send(e.message);
+ }
+ })
+ server.get(`/summary`, async (req, res, next) => {
+ try {
+ res.status(200).send({
+ week: {
+ out: await database.query("SELECT year(`when`) as y, week(`when`) as w, sum(amount) as s FROM transactions where amount > 0 group by year(`when`), WEEK(`when`);", { type: database.QueryTypes.SELECT }),
+ in: await database.query("SELECT year(`when`)as y, week(`when`) as w, sum(amount) as s FROM transactions where amount < 0 group by year(`when`), WEEK(`when`);", { type: database.QueryTypes.SELECT }),
+ net: await database.query("SELECT year(`when`) as y, week(`when`) as w, sum(amount) as s FROM transactions group by year(`when`), WEEK(`when`);", { type: database.QueryTypes.SELECT }),
+ },
+ month: {
+ out: await database.query("SELECT year(`when`) as y, month(`when`) as m, sum(amount) as s FROM transactions where amount > 0 group by year(`when`), month(`when`);", { type: database.QueryTypes.SELECT }),
+ in: await database.query("SELECT year(`when`) as y, month(`when`) as m, sum(amount) as s FROM transactions where amount < 0 group by year(`when`), month(`when`);", { type: database.QueryTypes.SELECT }),
+ net: await database.query("SELECT year(`when`) as y, month(`when`) as m, sum(amount) as s FROM transactions group by year(`when`), month(`when`);", { type: database.QueryTypes.SELECT }),
+ },
+ year: {
+ out: await database.query("SELECT year(`when`) as y, sum(amount) as s FROM transactions where amount > 0 group by year(`when`);", { type: database.QueryTypes.SELECT }),
+ in: await database.query("SELECT year(`when`) as y, sum(amount) as s FROM transactions where amount < 0 group by year(`when`);", { type: database.QueryTypes.SELECT }),
+ net: await database.query("SELECT year(`when`) as y, sum(amount) as s FROM transactions group by year(`when`);", { type: database.QueryTypes.SELECT }),
+ },
+ });
+ next();
+ } catch (e) {
+ console.log(e)
+ res.status(400).send(e.message);
+ }
+ })
+}
+
+module.exports = {
+ listen,
+ setUpRoutes
+};
+
+
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..4eedd0b
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,72 @@
+td {
+ border: 1px solid lightgrey;
+ min-width: 3em;
+}
+
+table {
+ max-width: 100%;
+}
+
+li {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+tr:nth-child(2n+1) {
+ background-color: lightgray;
+}
+tr {
+ width: 100%;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+#data {
+ width: 100%;
+}
+
+.border {
+ border: 1px solid lightgrey;
+}
+
+textarea {
+ border-radius: 4px;
+ width: 60%;
+ height: 10em;
+ display: block;
+}
+
+pre {
+ white-space: pre-line;
+}
+
+.net-negative {
+ /* color: red; */
+ background-color: lightcoral;
+}
+.net-positive {
+ /* color: green; */
+ background-color: lightgreen
+}
+.summary-panel {
+ float:left;
+ padding-right: 2em;
+}
+@media only screen and (max-width: 600px) {
+ .newItem td {
+ display:block;
+ }
+ .table-index {
+ display: none;
+ }
+ button {
+ font-size: 32px;
+ }
+ input {
+ font-size: 32px;
+ display: block;
+ width: 100%;
+ }
+} \ No newline at end of file