├── .gitignore ├── README.md ├── backend ├── index.js ├── model │ ├── accounts.model.js │ └── transactions.model.js ├── package-lock.json ├── package.json └── route │ ├── accounts.js │ └── transactions.js ├── frontend ├── .env ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── actions │ ├── actionCreators.js │ ├── actionCreators.spec.js │ ├── asyncActionCreators.js │ ├── asyncActionCreators.spec.js │ ├── constants.js │ └── index.js │ ├── components │ ├── Account.js │ ├── Logout.js │ ├── Logout.spec.js │ └── TransactionList.js │ ├── containers │ ├── AccountsPage.js │ ├── App.js │ ├── DevTools.js │ ├── Login.js │ ├── NewAccountDialog.js │ ├── Root.dev.js │ ├── Root.js │ ├── Root.prod.js │ ├── TransactionsPage.js │ ├── TransferFundsDialog.js │ ├── UserAccountPage.js │ ├── WelcomePage.js │ └── app.css │ ├── formatMoney.js │ ├── index.js │ ├── middleware │ └── api.js │ ├── reducers │ ├── accounts.js │ ├── accounts.spec.js │ ├── index.js │ ├── login.js │ ├── login.spec.js │ ├── transactions.js │ └── transactions.spec.js │ ├── routes.js │ └── store │ ├── configureStore.dev.js │ ├── configureStore.js │ └── configureStore.prod.js ├── package-lock.json ├── package.json └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | npm-debug.log 4 | .DS_Store 5 | coverage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### React Bank web app clone from the markpritchett repository for our project 2 | 3 | I added below new feature on this. 4 | 5 | 1. Individual user page 6 | 2. Admin page 7 | 3. Migrated backe-end from fake json server to express js rest api server 8 | 4. Connnected this app with local mongoDB database 9 | 5. We will deploying this web-App on GCP linux server. 10 | 11 | Technology Used 12 | 13 | 1. React 14 | 2. Redux 15 | 3. Express Js 16 | 4. MongoDb 17 | 5. GCP 18 | 6. Material UI 19 | 7. React-Router 20 | 21 | **Currently working on connection between bank server to the IBM Blockchain WebAPP** 22 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const cors = require("cors"); 4 | const PORT = process.env.Port || 3001; 5 | 6 | const mongoose = require("mongoose"); 7 | 8 | //database connection 9 | mongoose.connect("url", { 10 | useNewUrlParser: true, 11 | useCreateIndex: true, 12 | useUnifiedTopology: true, 13 | }); 14 | 15 | const connection = mongoose.connection; 16 | 17 | connection.once("open", () => 18 | console.log("mongoDB connection eastablished succesfully") 19 | ); 20 | 21 | //middleware 22 | app.use(express.json()); 23 | app.use(cors()); 24 | 25 | //routes 26 | const accounts = require("./route/accounts"); 27 | app.use("/accounts", accounts); 28 | const transactions = require("./route/transactions"); 29 | app.use("/transactions", transactions); 30 | 31 | //acknoledge api 32 | app.get("/", (req, res) => 33 | res.json({ message: "Welcome you are in the main page :)" }) 34 | ); 35 | 36 | app.listen(PORT, "0.0.0.0", () => 37 | console.log(`your app is running on port ${PORT} enjoy developing`) 38 | ); 39 | -------------------------------------------------------------------------------- /backend/model/accounts.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const AutoIncrement = require("mongoose-sequence")(mongoose); 4 | const Accounts = Schema({ 5 | id: { 6 | type: Number, 7 | default: -1, 8 | }, 9 | name: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | balance: { 15 | type: Number, 16 | required: true, 17 | }, 18 | password: { 19 | type: String, 20 | 21 | required: true, 22 | }, 23 | accountType: { 24 | type: String, 25 | required: true, 26 | }, 27 | }); 28 | 29 | Accounts.plugin(AutoIncrement, { id: "id_2", inc_field: "id" }); 30 | module.exports = mongoose.model("Accounts", Accounts); 31 | -------------------------------------------------------------------------------- /backend/model/transactions.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const AutoIncrement = require("mongoose-sequence")(mongoose); 4 | const Transaction = Schema({ 5 | id: { 6 | type: Number, 7 | default: 0, 8 | }, 9 | description: { 10 | type: String, 11 | required: true, 12 | }, 13 | debit: { 14 | type: Number, 15 | }, 16 | credit: { 17 | type: Number, 18 | }, 19 | accountId: { 20 | type: Number, 21 | }, 22 | date: { type: String }, 23 | }); 24 | 25 | Transaction.plugin(AutoIncrement, { id: "id_1", inc_field: "id" }); 26 | module.exports = mongoose.model("Transaction", Transaction); 27 | -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@sindresorhus/is": { 8 | "version": "0.14.0", 9 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", 10 | "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" 11 | }, 12 | "@szmarczak/http-timer": { 13 | "version": "1.1.2", 14 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", 15 | "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", 16 | "requires": { 17 | "defer-to-connect": "^1.0.1" 18 | } 19 | }, 20 | "@types/color-name": { 21 | "version": "1.1.1", 22 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 23 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" 24 | }, 25 | "abbrev": { 26 | "version": "1.1.1", 27 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 28 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 29 | }, 30 | "accepts": { 31 | "version": "1.3.7", 32 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 33 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 34 | "requires": { 35 | "mime-types": "~2.1.24", 36 | "negotiator": "0.6.2" 37 | } 38 | }, 39 | "ansi-align": { 40 | "version": "3.0.0", 41 | "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", 42 | "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", 43 | "requires": { 44 | "string-width": "^3.0.0" 45 | }, 46 | "dependencies": { 47 | "string-width": { 48 | "version": "3.1.0", 49 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 50 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 51 | "requires": { 52 | "emoji-regex": "^7.0.1", 53 | "is-fullwidth-code-point": "^2.0.0", 54 | "strip-ansi": "^5.1.0" 55 | } 56 | } 57 | } 58 | }, 59 | "ansi-regex": { 60 | "version": "4.1.0", 61 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 62 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" 63 | }, 64 | "ansi-styles": { 65 | "version": "4.2.1", 66 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 67 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 68 | "requires": { 69 | "@types/color-name": "^1.1.1", 70 | "color-convert": "^2.0.1" 71 | } 72 | }, 73 | "anymatch": { 74 | "version": "3.1.1", 75 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", 76 | "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", 77 | "requires": { 78 | "normalize-path": "^3.0.0", 79 | "picomatch": "^2.0.4" 80 | } 81 | }, 82 | "array-flatten": { 83 | "version": "1.1.1", 84 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 85 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 86 | }, 87 | "async": { 88 | "version": "2.6.3", 89 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", 90 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", 91 | "requires": { 92 | "lodash": "^4.17.14" 93 | } 94 | }, 95 | "balanced-match": { 96 | "version": "1.0.0", 97 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 98 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 99 | }, 100 | "binary-extensions": { 101 | "version": "2.0.0", 102 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", 103 | "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" 104 | }, 105 | "bl": { 106 | "version": "2.2.0", 107 | "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", 108 | "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", 109 | "requires": { 110 | "readable-stream": "^2.3.5", 111 | "safe-buffer": "^5.1.1" 112 | } 113 | }, 114 | "bluebird": { 115 | "version": "3.5.1", 116 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 117 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 118 | }, 119 | "body-parser": { 120 | "version": "1.19.0", 121 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 122 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 123 | "requires": { 124 | "bytes": "3.1.0", 125 | "content-type": "~1.0.4", 126 | "debug": "2.6.9", 127 | "depd": "~1.1.2", 128 | "http-errors": "1.7.2", 129 | "iconv-lite": "0.4.24", 130 | "on-finished": "~2.3.0", 131 | "qs": "6.7.0", 132 | "raw-body": "2.4.0", 133 | "type-is": "~1.6.17" 134 | }, 135 | "dependencies": { 136 | "debug": { 137 | "version": "2.6.9", 138 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 139 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 140 | "requires": { 141 | "ms": "2.0.0" 142 | } 143 | }, 144 | "ms": { 145 | "version": "2.0.0", 146 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 147 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 148 | } 149 | } 150 | }, 151 | "boxen": { 152 | "version": "4.2.0", 153 | "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", 154 | "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", 155 | "requires": { 156 | "ansi-align": "^3.0.0", 157 | "camelcase": "^5.3.1", 158 | "chalk": "^3.0.0", 159 | "cli-boxes": "^2.2.0", 160 | "string-width": "^4.1.0", 161 | "term-size": "^2.1.0", 162 | "type-fest": "^0.8.1", 163 | "widest-line": "^3.1.0" 164 | } 165 | }, 166 | "brace-expansion": { 167 | "version": "1.1.11", 168 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 169 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 170 | "requires": { 171 | "balanced-match": "^1.0.0", 172 | "concat-map": "0.0.1" 173 | } 174 | }, 175 | "braces": { 176 | "version": "3.0.2", 177 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 178 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 179 | "requires": { 180 | "fill-range": "^7.0.1" 181 | } 182 | }, 183 | "bson": { 184 | "version": "1.1.4", 185 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.4.tgz", 186 | "integrity": "sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==" 187 | }, 188 | "bytes": { 189 | "version": "3.1.0", 190 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 191 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 192 | }, 193 | "cacheable-request": { 194 | "version": "6.1.0", 195 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", 196 | "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", 197 | "requires": { 198 | "clone-response": "^1.0.2", 199 | "get-stream": "^5.1.0", 200 | "http-cache-semantics": "^4.0.0", 201 | "keyv": "^3.0.0", 202 | "lowercase-keys": "^2.0.0", 203 | "normalize-url": "^4.1.0", 204 | "responselike": "^1.0.2" 205 | }, 206 | "dependencies": { 207 | "get-stream": { 208 | "version": "5.1.0", 209 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", 210 | "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", 211 | "requires": { 212 | "pump": "^3.0.0" 213 | } 214 | }, 215 | "lowercase-keys": { 216 | "version": "2.0.0", 217 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", 218 | "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" 219 | } 220 | } 221 | }, 222 | "camelcase": { 223 | "version": "5.3.1", 224 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 225 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" 226 | }, 227 | "chalk": { 228 | "version": "3.0.0", 229 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", 230 | "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", 231 | "requires": { 232 | "ansi-styles": "^4.1.0", 233 | "supports-color": "^7.1.0" 234 | }, 235 | "dependencies": { 236 | "has-flag": { 237 | "version": "4.0.0", 238 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 239 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 240 | }, 241 | "supports-color": { 242 | "version": "7.1.0", 243 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 244 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 245 | "requires": { 246 | "has-flag": "^4.0.0" 247 | } 248 | } 249 | } 250 | }, 251 | "chokidar": { 252 | "version": "3.4.0", 253 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", 254 | "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", 255 | "requires": { 256 | "anymatch": "~3.1.1", 257 | "braces": "~3.0.2", 258 | "fsevents": "~2.1.2", 259 | "glob-parent": "~5.1.0", 260 | "is-binary-path": "~2.1.0", 261 | "is-glob": "~4.0.1", 262 | "normalize-path": "~3.0.0", 263 | "readdirp": "~3.4.0" 264 | } 265 | }, 266 | "ci-info": { 267 | "version": "2.0.0", 268 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", 269 | "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" 270 | }, 271 | "cli-boxes": { 272 | "version": "2.2.0", 273 | "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", 274 | "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==" 275 | }, 276 | "clone-response": { 277 | "version": "1.0.2", 278 | "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", 279 | "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", 280 | "requires": { 281 | "mimic-response": "^1.0.0" 282 | } 283 | }, 284 | "color-convert": { 285 | "version": "2.0.1", 286 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 287 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 288 | "requires": { 289 | "color-name": "~1.1.4" 290 | } 291 | }, 292 | "color-name": { 293 | "version": "1.1.4", 294 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 295 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 296 | }, 297 | "concat-map": { 298 | "version": "0.0.1", 299 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 300 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 301 | }, 302 | "configstore": { 303 | "version": "5.0.1", 304 | "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", 305 | "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", 306 | "requires": { 307 | "dot-prop": "^5.2.0", 308 | "graceful-fs": "^4.1.2", 309 | "make-dir": "^3.0.0", 310 | "unique-string": "^2.0.0", 311 | "write-file-atomic": "^3.0.0", 312 | "xdg-basedir": "^4.0.0" 313 | } 314 | }, 315 | "content-disposition": { 316 | "version": "0.5.3", 317 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 318 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 319 | "requires": { 320 | "safe-buffer": "5.1.2" 321 | } 322 | }, 323 | "content-type": { 324 | "version": "1.0.4", 325 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 326 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 327 | }, 328 | "cookie": { 329 | "version": "0.4.0", 330 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 331 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 332 | }, 333 | "cookie-signature": { 334 | "version": "1.0.6", 335 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 336 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 337 | }, 338 | "core-util-is": { 339 | "version": "1.0.2", 340 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 341 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 342 | }, 343 | "crypto-random-string": { 344 | "version": "2.0.0", 345 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", 346 | "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" 347 | }, 348 | "debug": { 349 | "version": "3.1.0", 350 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 351 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 352 | "requires": { 353 | "ms": "2.0.0" 354 | }, 355 | "dependencies": { 356 | "ms": { 357 | "version": "2.0.0", 358 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 359 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 360 | } 361 | } 362 | }, 363 | "decompress-response": { 364 | "version": "3.3.0", 365 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", 366 | "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", 367 | "requires": { 368 | "mimic-response": "^1.0.0" 369 | } 370 | }, 371 | "deep-extend": { 372 | "version": "0.6.0", 373 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 374 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 375 | }, 376 | "defer-to-connect": { 377 | "version": "1.1.3", 378 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", 379 | "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" 380 | }, 381 | "denque": { 382 | "version": "1.4.1", 383 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", 384 | "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" 385 | }, 386 | "depd": { 387 | "version": "1.1.2", 388 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 389 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 390 | }, 391 | "destroy": { 392 | "version": "1.0.4", 393 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 394 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 395 | }, 396 | "dot-prop": { 397 | "version": "5.2.0", 398 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", 399 | "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", 400 | "requires": { 401 | "is-obj": "^2.0.0" 402 | } 403 | }, 404 | "duplexer3": { 405 | "version": "0.1.4", 406 | "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", 407 | "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" 408 | }, 409 | "ee-first": { 410 | "version": "1.1.1", 411 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 412 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 413 | }, 414 | "emoji-regex": { 415 | "version": "7.0.3", 416 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 417 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" 418 | }, 419 | "encodeurl": { 420 | "version": "1.0.2", 421 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 422 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 423 | }, 424 | "end-of-stream": { 425 | "version": "1.4.4", 426 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 427 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 428 | "requires": { 429 | "once": "^1.4.0" 430 | } 431 | }, 432 | "escape-goat": { 433 | "version": "2.1.1", 434 | "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", 435 | "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" 436 | }, 437 | "escape-html": { 438 | "version": "1.0.3", 439 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 440 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 441 | }, 442 | "etag": { 443 | "version": "1.8.1", 444 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 445 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 446 | }, 447 | "express": { 448 | "version": "4.17.1", 449 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 450 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 451 | "requires": { 452 | "accepts": "~1.3.7", 453 | "array-flatten": "1.1.1", 454 | "body-parser": "1.19.0", 455 | "content-disposition": "0.5.3", 456 | "content-type": "~1.0.4", 457 | "cookie": "0.4.0", 458 | "cookie-signature": "1.0.6", 459 | "debug": "2.6.9", 460 | "depd": "~1.1.2", 461 | "encodeurl": "~1.0.2", 462 | "escape-html": "~1.0.3", 463 | "etag": "~1.8.1", 464 | "finalhandler": "~1.1.2", 465 | "fresh": "0.5.2", 466 | "merge-descriptors": "1.0.1", 467 | "methods": "~1.1.2", 468 | "on-finished": "~2.3.0", 469 | "parseurl": "~1.3.3", 470 | "path-to-regexp": "0.1.7", 471 | "proxy-addr": "~2.0.5", 472 | "qs": "6.7.0", 473 | "range-parser": "~1.2.1", 474 | "safe-buffer": "5.1.2", 475 | "send": "0.17.1", 476 | "serve-static": "1.14.1", 477 | "setprototypeof": "1.1.1", 478 | "statuses": "~1.5.0", 479 | "type-is": "~1.6.18", 480 | "utils-merge": "1.0.1", 481 | "vary": "~1.1.2" 482 | }, 483 | "dependencies": { 484 | "debug": { 485 | "version": "2.6.9", 486 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 487 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 488 | "requires": { 489 | "ms": "2.0.0" 490 | } 491 | }, 492 | "ms": { 493 | "version": "2.0.0", 494 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 495 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 496 | } 497 | } 498 | }, 499 | "fill-range": { 500 | "version": "7.0.1", 501 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 502 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 503 | "requires": { 504 | "to-regex-range": "^5.0.1" 505 | } 506 | }, 507 | "finalhandler": { 508 | "version": "1.1.2", 509 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 510 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 511 | "requires": { 512 | "debug": "2.6.9", 513 | "encodeurl": "~1.0.2", 514 | "escape-html": "~1.0.3", 515 | "on-finished": "~2.3.0", 516 | "parseurl": "~1.3.3", 517 | "statuses": "~1.5.0", 518 | "unpipe": "~1.0.0" 519 | }, 520 | "dependencies": { 521 | "debug": { 522 | "version": "2.6.9", 523 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 524 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 525 | "requires": { 526 | "ms": "2.0.0" 527 | } 528 | }, 529 | "ms": { 530 | "version": "2.0.0", 531 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 532 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 533 | } 534 | } 535 | }, 536 | "forwarded": { 537 | "version": "0.1.2", 538 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 539 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 540 | }, 541 | "fresh": { 542 | "version": "0.5.2", 543 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 544 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 545 | }, 546 | "fsevents": { 547 | "version": "2.1.3", 548 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", 549 | "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", 550 | "optional": true 551 | }, 552 | "get-stream": { 553 | "version": "4.1.0", 554 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", 555 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", 556 | "requires": { 557 | "pump": "^3.0.0" 558 | } 559 | }, 560 | "glob-parent": { 561 | "version": "5.1.1", 562 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", 563 | "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", 564 | "requires": { 565 | "is-glob": "^4.0.1" 566 | } 567 | }, 568 | "global-dirs": { 569 | "version": "2.0.1", 570 | "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", 571 | "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", 572 | "requires": { 573 | "ini": "^1.3.5" 574 | } 575 | }, 576 | "got": { 577 | "version": "9.6.0", 578 | "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", 579 | "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", 580 | "requires": { 581 | "@sindresorhus/is": "^0.14.0", 582 | "@szmarczak/http-timer": "^1.1.2", 583 | "cacheable-request": "^6.0.0", 584 | "decompress-response": "^3.3.0", 585 | "duplexer3": "^0.1.4", 586 | "get-stream": "^4.1.0", 587 | "lowercase-keys": "^1.0.1", 588 | "mimic-response": "^1.0.1", 589 | "p-cancelable": "^1.0.0", 590 | "to-readable-stream": "^1.0.0", 591 | "url-parse-lax": "^3.0.0" 592 | } 593 | }, 594 | "graceful-fs": { 595 | "version": "4.2.4", 596 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", 597 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" 598 | }, 599 | "has-flag": { 600 | "version": "3.0.0", 601 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 602 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 603 | }, 604 | "has-yarn": { 605 | "version": "2.1.0", 606 | "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", 607 | "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" 608 | }, 609 | "http-cache-semantics": { 610 | "version": "4.1.0", 611 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", 612 | "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" 613 | }, 614 | "http-errors": { 615 | "version": "1.7.2", 616 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 617 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 618 | "requires": { 619 | "depd": "~1.1.2", 620 | "inherits": "2.0.3", 621 | "setprototypeof": "1.1.1", 622 | "statuses": ">= 1.5.0 < 2", 623 | "toidentifier": "1.0.0" 624 | }, 625 | "dependencies": { 626 | "inherits": { 627 | "version": "2.0.3", 628 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 629 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 630 | } 631 | } 632 | }, 633 | "iconv-lite": { 634 | "version": "0.4.24", 635 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 636 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 637 | "requires": { 638 | "safer-buffer": ">= 2.1.2 < 3" 639 | } 640 | }, 641 | "ignore-by-default": { 642 | "version": "1.0.1", 643 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 644 | "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" 645 | }, 646 | "import-lazy": { 647 | "version": "2.1.0", 648 | "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", 649 | "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" 650 | }, 651 | "imurmurhash": { 652 | "version": "0.1.4", 653 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 654 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" 655 | }, 656 | "inherits": { 657 | "version": "2.0.4", 658 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 659 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 660 | }, 661 | "ini": { 662 | "version": "1.3.5", 663 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 664 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 665 | }, 666 | "ipaddr.js": { 667 | "version": "1.9.1", 668 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 669 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 670 | }, 671 | "is-binary-path": { 672 | "version": "2.1.0", 673 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 674 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 675 | "requires": { 676 | "binary-extensions": "^2.0.0" 677 | } 678 | }, 679 | "is-ci": { 680 | "version": "2.0.0", 681 | "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", 682 | "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", 683 | "requires": { 684 | "ci-info": "^2.0.0" 685 | } 686 | }, 687 | "is-extglob": { 688 | "version": "2.1.1", 689 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 690 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" 691 | }, 692 | "is-fullwidth-code-point": { 693 | "version": "2.0.0", 694 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 695 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 696 | }, 697 | "is-glob": { 698 | "version": "4.0.1", 699 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 700 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 701 | "requires": { 702 | "is-extglob": "^2.1.1" 703 | } 704 | }, 705 | "is-installed-globally": { 706 | "version": "0.3.2", 707 | "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", 708 | "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", 709 | "requires": { 710 | "global-dirs": "^2.0.1", 711 | "is-path-inside": "^3.0.1" 712 | } 713 | }, 714 | "is-npm": { 715 | "version": "4.0.0", 716 | "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", 717 | "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" 718 | }, 719 | "is-number": { 720 | "version": "7.0.0", 721 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 722 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 723 | }, 724 | "is-obj": { 725 | "version": "2.0.0", 726 | "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", 727 | "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" 728 | }, 729 | "is-path-inside": { 730 | "version": "3.0.2", 731 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", 732 | "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" 733 | }, 734 | "is-typedarray": { 735 | "version": "1.0.0", 736 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 737 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 738 | }, 739 | "is-yarn-global": { 740 | "version": "0.3.0", 741 | "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", 742 | "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" 743 | }, 744 | "isarray": { 745 | "version": "1.0.0", 746 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 747 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 748 | }, 749 | "json-buffer": { 750 | "version": "3.0.0", 751 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", 752 | "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" 753 | }, 754 | "kareem": { 755 | "version": "2.3.1", 756 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", 757 | "integrity": "sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==" 758 | }, 759 | "keyv": { 760 | "version": "3.1.0", 761 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", 762 | "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", 763 | "requires": { 764 | "json-buffer": "3.0.0" 765 | } 766 | }, 767 | "latest-version": { 768 | "version": "5.1.0", 769 | "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", 770 | "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", 771 | "requires": { 772 | "package-json": "^6.3.0" 773 | } 774 | }, 775 | "lodash": { 776 | "version": "4.17.15", 777 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 778 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 779 | }, 780 | "lowercase-keys": { 781 | "version": "1.0.1", 782 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", 783 | "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" 784 | }, 785 | "make-dir": { 786 | "version": "3.1.0", 787 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 788 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 789 | "requires": { 790 | "semver": "^6.0.0" 791 | }, 792 | "dependencies": { 793 | "semver": { 794 | "version": "6.3.0", 795 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 796 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 797 | } 798 | } 799 | }, 800 | "media-typer": { 801 | "version": "0.3.0", 802 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 803 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 804 | }, 805 | "memory-pager": { 806 | "version": "1.5.0", 807 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 808 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 809 | "optional": true 810 | }, 811 | "merge-descriptors": { 812 | "version": "1.0.1", 813 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 814 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 815 | }, 816 | "methods": { 817 | "version": "1.1.2", 818 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 819 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 820 | }, 821 | "mime": { 822 | "version": "1.6.0", 823 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 824 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 825 | }, 826 | "mime-db": { 827 | "version": "1.44.0", 828 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 829 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 830 | }, 831 | "mime-types": { 832 | "version": "2.1.27", 833 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 834 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 835 | "requires": { 836 | "mime-db": "1.44.0" 837 | } 838 | }, 839 | "mimic-response": { 840 | "version": "1.0.1", 841 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", 842 | "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" 843 | }, 844 | "minimatch": { 845 | "version": "3.0.4", 846 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 847 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 848 | "requires": { 849 | "brace-expansion": "^1.1.7" 850 | } 851 | }, 852 | "minimist": { 853 | "version": "1.2.5", 854 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 855 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 856 | }, 857 | "mongodb": { 858 | "version": "3.5.7", 859 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.7.tgz", 860 | "integrity": "sha512-lMtleRT+vIgY/JhhTn1nyGwnSMmJkJELp+4ZbrjctrnBxuLbj6rmLuJFz8W2xUzUqWmqoyVxJLYuC58ZKpcTYQ==", 861 | "requires": { 862 | "bl": "^2.2.0", 863 | "bson": "^1.1.4", 864 | "denque": "^1.4.1", 865 | "require_optional": "^1.0.1", 866 | "safe-buffer": "^5.1.2", 867 | "saslprep": "^1.0.0" 868 | } 869 | }, 870 | "mongoose": { 871 | "version": "5.9.14", 872 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.14.tgz", 873 | "integrity": "sha512-LScxCruQv0YpU/9DasKdThd+3r3PFQbCgesmfa6g0pTDOIiD1A9N9OQsGYrDf+dyUksfLCxJYYF9qpBHLvS1tg==", 874 | "requires": { 875 | "bson": "^1.1.4", 876 | "kareem": "2.3.1", 877 | "mongodb": "3.5.7", 878 | "mongoose-legacy-pluralize": "1.0.2", 879 | "mpath": "0.7.0", 880 | "mquery": "3.2.2", 881 | "ms": "2.1.2", 882 | "regexp-clone": "1.0.0", 883 | "safe-buffer": "5.1.2", 884 | "sift": "7.0.1", 885 | "sliced": "1.0.1" 886 | } 887 | }, 888 | "mongoose-legacy-pluralize": { 889 | "version": "1.0.2", 890 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 891 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 892 | }, 893 | "mongoose-sequence": { 894 | "version": "5.2.2", 895 | "resolved": "https://registry.npmjs.org/mongoose-sequence/-/mongoose-sequence-5.2.2.tgz", 896 | "integrity": "sha512-gtN33C4fXVgOH8SSQvwSf8+DcFtxw1n/Wk1RHEs+W3A/cqYgLjvjMalq/0q/TDboeapNi6RBymBnyw3fDoaDlg==", 897 | "requires": { 898 | "async": "^2.5.0", 899 | "lodash": "^4.17.11" 900 | } 901 | }, 902 | "mpath": { 903 | "version": "0.7.0", 904 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.7.0.tgz", 905 | "integrity": "sha512-Aiq04hILxhz1L+f7sjGyn7IxYzWm1zLNNXcfhDtx04kZ2Gk7uvFdgZ8ts1cWa/6d0TQmag2yR8zSGZUmp0tFNg==" 906 | }, 907 | "mquery": { 908 | "version": "3.2.2", 909 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.2.tgz", 910 | "integrity": "sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q==", 911 | "requires": { 912 | "bluebird": "3.5.1", 913 | "debug": "3.1.0", 914 | "regexp-clone": "^1.0.0", 915 | "safe-buffer": "5.1.2", 916 | "sliced": "1.0.1" 917 | } 918 | }, 919 | "ms": { 920 | "version": "2.1.2", 921 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 922 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 923 | }, 924 | "negotiator": { 925 | "version": "0.6.2", 926 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 927 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 928 | }, 929 | "nodemon": { 930 | "version": "2.0.3", 931 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.3.tgz", 932 | "integrity": "sha512-lLQLPS90Lqwc99IHe0U94rDgvjo+G9I4uEIxRG3evSLROcqQ9hwc0AxlSHKS4T1JW/IMj/7N5mthiN58NL/5kw==", 933 | "requires": { 934 | "chokidar": "^3.2.2", 935 | "debug": "^3.2.6", 936 | "ignore-by-default": "^1.0.1", 937 | "minimatch": "^3.0.4", 938 | "pstree.remy": "^1.1.7", 939 | "semver": "^5.7.1", 940 | "supports-color": "^5.5.0", 941 | "touch": "^3.1.0", 942 | "undefsafe": "^2.0.2", 943 | "update-notifier": "^4.0.0" 944 | }, 945 | "dependencies": { 946 | "debug": { 947 | "version": "3.2.6", 948 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 949 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 950 | "requires": { 951 | "ms": "^2.1.1" 952 | } 953 | } 954 | } 955 | }, 956 | "nopt": { 957 | "version": "1.0.10", 958 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 959 | "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", 960 | "requires": { 961 | "abbrev": "1" 962 | } 963 | }, 964 | "normalize-path": { 965 | "version": "3.0.0", 966 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 967 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" 968 | }, 969 | "normalize-url": { 970 | "version": "4.5.0", 971 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", 972 | "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" 973 | }, 974 | "on-finished": { 975 | "version": "2.3.0", 976 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 977 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 978 | "requires": { 979 | "ee-first": "1.1.1" 980 | } 981 | }, 982 | "once": { 983 | "version": "1.4.0", 984 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 985 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 986 | "requires": { 987 | "wrappy": "1" 988 | } 989 | }, 990 | "p-cancelable": { 991 | "version": "1.1.0", 992 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", 993 | "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" 994 | }, 995 | "package-json": { 996 | "version": "6.5.0", 997 | "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", 998 | "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", 999 | "requires": { 1000 | "got": "^9.6.0", 1001 | "registry-auth-token": "^4.0.0", 1002 | "registry-url": "^5.0.0", 1003 | "semver": "^6.2.0" 1004 | }, 1005 | "dependencies": { 1006 | "semver": { 1007 | "version": "6.3.0", 1008 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1009 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 1010 | } 1011 | } 1012 | }, 1013 | "parseurl": { 1014 | "version": "1.3.3", 1015 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1016 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1017 | }, 1018 | "path-to-regexp": { 1019 | "version": "0.1.7", 1020 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1021 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1022 | }, 1023 | "picomatch": { 1024 | "version": "2.2.2", 1025 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", 1026 | "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" 1027 | }, 1028 | "prepend-http": { 1029 | "version": "2.0.0", 1030 | "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", 1031 | "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" 1032 | }, 1033 | "process-nextick-args": { 1034 | "version": "2.0.1", 1035 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1036 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 1037 | }, 1038 | "proxy-addr": { 1039 | "version": "2.0.6", 1040 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 1041 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 1042 | "requires": { 1043 | "forwarded": "~0.1.2", 1044 | "ipaddr.js": "1.9.1" 1045 | } 1046 | }, 1047 | "pstree.remy": { 1048 | "version": "1.1.7", 1049 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", 1050 | "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==" 1051 | }, 1052 | "pump": { 1053 | "version": "3.0.0", 1054 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 1055 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 1056 | "requires": { 1057 | "end-of-stream": "^1.1.0", 1058 | "once": "^1.3.1" 1059 | } 1060 | }, 1061 | "pupa": { 1062 | "version": "2.0.1", 1063 | "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", 1064 | "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", 1065 | "requires": { 1066 | "escape-goat": "^2.0.0" 1067 | } 1068 | }, 1069 | "qs": { 1070 | "version": "6.7.0", 1071 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 1072 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 1073 | }, 1074 | "range-parser": { 1075 | "version": "1.2.1", 1076 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1077 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1078 | }, 1079 | "raw-body": { 1080 | "version": "2.4.0", 1081 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 1082 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 1083 | "requires": { 1084 | "bytes": "3.1.0", 1085 | "http-errors": "1.7.2", 1086 | "iconv-lite": "0.4.24", 1087 | "unpipe": "1.0.0" 1088 | } 1089 | }, 1090 | "rc": { 1091 | "version": "1.2.8", 1092 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 1093 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 1094 | "requires": { 1095 | "deep-extend": "^0.6.0", 1096 | "ini": "~1.3.0", 1097 | "minimist": "^1.2.0", 1098 | "strip-json-comments": "~2.0.1" 1099 | } 1100 | }, 1101 | "readable-stream": { 1102 | "version": "2.3.7", 1103 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 1104 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 1105 | "requires": { 1106 | "core-util-is": "~1.0.0", 1107 | "inherits": "~2.0.3", 1108 | "isarray": "~1.0.0", 1109 | "process-nextick-args": "~2.0.0", 1110 | "safe-buffer": "~5.1.1", 1111 | "string_decoder": "~1.1.1", 1112 | "util-deprecate": "~1.0.1" 1113 | } 1114 | }, 1115 | "readdirp": { 1116 | "version": "3.4.0", 1117 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", 1118 | "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", 1119 | "requires": { 1120 | "picomatch": "^2.2.1" 1121 | } 1122 | }, 1123 | "regexp-clone": { 1124 | "version": "1.0.0", 1125 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", 1126 | "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" 1127 | }, 1128 | "registry-auth-token": { 1129 | "version": "4.1.1", 1130 | "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.1.1.tgz", 1131 | "integrity": "sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==", 1132 | "requires": { 1133 | "rc": "^1.2.8" 1134 | } 1135 | }, 1136 | "registry-url": { 1137 | "version": "5.1.0", 1138 | "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", 1139 | "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", 1140 | "requires": { 1141 | "rc": "^1.2.8" 1142 | } 1143 | }, 1144 | "require_optional": { 1145 | "version": "1.0.1", 1146 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 1147 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 1148 | "requires": { 1149 | "resolve-from": "^2.0.0", 1150 | "semver": "^5.1.0" 1151 | } 1152 | }, 1153 | "resolve-from": { 1154 | "version": "2.0.0", 1155 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 1156 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 1157 | }, 1158 | "responselike": { 1159 | "version": "1.0.2", 1160 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", 1161 | "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", 1162 | "requires": { 1163 | "lowercase-keys": "^1.0.0" 1164 | } 1165 | }, 1166 | "safe-buffer": { 1167 | "version": "5.1.2", 1168 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1169 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1170 | }, 1171 | "safer-buffer": { 1172 | "version": "2.1.2", 1173 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1174 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1175 | }, 1176 | "saslprep": { 1177 | "version": "1.0.3", 1178 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", 1179 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", 1180 | "optional": true, 1181 | "requires": { 1182 | "sparse-bitfield": "^3.0.3" 1183 | } 1184 | }, 1185 | "semver": { 1186 | "version": "5.7.1", 1187 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1188 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1189 | }, 1190 | "semver-diff": { 1191 | "version": "3.1.1", 1192 | "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", 1193 | "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", 1194 | "requires": { 1195 | "semver": "^6.3.0" 1196 | }, 1197 | "dependencies": { 1198 | "semver": { 1199 | "version": "6.3.0", 1200 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1201 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 1202 | } 1203 | } 1204 | }, 1205 | "send": { 1206 | "version": "0.17.1", 1207 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 1208 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 1209 | "requires": { 1210 | "debug": "2.6.9", 1211 | "depd": "~1.1.2", 1212 | "destroy": "~1.0.4", 1213 | "encodeurl": "~1.0.2", 1214 | "escape-html": "~1.0.3", 1215 | "etag": "~1.8.1", 1216 | "fresh": "0.5.2", 1217 | "http-errors": "~1.7.2", 1218 | "mime": "1.6.0", 1219 | "ms": "2.1.1", 1220 | "on-finished": "~2.3.0", 1221 | "range-parser": "~1.2.1", 1222 | "statuses": "~1.5.0" 1223 | }, 1224 | "dependencies": { 1225 | "debug": { 1226 | "version": "2.6.9", 1227 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1228 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1229 | "requires": { 1230 | "ms": "2.0.0" 1231 | }, 1232 | "dependencies": { 1233 | "ms": { 1234 | "version": "2.0.0", 1235 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1236 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1237 | } 1238 | } 1239 | }, 1240 | "ms": { 1241 | "version": "2.1.1", 1242 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1243 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1244 | } 1245 | } 1246 | }, 1247 | "serve-static": { 1248 | "version": "1.14.1", 1249 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 1250 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 1251 | "requires": { 1252 | "encodeurl": "~1.0.2", 1253 | "escape-html": "~1.0.3", 1254 | "parseurl": "~1.3.3", 1255 | "send": "0.17.1" 1256 | } 1257 | }, 1258 | "setprototypeof": { 1259 | "version": "1.1.1", 1260 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1261 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1262 | }, 1263 | "sift": { 1264 | "version": "7.0.1", 1265 | "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", 1266 | "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" 1267 | }, 1268 | "signal-exit": { 1269 | "version": "3.0.3", 1270 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 1271 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 1272 | }, 1273 | "sliced": { 1274 | "version": "1.0.1", 1275 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 1276 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 1277 | }, 1278 | "sparse-bitfield": { 1279 | "version": "3.0.3", 1280 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 1281 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 1282 | "optional": true, 1283 | "requires": { 1284 | "memory-pager": "^1.0.2" 1285 | } 1286 | }, 1287 | "statuses": { 1288 | "version": "1.5.0", 1289 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1290 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1291 | }, 1292 | "string-width": { 1293 | "version": "4.2.0", 1294 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 1295 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 1296 | "requires": { 1297 | "emoji-regex": "^8.0.0", 1298 | "is-fullwidth-code-point": "^3.0.0", 1299 | "strip-ansi": "^6.0.0" 1300 | }, 1301 | "dependencies": { 1302 | "ansi-regex": { 1303 | "version": "5.0.0", 1304 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 1305 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 1306 | }, 1307 | "emoji-regex": { 1308 | "version": "8.0.0", 1309 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1310 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 1311 | }, 1312 | "is-fullwidth-code-point": { 1313 | "version": "3.0.0", 1314 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1315 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 1316 | }, 1317 | "strip-ansi": { 1318 | "version": "6.0.0", 1319 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 1320 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 1321 | "requires": { 1322 | "ansi-regex": "^5.0.0" 1323 | } 1324 | } 1325 | } 1326 | }, 1327 | "string_decoder": { 1328 | "version": "1.1.1", 1329 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1330 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1331 | "requires": { 1332 | "safe-buffer": "~5.1.0" 1333 | } 1334 | }, 1335 | "strip-ansi": { 1336 | "version": "5.2.0", 1337 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 1338 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 1339 | "requires": { 1340 | "ansi-regex": "^4.1.0" 1341 | } 1342 | }, 1343 | "strip-json-comments": { 1344 | "version": "2.0.1", 1345 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1346 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 1347 | }, 1348 | "supports-color": { 1349 | "version": "5.5.0", 1350 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1351 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1352 | "requires": { 1353 | "has-flag": "^3.0.0" 1354 | } 1355 | }, 1356 | "term-size": { 1357 | "version": "2.2.0", 1358 | "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", 1359 | "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" 1360 | }, 1361 | "to-readable-stream": { 1362 | "version": "1.0.0", 1363 | "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", 1364 | "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" 1365 | }, 1366 | "to-regex-range": { 1367 | "version": "5.0.1", 1368 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1369 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1370 | "requires": { 1371 | "is-number": "^7.0.0" 1372 | } 1373 | }, 1374 | "toidentifier": { 1375 | "version": "1.0.0", 1376 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1377 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1378 | }, 1379 | "touch": { 1380 | "version": "3.1.0", 1381 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 1382 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 1383 | "requires": { 1384 | "nopt": "~1.0.10" 1385 | } 1386 | }, 1387 | "type-fest": { 1388 | "version": "0.8.1", 1389 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", 1390 | "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" 1391 | }, 1392 | "type-is": { 1393 | "version": "1.6.18", 1394 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1395 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1396 | "requires": { 1397 | "media-typer": "0.3.0", 1398 | "mime-types": "~2.1.24" 1399 | } 1400 | }, 1401 | "typedarray-to-buffer": { 1402 | "version": "3.1.5", 1403 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 1404 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 1405 | "requires": { 1406 | "is-typedarray": "^1.0.0" 1407 | } 1408 | }, 1409 | "undefsafe": { 1410 | "version": "2.0.3", 1411 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", 1412 | "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", 1413 | "requires": { 1414 | "debug": "^2.2.0" 1415 | }, 1416 | "dependencies": { 1417 | "debug": { 1418 | "version": "2.6.9", 1419 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1420 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1421 | "requires": { 1422 | "ms": "2.0.0" 1423 | } 1424 | }, 1425 | "ms": { 1426 | "version": "2.0.0", 1427 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1428 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1429 | } 1430 | } 1431 | }, 1432 | "unique-string": { 1433 | "version": "2.0.0", 1434 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", 1435 | "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", 1436 | "requires": { 1437 | "crypto-random-string": "^2.0.0" 1438 | } 1439 | }, 1440 | "unpipe": { 1441 | "version": "1.0.0", 1442 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1443 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1444 | }, 1445 | "update-notifier": { 1446 | "version": "4.1.0", 1447 | "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", 1448 | "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", 1449 | "requires": { 1450 | "boxen": "^4.2.0", 1451 | "chalk": "^3.0.0", 1452 | "configstore": "^5.0.1", 1453 | "has-yarn": "^2.1.0", 1454 | "import-lazy": "^2.1.0", 1455 | "is-ci": "^2.0.0", 1456 | "is-installed-globally": "^0.3.1", 1457 | "is-npm": "^4.0.0", 1458 | "is-yarn-global": "^0.3.0", 1459 | "latest-version": "^5.0.0", 1460 | "pupa": "^2.0.1", 1461 | "semver-diff": "^3.1.1", 1462 | "xdg-basedir": "^4.0.0" 1463 | } 1464 | }, 1465 | "url-parse-lax": { 1466 | "version": "3.0.0", 1467 | "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", 1468 | "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", 1469 | "requires": { 1470 | "prepend-http": "^2.0.0" 1471 | } 1472 | }, 1473 | "util-deprecate": { 1474 | "version": "1.0.2", 1475 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1476 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1477 | }, 1478 | "utils-merge": { 1479 | "version": "1.0.1", 1480 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1481 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1482 | }, 1483 | "vary": { 1484 | "version": "1.1.2", 1485 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1486 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1487 | }, 1488 | "widest-line": { 1489 | "version": "3.1.0", 1490 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", 1491 | "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", 1492 | "requires": { 1493 | "string-width": "^4.0.0" 1494 | } 1495 | }, 1496 | "wrappy": { 1497 | "version": "1.0.2", 1498 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1499 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1500 | }, 1501 | "write-file-atomic": { 1502 | "version": "3.0.3", 1503 | "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", 1504 | "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", 1505 | "requires": { 1506 | "imurmurhash": "^0.1.4", 1507 | "is-typedarray": "^1.0.0", 1508 | "signal-exit": "^3.0.2", 1509 | "typedarray-to-buffer": "^3.1.5" 1510 | } 1511 | }, 1512 | "xdg-basedir": { 1513 | "version": "4.0.0", 1514 | "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", 1515 | "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" 1516 | } 1517 | } 1518 | } 1519 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js", 9 | "dev": "nodemon index.js" 10 | }, 11 | "author": "balram0608", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.19.0", 15 | "express": "^4.17.1", 16 | "mongoose": "^5.9.14", 17 | "mongoose-sequence": "^5.2.2", 18 | "nodemon": "^2.0.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/route/accounts.js: -------------------------------------------------------------------------------- 1 | const Accounts = require("../model/accounts.model"); 2 | const express = require("express"); 3 | const router = express.Router(); 4 | const mongoose = require("mongoose"); 5 | const Schema = mongoose.Schema; 6 | let ObjectId = mongoose.Types.ObjectId; 7 | router.route("/").get(async (req, res) => { 8 | await Accounts.find({}, (err, accounts) => { 9 | if (err) { 10 | res.status(400).json({ error: err }); 11 | } else { 12 | res.json(accounts); 13 | } 14 | }); 15 | }); 16 | 17 | router.route("/:id").patch(async (req, res) => { 18 | // await Accounts.findOneAndUpdate({id:req.params.id}, (err, accounts) => { 19 | // if (err) { 20 | // res.status(400).json({ error: err }); 21 | // } else { 22 | // res.json(accounts); 23 | // } 24 | // }); 25 | 26 | await Accounts.update( 27 | { id: req.params.id }, 28 | { balance: req.body.balance }, 29 | (err, post) => { 30 | if (err) return res.status(500).send(err); 31 | const response = { 32 | message: "ok", 33 | }; 34 | return res.status(200).send(response); 35 | } 36 | ); 37 | }); 38 | 39 | //getting a single post of single user 40 | router.route("/").post(async (req, res) => { 41 | // console.log(req.body); 42 | const accounts = Accounts({ 43 | name: req.body.name, 44 | balance: req.body.balance, 45 | password: req.body.password, 46 | accountType: req.body.accountType, 47 | }); 48 | 49 | accounts 50 | .save() 51 | .then( 52 | () => res.json({ message: "ok", data: accounts }), 53 | console.log(accounts) 54 | ) 55 | .catch((err) => res.status(400).json({ Error: err })); 56 | }); 57 | 58 | module.exports = router; 59 | -------------------------------------------------------------------------------- /backend/route/transactions.js: -------------------------------------------------------------------------------- 1 | const Transactions = require("../model/transactions.model"); 2 | const express = require("express"); 3 | const router = express.Router(); 4 | const mongoose = require("mongoose"); 5 | const Schema = mongoose.Schema; 6 | // let ObjectId = mongoose.Types.ObjectId; 7 | router.route("/").get(async (req, res) => { 8 | await Transactions.find({}, (err, transactions) => { 9 | if (err) { 10 | res.status(400).json({ error: err }); 11 | } else { 12 | res.json(transactions); 13 | } 14 | }); 15 | }); 16 | router.route("/:id").get(async (req, res) => { 17 | console.log(req.params.id); 18 | await Transactions.find({ accountId: req.params.id }, (err, transaction) => { 19 | if (err) { 20 | res.status(400).json({ error: err }); 21 | } else { 22 | res.json(transaction); 23 | } 24 | }); 25 | }); 26 | 27 | //getting a single post of single user 28 | router.route("/").post(async (req, res) => { 29 | // console.log(req.body); 30 | const transactions = Transactions({ 31 | description: req.body.description, 32 | debit: req.body.debit, 33 | credit: req.body.credit, 34 | accountId: req.body.accountId, 35 | date: Date(), 36 | }); 37 | 38 | transactions 39 | .save() 40 | .then( 41 | () => res.json({ message: "ok", data: transactions }), 42 | console.log(transactions) 43 | ) 44 | .catch((err) => res.status(400).json({ Error: err })); 45 | }); 46 | 47 | module.exports = router; 48 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:3001 2 | 3 | mongo_url="mongodb+srv://BlogUser:jAVhPqR9YORwjwXW@cluster0-fc8do.gcp.mongodb.net/BankDB?retryWrites=true&w=majority"; -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-bank-ui", 3 | "version": "0.1.0", 4 | "devDependencies": { 5 | "deep-freeze": "0.0.1", 6 | "enzyme": "^2.7.1", 7 | "nock": "^9.0.9", 8 | "react-addons-test-utils": "^15.4.2", 9 | "react-scripts": "0.9.5", 10 | "redux-devtools": "^3.3.2", 11 | "redux-devtools-dock-monitor": "^1.1.1", 12 | "redux-devtools-log-monitor": "^1.2.0", 13 | "redux-logger": "^2.8.2", 14 | "redux-mock-store": "^1.2.2" 15 | }, 16 | "dependencies": { 17 | "material-ui": "^0.17.1", 18 | "moment": "^2.17.1", 19 | "react": "^15.4.2", 20 | "react-dom": "^15.4.2", 21 | "react-redux": "^5.0.3", 22 | "react-router": "^3.0.2", 23 | "react-router-redux": "^4.0.8", 24 | "react-tap-event-plugin": "^2.0.1", 25 | "redux": "^3.6.0", 26 | "redux-thunk": "^2.2.0" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test --env=jsdom", 32 | "eject": "react-scripts eject" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevStack06/MERN-Bank-Web-App/630d96c1eb5a2bdba0ff403f88c480f8c46f8daa/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React Bank 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/actions/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as types from "./constants"; 2 | 3 | export const resetErrorMessage = () => ({ 4 | type: types.RESET_ERROR_MESSAGE, 5 | }); 6 | 7 | export const requestLogin = (credentials) => ({ 8 | type: types.REQUEST_LOGIN, 9 | credentials, 10 | }); 11 | 12 | export const loginSuccessful = () => ({ 13 | type: types.REQUEST_LOGIN_SUCCESS, 14 | }); 15 | 16 | export const loginFailed = (validationResult) => ({ 17 | type: types.REQUEST_LOGIN_FAILURE, 18 | validationResult, 19 | }); 20 | 21 | export const requestLogout = () => ({ 22 | type: types.REQUEST_LOGOUT, 23 | }); 24 | 25 | export const logoutSuccessful = () => ({ 26 | type: types.REQUEST_LOGOUT_SUCCESS, 27 | }); 28 | 29 | export const requestAccounts = () => ({ 30 | type: types.REQUEST_ACCOUNTS, 31 | }); 32 | 33 | export const requestAccountById = (id) => ({ 34 | type: types.REQUEST_ACCOUNTBYID, 35 | id: id, 36 | }); 37 | 38 | export const receiveAccounts = (accounts) => ({ 39 | type: types.RECEIVE_ACCOUNTS, 40 | accounts, 41 | }); 42 | 43 | export const requestTransactions = (accountId) => ({ 44 | type: types.REQUEST_TRANSACTIONS, 45 | }); 46 | 47 | export const receiveTransactions = (transactions) => ({ 48 | type: types.RECEIVE_TRANSACTIONS, 49 | transactions, 50 | }); 51 | 52 | export const showNewAccountForm = () => ({ 53 | type: types.SHOW_NEW_ACCOUNTS_FORM, 54 | }); 55 | 56 | export const hideNewAccountForm = () => ({ 57 | type: types.HIDE_NEW_ACCOUNTS_FORM, 58 | }); 59 | 60 | export const invalidCreateAccountRequest = (validationResult) => ({ 61 | type: types.CREATE_ACCOUNT_VALIDATION_FAILURE, 62 | validationResult, 63 | }); 64 | 65 | export const accountCreated = (account) => ({ 66 | type: types.ACCOUNT_CREATED, 67 | account, 68 | }); 69 | 70 | export const showTransferFunds = () => ({ 71 | type: types.SHOW_TRANSFER_FUNDS, 72 | }); 73 | 74 | export const hideTransferFunds = () => ({ 75 | type: types.HIDE_TRANSFER_FUNDS, 76 | }); 77 | 78 | export const invalidTransferFundsRequest = (validationResult) => ({ 79 | type: types.TRANSFER_FUNDS_VALIDATION_FAILURE, 80 | validationResult, 81 | }); 82 | 83 | export const transferFundsComplete = () => ({ 84 | type: types.TRANSFER_FUNDS_COMPLETE, 85 | }); 86 | 87 | export const refreshAccountBalance = (accountId, newBalance) => ({ 88 | type: types.UPDATE_ACCOUNT_BALANCE, 89 | accountId, 90 | newBalance, 91 | }); 92 | -------------------------------------------------------------------------------- /frontend/src/actions/actionCreators.spec.js: -------------------------------------------------------------------------------- 1 | import * as actions from './index' 2 | 3 | describe('action creators', () => { 4 | it('should create reset error message', () => { 5 | const expectedAction = { 6 | type: 'RESET_ERROR_MESSAGE' 7 | } 8 | 9 | const actualAction = actions.resetErrorMessage() 10 | 11 | expect(actualAction).toEqual(expectedAction) 12 | }) 13 | 14 | it('should create request login action', () => { 15 | const credentials = { 16 | username: 'bob', 17 | password: 'password1' 18 | } 19 | 20 | const expectedAction = { 21 | type: 'REQUEST_LOGIN', 22 | credentials 23 | } 24 | 25 | const actualAction = actions.requestLogin(credentials) 26 | 27 | expect(actualAction).toEqual(expectedAction) 28 | }) 29 | 30 | it('should create login successful action', () => { 31 | const expectedAction = { 32 | type: 'REQUEST_LOGIN_SUCCESS' 33 | } 34 | const actualAction = actions.loginSuccessful() 35 | expect(actualAction).toEqual(expectedAction) 36 | }) 37 | 38 | it('should create login failed action', () => { 39 | const validationResult = { 40 | isValid: false, 41 | usernameValidationMessage: 'Username is required', 42 | passwordValidationMessage: 'Password is required' 43 | } 44 | 45 | const expectedAction = { 46 | type: 'REQUEST_LOGIN_FAILURE', 47 | validationResult 48 | } 49 | 50 | const actualAction = actions.loginFailed(validationResult) 51 | 52 | expect(actualAction).toEqual(expectedAction) 53 | }) 54 | 55 | it('should create request logout action', () => { 56 | const expectedAction = { 57 | type: 'REQUEST_LOGOUT' 58 | } 59 | 60 | const actualAction = actions.requestLogout() 61 | 62 | expect(actualAction).toEqual(expectedAction) 63 | }) 64 | 65 | it('should create logout successful action', () => { 66 | const expectedAction = { 67 | type: 'REQUEST_LOGOUT_SUCCESS' 68 | } 69 | 70 | const actualAction = actions.logoutSuccessful() 71 | 72 | expect(actualAction).toEqual(expectedAction) 73 | }) 74 | 75 | it('should create show new account form action', () => { 76 | const expectedAction = { 77 | type: 'SHOW_NEW_ACCOUNTS_FORM' 78 | } 79 | 80 | const actualAction = actions.showNewAccountForm() 81 | 82 | expect(actualAction).toEqual(expectedAction) 83 | }) 84 | 85 | it('should create hide new account form action', () => { 86 | const expectedAction = { 87 | type: 'HIDE_NEW_ACCOUNTS_FORM' 88 | } 89 | 90 | const actualAction = actions.hideNewAccountForm() 91 | 92 | expect(actualAction).toEqual(expectedAction) 93 | }) 94 | 95 | it('should create show transfer funds action', () => { 96 | const expectedAction = { 97 | type: 'SHOW_TRANSFER_FUNDS' 98 | } 99 | 100 | const actualAction = actions.showTransferFunds() 101 | 102 | expect(actualAction).toEqual(expectedAction) 103 | }) 104 | 105 | it('should create hide transfer funds action', () => { 106 | const expectedAction = { 107 | type: 'HIDE_TRANSFER_FUNDS' 108 | } 109 | 110 | const actualAction = actions.hideTransferFunds() 111 | 112 | expect(actualAction).toEqual(expectedAction) 113 | }) 114 | }) -------------------------------------------------------------------------------- /frontend/src/actions/asyncActionCreators.js: -------------------------------------------------------------------------------- 1 | import { browserHistory } from "react-router"; 2 | import { 3 | requestLogin, 4 | loginSuccessful, 5 | loginFailed, 6 | requestLogout, 7 | logoutSuccessful, 8 | invalidCreateAccountRequest, 9 | hideTransferFunds, 10 | invalidTransferFundsRequest, 11 | requestAccountById, 12 | } from "./actionCreators"; 13 | 14 | import * as actionTypes from "./constants"; 15 | 16 | import formatMoney from "../formatMoney"; 17 | import { CALL_API } from "../middleware/api"; 18 | 19 | const validateLoginRequest = (credentials, existingAccounts) => { 20 | let result = { 21 | isValid: true, 22 | usernameValidationMessage: null, 23 | passwordValidationMessage: null, 24 | id: null, 25 | }; 26 | if (!credentials.username) { 27 | result.isValid = false; 28 | result.usernameValidationMessage = "Username is required"; 29 | } 30 | 31 | if (!credentials.password) { 32 | result.isValid = false; 33 | result.passwordValidationMessage = "Password is required"; 34 | } 35 | const val = existingAccounts.find( 36 | (account) => 37 | account.name === credentials.username && 38 | account.password === credentials.password 39 | ); 40 | if (val) { 41 | result.id = val.id; 42 | } else { 43 | result.isValid = false; 44 | result.usernameValidationMessage = "Either Username or password is wrong"; 45 | result.passwordValidationMessage = "Either Username or password is wrong"; 46 | } 47 | 48 | return result; 49 | }; 50 | 51 | export const attemptLogin = (credentials) => { 52 | return (dispatch, getState) => { 53 | const { accounts } = getState(); 54 | let validationResult = validateLoginRequest( 55 | credentials, 56 | accounts.items || [] 57 | ); 58 | 59 | if (!validationResult.isValid) { 60 | return Promise.resolve(dispatch(loginFailed(validationResult))); 61 | } 62 | // fetchAccounts(); 63 | console.log("id finded here " + validationResult.id); 64 | dispatch(requestAccountById(validationResult.id)); 65 | dispatch(requestLogin(credentials)); 66 | 67 | dispatch(loginSuccessful()); 68 | if (validationResult.id === -1) { 69 | browserHistory.push("/accounts"); 70 | } else { 71 | browserHistory.push("/user-accounts"); 72 | } 73 | 74 | return Promise.resolve(); 75 | }; 76 | }; 77 | 78 | export const attemptLogout = () => { 79 | return (dispatch) => { 80 | dispatch(requestLogout()); 81 | dispatch(logoutSuccessful()); 82 | browserHistory.push("/"); 83 | }; 84 | }; 85 | 86 | export const fetchAccounts = () => ({ 87 | [CALL_API]: { 88 | types: [ 89 | actionTypes.REQUEST_ACCOUNTS, 90 | actionTypes.RECEIVE_ACCOUNTS, 91 | actionTypes.REQUEST_ACCOUNTS_FAILURE, 92 | ], 93 | endpoint: "/accounts", 94 | }, 95 | }); 96 | 97 | export const fetchTransactions = (accountId) => ({ 98 | [CALL_API]: { 99 | types: [ 100 | "REQUEST_TRANSACTIONS", 101 | "RECEIVE_TRANSACTIONS", 102 | "REQUEST_TRANSACTIONS_FAILURE", 103 | ], 104 | endpoint: `/transactions/${accountId}`, 105 | }, 106 | }); 107 | 108 | const validateCreateAccountRequest = ( 109 | name, 110 | openingBalance, 111 | existingAccounts 112 | ) => { 113 | let result = { 114 | isValid: true, 115 | nameValidationMessage: null, 116 | openingBalanceValidationMessage: null, 117 | }; 118 | 119 | if (!name) { 120 | result.isValid = false; 121 | result.nameValidationMessage = "User Name is required"; 122 | } 123 | 124 | if ( 125 | existingAccounts.find( 126 | (account) => account.name.toLowerCase() === name.toLowerCase() 127 | ) 128 | ) { 129 | result.isValid = false; 130 | result.nameValidationMessage = `User Name ${name} already exists!`; 131 | } 132 | 133 | if ( 134 | typeof openingBalance === "string" && 135 | openingBalance.trim().length === 0 136 | ) { 137 | result.isValid = false; 138 | result.openingBalanceValidationMessage = "Opening Balance is required"; 139 | } else { 140 | openingBalance = parseFloat(openingBalance); 141 | if (Number.isNaN(openingBalance)) { 142 | result.isValid = false; 143 | result.openingBalanceValidationMessage = 144 | "Opening Balance must be a number"; 145 | } else if (openingBalance < 0.01) { 146 | result.isValid = false; 147 | result.openingBalanceValidationMessage = `Opening Balance cannot be less than ${formatMoney( 148 | 0.01 149 | )}`; 150 | } else if (openingBalance > 1000.0) { 151 | result.isValid = false; 152 | result.openingBalanceValidationMessage = `Jeez, I know this is a fake app, but we can't give out more than ${formatMoney( 153 | 1000.0 154 | )}`; 155 | } 156 | } 157 | 158 | return result; 159 | }; 160 | 161 | const submitCreateAccountRequest = ( 162 | name, 163 | openingBalance, 164 | accountType, 165 | password 166 | ) => ({ 167 | [CALL_API]: { 168 | types: [ 169 | actionTypes.CREATE_ACCOUNT_REQUEST, 170 | actionTypes.CREATE_ACCOUNT_SUCCESS, 171 | actionTypes.CREATE_ACCOUNT_FAILED, 172 | ], 173 | endpoint: "/accounts", 174 | method: "POST", 175 | data: { name, balance: openingBalance, accountType, password }, 176 | }, 177 | }); 178 | 179 | export const createAccount = (name, openingBalance, username, password) => { 180 | return (dispatch, getState) => { 181 | const { accounts } = getState(); 182 | 183 | let validationResult = validateCreateAccountRequest( 184 | name, 185 | openingBalance, 186 | 187 | accounts.items || [] 188 | ); 189 | 190 | if (!validationResult.isValid) { 191 | return Promise.resolve( 192 | dispatch(invalidCreateAccountRequest(validationResult)) 193 | ); 194 | } 195 | 196 | name = name.trim(); 197 | 198 | if (typeof openingBalance === "string") { 199 | openingBalance = parseFloat(openingBalance.trim()); 200 | } 201 | 202 | dispatch( 203 | submitCreateAccountRequest(name, openingBalance, username, password) 204 | ); 205 | }; 206 | }; 207 | 208 | const postTransaction = (description, debit, credit, accountId) => ({ 209 | [CALL_API]: { 210 | types: [ 211 | actionTypes.CREATE_TRANSACTION_REQUEST, 212 | actionTypes.CREATE_TRANSACTION_SUCCESS, 213 | actionTypes.CREATE_TRANSACTION_FAILED, 214 | ], 215 | endpoint: "/transactions", 216 | method: "POST", 217 | data: { date: new Date(), description, debit, credit, accountId }, 218 | }, 219 | }); 220 | 221 | const updateAccountBalance = (accountId, newBalance) => ({ 222 | [CALL_API]: { 223 | types: [ 224 | actionTypes.UPDATE_ACCOUNT_BALANCE_REQUEST, 225 | actionTypes.UPDATE_ACCOUNT_BALANCE_SUCCESS, 226 | actionTypes.UPDATE_ACCOUNT_BALANCE_FAILED, 227 | ], 228 | endpoint: `/accounts/${accountId}`, 229 | method: "PATCH", 230 | data: { balance: newBalance }, 231 | }, 232 | }); 233 | 234 | const creditAccount = (account, amount) => { 235 | const newBalance = account.balance + amount; 236 | return updateAccountBalance(account.id, newBalance); 237 | }; 238 | 239 | const debitAccount = (account, amount) => { 240 | const newBalance = account.balance - amount; 241 | return updateAccountBalance(account.id, newBalance); 242 | }; 243 | 244 | const validateTransferFundsRequest = ( 245 | fromAccount, 246 | toAccount, 247 | transferAmount 248 | ) => { 249 | let result = { 250 | isValid: true, 251 | fromAccountValidationMessage: null, 252 | toAccountValidationMessage: null, 253 | transferAmountValidationMessage: null, 254 | }; 255 | 256 | if (!fromAccount) { 257 | result.isValid = false; 258 | result.fromAccountValidationMessage = "From Account is required"; 259 | } 260 | 261 | if (!toAccount) { 262 | result.isValid = false; 263 | result.toAccountValidationMessage = "To Account is required"; 264 | } 265 | 266 | if ( 267 | typeof transferAmount === "string" && 268 | transferAmount.trim().length === 0 269 | ) { 270 | result.isValid = false; 271 | result.transferAmountValidationMessage = "Transfer Amount is required"; 272 | } else { 273 | transferAmount = parseFloat(transferAmount); 274 | if (Number.isNaN(transferAmount)) { 275 | result.isValid = false; 276 | result.transferAmountValidationMessage = 277 | "Transfer Amount must be a number"; 278 | } else if (transferAmount < 0.01) { 279 | result.isValid = false; 280 | result.transferAmountValidationMessage = `Transfer Amount cannot be less that ${formatMoney( 281 | 0.01 282 | )}`; 283 | } else if (fromAccount && transferAmount > fromAccount.balance) { 284 | result.isValid = false; 285 | result.transferAmountValidationMessage = `Insufficent funds in account ${ 286 | fromAccount.name 287 | }. You can transfer up to ${formatMoney(fromAccount.balance)}`; 288 | } 289 | } 290 | 291 | if (result.isValid) { 292 | if (toAccount.id === fromAccount.id) { 293 | result.isValid = false; 294 | result.toAccountValidationMessage = 295 | "You cannot transfer funds to the same account"; 296 | } 297 | } 298 | 299 | return result; 300 | }; 301 | 302 | export const transferFunds = (fromAccount, toAccount, transferAmount) => { 303 | return (dispatch) => { 304 | let validationResult = validateTransferFundsRequest( 305 | fromAccount, 306 | toAccount, 307 | transferAmount 308 | ); 309 | 310 | if (!validationResult.isValid) { 311 | return Promise.resolve( 312 | dispatch(invalidTransferFundsRequest(validationResult)) 313 | ); 314 | } 315 | transferAmount = parseFloat(transferAmount); 316 | 317 | dispatch( 318 | postTransaction( 319 | `Transfer to ${toAccount.name}`, 320 | transferAmount, 321 | null, 322 | fromAccount.id 323 | ) 324 | ); 325 | 326 | dispatch(debitAccount(fromAccount, transferAmount)); 327 | 328 | dispatch( 329 | postTransaction( 330 | `Transfer from ${fromAccount.name}`, 331 | null, 332 | transferAmount, 333 | toAccount.id 334 | ) 335 | ); 336 | 337 | dispatch(creditAccount(toAccount, transferAmount)); 338 | 339 | dispatch(hideTransferFunds()); 340 | }; 341 | }; 342 | -------------------------------------------------------------------------------- /frontend/src/actions/asyncActionCreators.spec.js: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store' 2 | import thunk from 'redux-thunk' 3 | import * as actionTypes from './constants' 4 | import * as actions from './asyncActionCreators' 5 | 6 | const middlewares = [thunk] 7 | const mockStore = configureMockStore(middlewares) 8 | 9 | describe('Create Account', () => { 10 | describe('Validation', () => { 11 | it('should not allow an empty account name', () => { 12 | const store = mockStore({ accounts: [] }) 13 | 14 | const expectedActions = [{ 15 | type: actionTypes.CREATE_ACCOUNT_VALIDATION_FAILURE, 16 | validationResult: { 17 | isValid: false, 18 | nameValidationMessage: 'Account Name is required', 19 | openingBalanceValidationMessage: null 20 | } 21 | }] 22 | 23 | return store.dispatch(actions.createAccount('', 1.00)) 24 | .then(() => { 25 | expect(store.getActions()).toEqual(expectedActions) 26 | }) 27 | }) 28 | 29 | it('should not allow duplicate account names', () => { 30 | const store = mockStore({ 31 | accounts: { 32 | items: [ 33 | { id: 1, name: 'Test Account', balance: 0.01 } 34 | ] 35 | } 36 | }) 37 | 38 | const expectedActions = [{ 39 | type: actionTypes.CREATE_ACCOUNT_VALIDATION_FAILURE, 40 | validationResult: { 41 | isValid: false, 42 | nameValidationMessage: 'Account Name Test Account already exists!', 43 | openingBalanceValidationMessage: null 44 | } 45 | }] 46 | 47 | return store.dispatch(actions.createAccount('Test Account', 1.00)) 48 | .then(() => { 49 | expect(store.getActions()).toEqual(expectedActions) 50 | }) 51 | }) 52 | 53 | it('should disallow no opening balance', () => { 54 | const store = mockStore({ accounts: [] }) 55 | 56 | const expectedActions = [{ 57 | type: actionTypes.CREATE_ACCOUNT_VALIDATION_FAILURE, 58 | validationResult: { 59 | isValid: false, 60 | nameValidationMessage: null, 61 | openingBalanceValidationMessage: 'Opening Balance is required' 62 | } 63 | }] 64 | 65 | return store.dispatch(actions.createAccount('Foo', '')) 66 | .then(() => { 67 | expect(store.getActions()).toEqual(expectedActions) 68 | }) 69 | }) 70 | 71 | it('should ensure that opening balance is a number', () => { 72 | const store = mockStore({ accounts: [] }) 73 | 74 | const expectedActions = [{ 75 | type: actionTypes.CREATE_ACCOUNT_VALIDATION_FAILURE, 76 | validationResult: { 77 | isValid: false, 78 | nameValidationMessage: null, 79 | openingBalanceValidationMessage: 'Opening Balance must be a number' 80 | } 81 | }] 82 | 83 | return store.dispatch(actions.createAccount('Foo', 'bar')) 84 | .then(() => { 85 | expect(store.getActions()).toEqual(expectedActions) 86 | }) 87 | }) 88 | 89 | it('should ensure opening balance is greater than zero', () => { 90 | const store = mockStore({ accounts: [] }) 91 | 92 | const expectedActions = [{ 93 | type: actionTypes.CREATE_ACCOUNT_VALIDATION_FAILURE, 94 | validationResult: { 95 | isValid: false, 96 | nameValidationMessage: null, 97 | openingBalanceValidationMessage: 'Opening Balance cannot be less than £0.01' 98 | } 99 | }] 100 | 101 | return store.dispatch(actions.createAccount('Foo', '0')) 102 | .then(() => { 103 | expect(store.getActions()).toEqual(expectedActions) 104 | }) 105 | }) 106 | 107 | it('should ensure opening balance is 1000 or less', () => { 108 | const store = mockStore({ accounts: [] }) 109 | 110 | const expectedActions = [{ 111 | type: actionTypes.CREATE_ACCOUNT_VALIDATION_FAILURE, 112 | validationResult: { 113 | isValid: false, 114 | nameValidationMessage: null, 115 | openingBalanceValidationMessage: 'Jeez, I know this is a fake app, but we can\'t give out more than £1,000.00' 116 | } 117 | }] 118 | 119 | return store.dispatch(actions.createAccount('Foo', '1000.01')) 120 | .then(() => { 121 | expect(store.getActions()).toEqual(expectedActions) 122 | }) 123 | }) 124 | }) 125 | }) 126 | 127 | describe('Transfer Funds', () => { 128 | describe('Validation', () => { 129 | it('should ensure a from account is selected', () => { 130 | const accounts = [ 131 | { id: 1, name: 'Current Account', balance: 100.00 }, 132 | { id: 2, name: 'Savings Account', balance: 50.00 } 133 | ] 134 | 135 | const store = mockStore({ 136 | accounts: { 137 | items: accounts 138 | } 139 | }) 140 | 141 | const expectedActions = [{ 142 | type: actionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE, 143 | validationResult: { 144 | isValid: false, 145 | fromAccountValidationMessage: 'From Account is required', 146 | toAccountValidationMessage: null, 147 | transferAmountValidationMessage: null 148 | } 149 | }] 150 | 151 | return store.dispatch(actions.transferFunds(null, accounts[0], 20.00)) 152 | .then(() => { 153 | expect(store.getActions()).toEqual(expectedActions) 154 | }) 155 | }) 156 | it('should ensure a to account is selected', () => { 157 | const accounts = [ 158 | { id: 1, name: 'Current Account', balance: 100.00 }, 159 | { id: 2, name: 'Savings Account', balance: 50.00 } 160 | ] 161 | 162 | const store = mockStore({ 163 | accounts: { 164 | items: accounts 165 | } 166 | }) 167 | 168 | const expectedActions = [{ 169 | type: actionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE, 170 | validationResult: { 171 | isValid: false, 172 | fromAccountValidationMessage: null, 173 | toAccountValidationMessage: 'To Account is required', 174 | transferAmountValidationMessage: null 175 | } 176 | }] 177 | 178 | return store.dispatch(actions.transferFunds(accounts[1], null, 20.00)) 179 | .then(() => { 180 | expect(store.getActions()).toEqual(expectedActions) 181 | }) 182 | }) 183 | it('should ensure transfer amount is set', () => { 184 | const accounts = [ 185 | { id: 1, name: 'Current Account', balance: 100.00 }, 186 | { id: 2, name: 'Savings Account', balance: 50.00 } 187 | ] 188 | 189 | const store = mockStore({ 190 | accounts: { 191 | items: accounts 192 | } 193 | }) 194 | 195 | const expectedActions = [{ 196 | type: actionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE, 197 | validationResult: { 198 | isValid: false, 199 | fromAccountValidationMessage: null, 200 | toAccountValidationMessage: null, 201 | transferAmountValidationMessage: 'Transfer Amount is required' 202 | } 203 | }] 204 | 205 | return store.dispatch(actions.transferFunds(accounts[0], accounts[1], '')) 206 | .then(() => { 207 | expect(store.getActions()).toEqual(expectedActions) 208 | }) 209 | }) 210 | 211 | it('should ensure transfer amount is a number', () => { 212 | const accounts = [ 213 | { id: 1, name: 'Current Account', balance: 100.00 }, 214 | { id: 2, name: 'Savings Account', balance: 50.00 } 215 | ] 216 | 217 | const store = mockStore({ 218 | accounts: { 219 | items: accounts 220 | } 221 | }) 222 | 223 | const expectedActions = [{ 224 | type: actionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE, 225 | validationResult: { 226 | isValid: false, 227 | fromAccountValidationMessage: null, 228 | toAccountValidationMessage: null, 229 | transferAmountValidationMessage: 'Transfer Amount must be a number' 230 | } 231 | }] 232 | 233 | return store.dispatch(actions.transferFunds(accounts[0], accounts[1], 'foo')) 234 | .then(() => { 235 | expect(store.getActions()).toEqual(expectedActions) 236 | }) 237 | }) 238 | 239 | it('should ensure transfer amount is greater than zero', () => { 240 | const accounts = [ 241 | { id: 1, name: 'Current Account', balance: 100.00 }, 242 | { id: 2, name: 'Savings Account', balance: 50.00 } 243 | ] 244 | 245 | const store = mockStore({ 246 | accounts: { 247 | items: accounts 248 | } 249 | }) 250 | 251 | const expectedActions = [{ 252 | type: actionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE, 253 | validationResult: { 254 | isValid: false, 255 | fromAccountValidationMessage: null, 256 | toAccountValidationMessage: null, 257 | transferAmountValidationMessage: 'Transfer Amount cannot be less that £0.01' 258 | } 259 | }] 260 | 261 | return store.dispatch(actions.transferFunds(accounts[0], accounts[1], '0')) 262 | .then(() => { 263 | expect(store.getActions()).toEqual(expectedActions) 264 | }) 265 | }) 266 | 267 | it('should ensure transfer amount cannot exceed amount available in from account', () => { 268 | const accounts = [ 269 | { id: 1, name: 'Current Account', balance: 100.00 }, 270 | { id: 2, name: 'Savings Account', balance: 50.00 } 271 | ] 272 | 273 | const store = mockStore({ 274 | accounts: { 275 | items: accounts 276 | } 277 | }) 278 | 279 | const expectedActions = [{ 280 | type: actionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE, 281 | validationResult: { 282 | isValid: false, 283 | fromAccountValidationMessage: null, 284 | toAccountValidationMessage: null, 285 | transferAmountValidationMessage: 'Insufficent funds in account Current Account. You can transfer up to £100.00' 286 | } 287 | }] 288 | 289 | return store.dispatch(actions.transferFunds(accounts[0], accounts[1], '200.00')) 290 | .then(() => { 291 | expect(store.getActions()).toEqual(expectedActions) 292 | }) 293 | }) 294 | 295 | it('should prevent transferring funds between the same account', () => { 296 | const accounts = [ 297 | { id: 1, name: 'Current Account', balance: 100.00 }, 298 | { id: 2, name: 'Savings Account', balance: 50.00 } 299 | ] 300 | 301 | const store = mockStore({ 302 | accounts: { 303 | items: accounts 304 | } 305 | }) 306 | 307 | const expectedActions = [{ 308 | type: actionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE, 309 | validationResult: { 310 | isValid: false, 311 | fromAccountValidationMessage: null, 312 | toAccountValidationMessage: 'You cannot transfer funds to the same account', 313 | transferAmountValidationMessage: null 314 | } 315 | }] 316 | 317 | return store.dispatch(actions.transferFunds(accounts[0], accounts[0], '50.00')) 318 | .then(() => { 319 | expect(store.getActions()).toEqual(expectedActions) 320 | }) 321 | }) 322 | }) 323 | }) -------------------------------------------------------------------------------- /frontend/src/actions/constants.js: -------------------------------------------------------------------------------- 1 | export const RESET_ERROR_MESSAGE = "RESET_ERROR_MESSAGE"; 2 | 3 | export const REQUEST_LOGIN = "REQUEST_LOGIN"; 4 | export const REQUEST_LOGIN_SUCCESS = "REQUEST_LOGIN_SUCCESS"; 5 | export const REQUEST_LOGIN_FAILURE = "REQUEST_LOGIN_FAILURE"; 6 | 7 | export const REQUEST_LOGOUT = "REQUEST_LOGOUT"; 8 | export const REQUEST_LOGOUT_SUCCESS = "REQUEST_LOGOUT_SUCCESS"; 9 | 10 | export const REQUEST_ACCOUNTBYID = "REQUEST_ACCOUNTBYID"; 11 | export const REQUEST_ACCOUNTS = "REQUEST_ACCOUNTS"; 12 | export const RECEIVE_ACCOUNTS = "RECEIVE_ACCOUNTS"; 13 | export const REQUEST_ACCOUNTS_FAILURE = "REQUEST_ACCOUNTS_FAILURE"; 14 | 15 | export const REQUEST_TRANSACTIONS = "REQUEST_TRANSACTIONS"; 16 | export const RECEIVE_TRANSACTIONS = "RECEIVE_TRANSACTIONS"; 17 | export const REQUEST_TRANSACTIONS_FAILURE = "REQUEST_TRANSACTIONS_FAILURE"; 18 | export const SHOW_NEW_ACCOUNTS_FORM = "SHOW_NEW_ACCOUNTS_FORM"; 19 | export const HIDE_NEW_ACCOUNTS_FORM = "HIDE_NEW_ACCOUNTS_FORM"; 20 | 21 | export const CREATE_ACCOUNT_REQUEST = "CREATE_ACCOUNT_REQUEST"; 22 | export const CREATE_ACCOUNT_VALIDATION_FAILURE = 23 | "CREATE_ACCOUNT_VALIDATION_FAILURE"; 24 | export const CREATE_ACCOUNT_SUCCESS = "CREATE_ACCOUNT_SUCCESS"; 25 | export const CREATE_ACCOUNT_FAILED = "CREATE_ACCOUNT_FAILED"; 26 | 27 | export const SHOW_TRANSFER_FUNDS = "SHOW_TRANSFER_FUNDS"; 28 | export const HIDE_TRANSFER_FUNDS = "HIDE_TRANSFER_FUNDS"; 29 | 30 | export const TRANSFER_FUNDS_VALIDATION_FAILURE = 31 | "TRANSFER_FUNDS_VALIDATION_FAILURE"; 32 | 33 | export const CREATE_TRANSACTION_REQUEST = "CREATE_TRANSACTION_REQUEST"; 34 | export const CREATE_TRANSACTION_SUCCESS = "CREATE_TRANSACTION_SUCCESS"; 35 | export const CREATE_TRANSACTION_FAILED = "CREATE_TRANSACTION_FAILED"; 36 | 37 | export const UPDATE_ACCOUNT_BALANCE_REQUEST = "UPDATE_ACCOUNT_BALANCE_REQUEST"; 38 | export const UPDATE_ACCOUNT_BALANCE_SUCCESS = "UPDATE_ACCOUNT_BALANCE_SUCCESS"; 39 | export const UPDATE_ACCOUNT_BALANCE_FAILED = "UPDATE_ACCOUNT_BALANCE_FAILED"; 40 | -------------------------------------------------------------------------------- /frontend/src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './asyncActionCreators' 2 | export * from './actionCreators' -------------------------------------------------------------------------------- /frontend/src/components/Account.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, CardTitle, CardActions } from 'material-ui/Card' 3 | import FlatButton from 'material-ui/FlatButton' 4 | import formatMoney from '../formatMoney' 5 | 6 | const Account = ({ 7 | id, 8 | name, 9 | balance, 10 | viewTransactions 11 | }) => ( 12 | 13 | 14 | 15 | viewTransactions(id)} label="View Transactions" primary={true} /> 16 | 17 | 18 | ) 19 | 20 | export default Account -------------------------------------------------------------------------------- /frontend/src/components/Logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FlatButton from 'material-ui/FlatButton' 3 | 4 | const Logout = ({ 5 | visible, 6 | onClick 7 | }) => ( 8 | 9 | { 10 | visible && 11 | 12 | } 13 | 14 | ) 15 | 16 | export default Logout -------------------------------------------------------------------------------- /frontend/src/components/Logout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import FlatButton from 'material-ui/FlatButton' 4 | import Logout from './Logout'; 5 | 6 | describe(' component', () => { 7 | it('shows logout when visible is set to true', () => { 8 | const wrapper = shallow( 9 | 10 | ) 11 | 12 | expect(wrapper.find(FlatButton).length).toBe(1) 13 | }) 14 | 15 | it('hides logout when visible is not set', () => { 16 | const wrapper = shallow( 17 | 18 | ) 19 | 20 | expect(wrapper.find(FlatButton).length).toBe(0) 21 | }) 22 | 23 | it('has correct label', () => { 24 | const wrapper = shallow( 25 | 26 | ) 27 | 28 | expect(wrapper.find(FlatButton).props().label).toBe('Logout') 29 | }) 30 | 31 | it('handles click event', () => { 32 | let onButtonClick = jest.fn() 33 | 34 | const wrapper = shallow( 35 | 36 | ) 37 | 38 | const button = wrapper.find(FlatButton) 39 | 40 | expect(onButtonClick.mock.calls.length).toBe(0) 41 | button.simulate('click') 42 | expect(onButtonClick.mock.calls.length).toBe(1) 43 | }) 44 | }) -------------------------------------------------------------------------------- /frontend/src/components/TransactionList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table' 3 | import moment from 'moment' 4 | import formatMoney from '../formatMoney' 5 | 6 | const formatDisplayAmount = (amount) => { 7 | return amount ? formatMoney(amount) : ' - ' 8 | } 9 | 10 | const TransactionList = ({ 11 | transactions 12 | }) => ( 13 | 14 | 15 | 16 | Date 17 | Description 18 | Debit 19 | Credit 20 | 21 | 22 | 23 | { 24 | transactions.map(transaction => { 25 | return ( 26 | 27 | {moment(transaction.date).fromNow()} 28 | {transaction.description} 29 | {formatDisplayAmount(transaction.debit)} 30 | {formatDisplayAmount(transaction.credit)} 31 | 32 | ) 33 | }) 34 | } 35 | 36 |
37 | ) 38 | 39 | export default TransactionList -------------------------------------------------------------------------------- /frontend/src/containers/AccountsPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { browserHistory } from "react-router"; 3 | import { connect } from "react-redux"; 4 | import FloatingActionButton from "material-ui/FloatingActionButton"; 5 | import ContentAdd from "material-ui/svg-icons/content/add"; 6 | import SwapIcon from "material-ui/svg-icons/action/swap-horiz"; 7 | import FlatButton from "material-ui/FlatButton"; 8 | import Account from "../components/Account"; 9 | import NewAccountDialog from "./NewAccountDialog"; 10 | import TransferFundsDialog from "./TransferFundsDialog"; 11 | 12 | import { 13 | fetchAccounts, 14 | showNewAccountForm, 15 | showTransferFunds, 16 | } from "../actions"; 17 | 18 | const style = { 19 | float: "right", 20 | }; 21 | 22 | class AccountsPage extends Component { 23 | // state = { 24 | // data: [], 25 | // }; 26 | 27 | // startTimer = () => { 28 | // setInterval(() => { 29 | // this.setState({ 30 | // show: true, 31 | // }); 32 | // }, 2000); 33 | // }; 34 | 35 | componentDidMount() { 36 | // this.startTimer(); 37 | const { dispatch, authenticated } = this.props; 38 | if (!authenticated) { 39 | browserHistory.push("/"); 40 | } 41 | dispatch(fetchAccounts()); 42 | // this.forceUpdate(); 43 | // setTimeout(() => { 44 | // this.setState({ 45 | // // show: true, 46 | // }); 47 | // }, 10000); 48 | // this.fetchAccountData(); 49 | } 50 | // componentDidUpdate() {} 51 | 52 | // componentDidUpdate(prevProps, prevState) { 53 | // console.log(prevState.data); 54 | // console.log(this.state.data); 55 | // if (prevState.data !== this.state.data) { 56 | // this.fetchAccountData(); 57 | // this.setState({ 58 | // data: this.state.data.slice(1, this.state.data.length), 59 | // }); 60 | // } 61 | // } 62 | 63 | // fetchAccountData = () => { 64 | // console.log("hello -->0"); 65 | // axios.get("http://localhost:3001/accounts").then((res) => { 66 | // this.setState({ 67 | // data: res.data, 68 | // }); 69 | // }); 70 | // }; 71 | 72 | goToTransactions(id) { 73 | browserHistory.push(`/accounts/${id}/transactions`); 74 | } 75 | 76 | render() { 77 | const { 78 | accounts, 79 | onAddAccountClick, 80 | showNewAccountForm, 81 | showTransferFunds, 82 | onTransferFundsClick, 83 | showTransferFundsButton, 84 | } = this.props; 85 | // console.log("account data" + JSON.stringify(this.state.accounts)); 86 | return ( 87 |
88 |

89 | Admin Accounts 90 | {/* {showTransferFundsButton && ( 91 | } 97 | onTouchTap={onTransferFundsClick} 98 | /> 99 | )} */} 100 |

101 |
102 | {accounts.map((account, index) => ( 103 | 108 | ))} 109 |
110 | 115 | 116 | 117 | {showNewAccountForm && } 118 | {showTransferFunds && } 119 |
120 | ); 121 | } 122 | } 123 | 124 | const mapStateToProps = (state) => { 125 | const { accounts, login } = state; 126 | return { 127 | isFetching: accounts.isFetching, 128 | accounts: accounts.items.slice(1, accounts.items.length), 129 | authenticated: login.authenticated, 130 | showNewAccountForm: accounts.showNewAccountForm, 131 | showTransferFunds: accounts.showTransferFunds, 132 | showTransferFundsButton: accounts.showTransferFundsButton, 133 | }; 134 | }; 135 | 136 | const mapDispatchToProps = (dispatch) => { 137 | return { 138 | onAddAccountClick: () => { 139 | dispatch(showNewAccountForm()); 140 | }, 141 | onTransferFundsClick: () => { 142 | dispatch(showTransferFunds()); 143 | }, 144 | dispatch, 145 | }; 146 | }; 147 | 148 | export default connect(mapStateToProps, mapDispatchToProps)(AccountsPage); 149 | -------------------------------------------------------------------------------- /frontend/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react"; 2 | import { connect } from "react-redux"; 3 | import { browserHistory } from "react-router"; 4 | import AppBar from "material-ui/AppBar"; 5 | import { resetErrorMessage, attemptLogout } from "../actions"; 6 | import "./app.css"; 7 | import Logout from "../components/Logout"; 8 | 9 | const styles = { 10 | title: { 11 | cursor: "pointer", 12 | }, 13 | }; 14 | 15 | class App extends Component { 16 | static propTypes = { 17 | errorMessage: PropTypes.string, 18 | resetErrorMessage: PropTypes.func.isRequired, 19 | children: PropTypes.node, 20 | }; 21 | 22 | goHome() { 23 | browserHistory.push("/"); 24 | } 25 | 26 | handleDismissClick = (e) => { 27 | this.props.resetErrorMessage(); 28 | e.preventDefault(); 29 | }; 30 | 31 | renderErrorMessage() { 32 | const { errorMessage } = this.props; 33 | 34 | if (!errorMessage) { 35 | return null; 36 | } 37 | 38 | return ( 39 |

40 | {errorMessage} ( 41 | 42 | Dismiss 43 | 44 | ) 45 |

46 | ); 47 | } 48 | 49 | render() { 50 | const { children, authenticated, onLogoutClick } = this.props; 51 | return ( 52 |
53 | Dev Stack Bank} 55 | // onTitleTouchTap={() => this.goHome()} 56 | showMenuIconButton={false} 57 | iconElementRight={ 58 | 59 | } 60 | /> 61 | {this.renderErrorMessage()} 62 | {children} 63 |
64 | ); 65 | } 66 | } 67 | 68 | const mapStateToProps = (state) => { 69 | const { login } = state; 70 | return { 71 | errorMessage: state.errorMessage, 72 | authenticated: login.authenticated, 73 | }; 74 | }; 75 | 76 | const mapDispatchToProps = (dispatch) => { 77 | return { 78 | onLogoutClick: () => { 79 | dispatch(attemptLogout()); 80 | }, 81 | resetErrorMessage, 82 | }; 83 | }; 84 | 85 | export default connect(mapStateToProps, mapDispatchToProps)(App); 86 | -------------------------------------------------------------------------------- /frontend/src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /frontend/src/containers/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import TextField from "material-ui/TextField"; 4 | import RaisedButton from "material-ui/RaisedButton"; 5 | import { attemptLogin } from "../actions"; 6 | // import { browserHistory } from "react-router"; 7 | 8 | import { fetchAccounts } from "../actions"; 9 | 10 | class Login extends Component { 11 | componentDidMount() { 12 | const { dispatch } = this.props; 13 | 14 | dispatch(fetchAccounts()); 15 | } 16 | 17 | onLoginClick() { 18 | const { dispatch } = this.props; 19 | let username = this.refs.username.input.value; 20 | let password = this.refs.password.input.value; 21 | 22 | // console.log("jdbbdvb " + u); 23 | dispatch( 24 | attemptLogin({ 25 | username, 26 | password, 27 | }) 28 | ); 29 | } 30 | 31 | render() { 32 | const { usernameValidationMessage, passwordValidationMessage } = this.props; 33 | return ( 34 |
35 |

Login

36 | 37 |

38 | Enter your username and password{" "} 39 | {/* 40 | (anything will do, it's not real) 41 | */} 42 |

43 | 44 |
45 | 50 |
51 |
52 | 59 |
60 | this.onLoginClick()} 64 | /> 65 |
66 | ); 67 | } 68 | } 69 | 70 | const mapStateToProps = (state) => { 71 | const { login } = state; 72 | return { 73 | usernameValidationMessage: login.usernameValidationMessage, 74 | passwordValidationMessage: login.passwordValidationMessage, 75 | }; 76 | }; 77 | 78 | export default connect(mapStateToProps)(Login); 79 | -------------------------------------------------------------------------------- /frontend/src/containers/NewAccountDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import Dialog from "material-ui/Dialog"; 4 | import TextField from "material-ui/TextField"; 5 | import FlatButton from "material-ui/FlatButton"; 6 | import { createAccount, hideNewAccountForm, fetchAccounts } from "../actions"; 7 | import { browserHistory } from "react-router"; 8 | class NewAccountDialog extends Component { 9 | async submitRequest() { 10 | const { dispatch } = this.props; 11 | 12 | let accountType = this.refs.accountType.input.value; 13 | let openingBalance = this.refs.openingBalance.input.value; 14 | let userName = this.refs.userName.input.value; 15 | let password = this.refs.password.input.value; 16 | console.log("opening balance" + openingBalance); 17 | 18 | dispatch(createAccount(userName, openingBalance, accountType, password)); 19 | dispatch(fetchAccounts()); 20 | // setInterval(() => { 21 | // console.log("hello world"); 22 | // }, 2000); 23 | // await this.sleep(3000); 24 | // this.forceUpdate(); 25 | browserHistory.push("/accounts"); 26 | } 27 | 28 | // sleep = (ms) => { 29 | // return new Promise((resolve) => setTimeout(resolve, ms)); 30 | // }; 31 | 32 | cancel() { 33 | const { dispatch } = this.props; 34 | 35 | dispatch(hideNewAccountForm()); 36 | } 37 | 38 | render() { 39 | const { 40 | showNewAccountForm, 41 | nameValidationMessage, 42 | openingBalanceValidationMessage, 43 | } = this.props; 44 | 45 | const actions = [ 46 | this.submitRequest()} 50 | />, 51 | this.cancel()} 55 | />, 56 | ]; 57 | 58 | return ( 59 | this.cancel()} 65 | > 66 |
67 |
68 | 73 |
74 | 78 |
79 |
80 | 85 |
86 | 87 |
88 | 89 |
90 |
91 | ); 92 | } 93 | } 94 | 95 | const mapStateToProps = (state) => { 96 | const { accounts } = state; 97 | return { 98 | accounts: accounts.items, 99 | showNewAccountForm: accounts.showNewAccountForm, 100 | nameValidationMessage: accounts.nameValidationMessage, 101 | openingBalanceValidationMessage: accounts.openingBalanceValidationMessage, 102 | }; 103 | }; 104 | 105 | export default connect(mapStateToProps)(NewAccountDialog); 106 | -------------------------------------------------------------------------------- /frontend/src/containers/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from "react"; 2 | import { Provider } from "react-redux"; 3 | import routes from "../routes"; 4 | // import DevTools from "./DevTools"; 5 | import { Router } from "react-router"; 6 | 7 | const Root = ({ store, history }) => ( 8 | 9 |
10 | 11 | {/* */} 12 |
13 |
14 | ); 15 | 16 | Root.propTypes = { 17 | store: PropTypes.object.isRequired, 18 | history: PropTypes.object.isRequired, 19 | }; 20 | 21 | export default Root; 22 | -------------------------------------------------------------------------------- /frontend/src/containers/Root.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./Root.prod') 3 | } else { 4 | module.exports = require('./Root.dev') 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/containers/Root.prod.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Provider } from 'react-redux' 3 | import routes from '../routes' 4 | import { Router } from 'react-router' 5 | 6 | const Root = ({ store, history }) => ( 7 | 8 | 9 | 10 | ) 11 | 12 | Root.propTypes = { 13 | store: PropTypes.object.isRequired, 14 | history: PropTypes.object.isRequired 15 | } 16 | export default Root 17 | -------------------------------------------------------------------------------- /frontend/src/containers/TransactionsPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { browserHistory } from "react-router"; 3 | import { connect } from "react-redux"; 4 | import Subheader from "material-ui/Subheader"; 5 | import Paper from "material-ui/Paper"; 6 | import FlatButton from "material-ui/FlatButton"; 7 | import { fetchTransactions } from "../actions"; 8 | import formatMoney from "../formatMoney"; 9 | import TransactionList from "../components/TransactionList"; 10 | 11 | class TransactionsPage extends Component { 12 | componentDidMount() { 13 | const { dispatch, authenticated, accountId } = this.props; 14 | if (!authenticated) { 15 | browserHistory.push("/"); 16 | } 17 | dispatch(fetchTransactions(accountId)); 18 | } 19 | 20 | goToAccounts() { 21 | browserHistory.goBack(); 22 | } 23 | 24 | render() { 25 | const { account, transactions } = this.props; 26 | console.log(account); 27 | return ( 28 |
29 | this.goToAccounts()} 31 | label="Back to accounts" 32 | primary={true} 33 | /> 34 | 35 |

{account.name} Transactions

36 | 37 | 38 | Account balance {formatMoney(account.balance)} 39 | {transactions.length === 0 && ( 40 | No transactions available 41 | )} 42 | {transactions.length > 0 && ( 43 | 44 | )} 45 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | const mapStateToProps = (state, ownProps) => { 52 | const accountId = parseInt(ownProps.params.accountId, 10); 53 | const { accounts, transactions, login } = state; 54 | 55 | return { 56 | accountId, 57 | account: accounts.items.find((a) => a.id === accountId), 58 | transactions: transactions.items, 59 | authenticated: login.authenticated, 60 | }; 61 | }; 62 | 63 | export default connect(mapStateToProps)(TransactionsPage); 64 | -------------------------------------------------------------------------------- /frontend/src/containers/TransferFundsDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import Dialog from "material-ui/Dialog"; 4 | import TextField from "material-ui/TextField"; 5 | import SelectField from "material-ui/SelectField"; 6 | import FlatButton from "material-ui/FlatButton"; 7 | import MenuItem from "material-ui/MenuItem"; 8 | import { browserHistory } from "react-router"; 9 | import { 10 | requestAccountById, 11 | hideTransferFunds, 12 | transferFunds, 13 | } from "../actions"; 14 | 15 | class TransferFundsDialog extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | fromAccount: null, 20 | toAccount: null, 21 | }; 22 | } 23 | 24 | handleFromAccountChange = (event, index, value) => 25 | this.setState({ fromAccount: value }); 26 | handleToAccountChange = (event, index, value) => 27 | this.setState({ toAccount: value }); 28 | 29 | submitRequest(id) { 30 | const { dispatch } = this.props; 31 | 32 | let transferAmount = this.refs.transferAmount.input.value; 33 | // dispatch(requestAccountById(id)); 34 | console.log("id goes here yrrr" + id); 35 | dispatch( 36 | transferFunds( 37 | this.state.fromAccount, 38 | this.state.toAccount, 39 | transferAmount 40 | ) 41 | ); 42 | 43 | browserHistory.push("/user-accounts"); 44 | } 45 | 46 | cancel() { 47 | const { dispatch } = this.props; 48 | dispatch(hideTransferFunds()); 49 | } 50 | 51 | render() { 52 | const { 53 | accounts, 54 | item, 55 | showTransferFunds, 56 | fromAccountValidationMessage, 57 | toAccountValidationMessage, 58 | transferAmountValidationMessage, 59 | } = this.props; 60 | 61 | const actions = [ 62 | this.submitRequest(item.id)} 66 | />, 67 | this.cancel()} 71 | />, 72 | ]; 73 | console.log("item goes here " + item.name); 74 | return ( 75 | this.cancel()} 81 | > 82 |
83 | 89 | 90 | 91 |
92 |
93 | 99 | {accounts.map((account) => ( 100 | 105 | ))} 106 | 107 |
108 |
109 | 114 |
115 |
116 | ); 117 | } 118 | } 119 | 120 | const mapStateToProps = (state) => { 121 | const { accounts } = state; 122 | return { 123 | accounts: accounts.items, 124 | item: accounts.item, 125 | showTransferFunds: accounts.showTransferFunds, 126 | fromAccountValidationMessage: accounts.fromAccountValidationMessage, 127 | toAccountValidationMessage: accounts.toAccountValidationMessage, 128 | transferAmountValidationMessage: accounts.transferAmountValidationMessage, 129 | }; 130 | }; 131 | 132 | export default connect(mapStateToProps)(TransferFundsDialog); 133 | -------------------------------------------------------------------------------- /frontend/src/containers/UserAccountPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { browserHistory } from "react-router"; 3 | import { connect } from "react-redux"; 4 | import FloatingActionButton from "material-ui/FloatingActionButton"; 5 | import ContentAdd from "material-ui/svg-icons/content/add"; 6 | import SwapIcon from "material-ui/svg-icons/action/swap-horiz"; 7 | import FlatButton from "material-ui/FlatButton"; 8 | import Account from "../components/Account"; 9 | import NewAccountDialog from "./NewAccountDialog"; 10 | import TransferFundsDialog from "./TransferFundsDialog"; 11 | import { 12 | fetchAccounts, 13 | requestAccountById, 14 | showNewAccountForm, 15 | showTransferFunds, 16 | } from "../actions"; 17 | 18 | const style = { 19 | float: "right", 20 | }; 21 | 22 | class UserAccountsPage extends Component { 23 | componentDidMount() { 24 | const { dispatch, authenticated } = this.props; 25 | if (!authenticated) { 26 | browserHistory.push("/"); 27 | } 28 | dispatch(fetchAccounts()); 29 | } 30 | 31 | goToTransactions(id) { 32 | browserHistory.push(`/accounts/${id}/transactions`); 33 | } 34 | 35 | render() { 36 | const { 37 | accounts, 38 | account, 39 | onAddAccountClick, 40 | showNewAccountForm, 41 | showTransferFunds, 42 | onTransferFundsClick, 43 | showTransferFundsButton, 44 | } = this.props; 45 | console.log("accounts---->" + JSON.stringify(accounts)); 46 | console.log("account --->" + JSON.stringify(account)); 47 | const realAccount = accounts.items.find((acc) => { 48 | return acc.id === account.id; 49 | }); 50 | console.log("reaalaccount--->" + realAccount); 51 | 52 | return ( 53 |
54 |

55 | User Accounts 56 | {showTransferFundsButton && ( 57 | } 63 | onTouchTap={onTransferFundsClick} 64 | /> 65 | )} 66 |

67 |
68 | 73 |
74 | {/* 79 | 80 | */} 81 | {/* {showNewAccountForm && } */} 82 | {showTransferFunds && } 83 |
84 | ); 85 | } 86 | } 87 | 88 | const mapStateToProps = (state) => { 89 | const { accounts, login } = state; 90 | return { 91 | accounts: accounts, 92 | isFetching: accounts.isFetching, 93 | account: accounts.item, 94 | authenticated: login.authenticated, 95 | showNewAccountForm: accounts.showNewAccountForm, 96 | showTransferFunds: accounts.showTransferFunds, 97 | showTransferFundsButton: accounts.showTransferFundsButton, 98 | }; 99 | }; 100 | 101 | const mapDispatchToProps = (dispatch) => { 102 | return { 103 | onAddAccountClick: () => { 104 | dispatch(showNewAccountForm()); 105 | }, 106 | onTransferFundsClick: () => { 107 | dispatch(showTransferFunds()); 108 | }, 109 | dispatch, 110 | }; 111 | }; 112 | 113 | export default connect(mapStateToProps, mapDispatchToProps)(UserAccountsPage); 114 | -------------------------------------------------------------------------------- /frontend/src/containers/WelcomePage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | // import { browserHistory } from "react-router"; 4 | // import FlatButton from "material-ui/FlatButton"; 5 | import Login from "./Login"; 6 | 7 | class WelcomePage extends Component { 8 | // goToAccounts() { 9 | // browserHistory.push("/accounts"); 10 | // } 11 | render() { 12 | const { authenticated } = this.props; 13 | return ( 14 |
15 |

Welcome to Dev Stack Bank

16 | 17 | {!authenticated && } 18 | {/* {authenticated && ( 19 | this.goToAccounts()} 21 | label="View Accounts" 22 | primary={true} 23 | /> 24 | )} */} 25 |
26 | ); 27 | } 28 | } 29 | 30 | const mapStateToProps = (state) => { 31 | const { login } = state; 32 | return { 33 | authenticated: login.authenticated, 34 | }; 35 | }; 36 | 37 | export default connect(mapStateToProps)(WelcomePage); 38 | -------------------------------------------------------------------------------- /frontend/src/containers/app.css: -------------------------------------------------------------------------------- 1 | .app { 2 | font-family: sans-serif; 3 | width: 60%; 4 | margin-left: auto; 5 | margin-right: auto; 6 | padding: 0; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/formatMoney.js: -------------------------------------------------------------------------------- 1 | const formatMoney = (amount) => { 2 | let formatter = new Intl.NumberFormat("en-GB", { 3 | style: "currency", 4 | currency: "INR", 5 | minimumFractionDigits: 2, 6 | }); 7 | 8 | let formattedValue = formatter.format(amount); 9 | 10 | return formattedValue; 11 | }; 12 | 13 | export default formatMoney; 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import { browserHistory } from "react-router"; 4 | import { syncHistoryWithStore } from "react-router-redux"; 5 | import injectTapEventPlugin from "react-tap-event-plugin"; 6 | import MuiThemeProvider from "material-ui/styles/MuiThemeProvider"; 7 | import Root from "./containers/Root"; 8 | import configureStore from "./store/configureStore"; 9 | 10 | injectTapEventPlugin(); 11 | 12 | const store = configureStore(); 13 | const history = syncHistoryWithStore(browserHistory, store); 14 | 15 | render( 16 | 17 | 18 | , 19 | document.getElementById("root") 20 | ); 21 | -------------------------------------------------------------------------------- /frontend/src/middleware/api.js: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-fetch"; 2 | 3 | const BASE_URL = process.env.REACT_APP_BASE_URL; 4 | 5 | const apiResponse = (response) => { 6 | return response.json().then((json) => { 7 | if (!response.ok) { 8 | return Promise.reject(json); 9 | } 10 | return json; 11 | }); 12 | }; 13 | 14 | const fetchOptions = (method, data) => { 15 | return { 16 | method, 17 | body: JSON.stringify(data), 18 | headers: new Headers({ 19 | "Content-Type": "application/json; charset=utf-8", 20 | }), 21 | }; 22 | }; 23 | 24 | const callApi = (endpoint, method, data) => { 25 | const fullUrl = 26 | endpoint.indexOf(BASE_URL) === -1 ? BASE_URL + endpoint : endpoint; 27 | if (method) { 28 | method = method.toUpperCase(); 29 | return fetch(fullUrl, fetchOptions(method, data)).then(apiResponse); 30 | } else { 31 | return fetch(fullUrl).then(apiResponse); 32 | } 33 | }; 34 | 35 | export const CALL_API = "Call API"; 36 | 37 | export default (store) => (next) => (action) => { 38 | const callAPI = action[CALL_API]; 39 | 40 | if (typeof callAPI === "undefined") { 41 | return next(action); 42 | } 43 | 44 | const { endpoint, types, data, method } = callAPI; 45 | 46 | if (typeof endpoint !== "string") { 47 | throw new Error("Specify a string endpoint URL."); 48 | } 49 | 50 | if (!Array.isArray(types) || types.length !== 3) { 51 | throw new Error("Expected an array of three action types."); 52 | } 53 | 54 | if (!types.every((type) => typeof type === "string")) { 55 | throw new Error("Expected action types to be strings."); 56 | } 57 | 58 | const actionWith = (data) => { 59 | const finalAction = Object.assign({}, action, data); 60 | delete finalAction[CALL_API]; 61 | return finalAction; 62 | }; 63 | 64 | const [requestType, successType, failureType] = types; 65 | next(actionWith({ type: requestType })); 66 | 67 | return callApi(endpoint, method, data).then( 68 | (response) => 69 | next( 70 | actionWith({ 71 | response, 72 | type: successType, 73 | }) 74 | ), 75 | (error) => 76 | next( 77 | actionWith({ 78 | type: failureType, 79 | error: error.message || "Something bad happened", 80 | }) 81 | ) 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /frontend/src/reducers/accounts.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from "../actions/constants"; 2 | 3 | const accounts = ( 4 | state = { 5 | isFetching: false, 6 | showTransferFundsButton: false, 7 | item: {}, 8 | items: [], 9 | }, 10 | action 11 | ) => { 12 | switch (action.type) { 13 | case ActionTypes.REQUEST_ACCOUNTBYID: 14 | return Object.assign({}, state, { 15 | item: state.items.find((account) => account.id == action.id), 16 | }); 17 | 18 | case ActionTypes.REQUEST_ACCOUNTS: 19 | return Object.assign({}, state, { 20 | isFetching: true, 21 | }); 22 | case ActionTypes.RECEIVE_ACCOUNTS: 23 | return Object.assign({}, state, { 24 | isFetching: false, 25 | items: action.response, 26 | showTransferFundsButton: 27 | Array.isArray(action.response) && action.response.length > 1, 28 | }); 29 | case ActionTypes.SHOW_NEW_ACCOUNTS_FORM: 30 | return Object.assign({}, state, { 31 | showNewAccountForm: true, 32 | }); 33 | case ActionTypes.HIDE_NEW_ACCOUNTS_FORM: 34 | return Object.assign({}, state, { 35 | showNewAccountForm: false, 36 | nameValidationMessage: null, 37 | openingBalanceValidationMessage: null, 38 | }); 39 | case ActionTypes.CREATE_ACCOUNT_VALIDATION_FAILURE: 40 | return Object.assign({}, state, { 41 | nameValidationMessage: action.validationResult.nameValidationMessage, 42 | openingBalanceValidationMessage: 43 | action.validationResult.openingBalanceValidationMessage, 44 | }); 45 | case ActionTypes.CREATE_ACCOUNT_SUCCESS: 46 | const items = [...state.items, action.response]; 47 | return Object.assign({}, state, { 48 | items, 49 | showNewAccountForm: false, 50 | showTransferFundsButton: items.length > 1, 51 | }); 52 | case ActionTypes.SHOW_TRANSFER_FUNDS: 53 | return Object.assign({}, state, { 54 | showTransferFunds: true, 55 | }); 56 | case ActionTypes.HIDE_TRANSFER_FUNDS: 57 | return Object.assign({}, state, { 58 | showTransferFunds: false, 59 | fromAccountValidationMessage: null, 60 | toAccountValidationMessage: null, 61 | transferAmountValidationMessage: null, 62 | }); 63 | case ActionTypes.TRANSFER_FUNDS_VALIDATION_FAILURE: 64 | return Object.assign({}, state, { 65 | fromAccountValidationMessage: 66 | action.validationResult.fromAccountValidationMessage, 67 | toAccountValidationMessage: 68 | action.validationResult.toAccountValidationMessage, 69 | transferAmountValidationMessage: 70 | action.validationResult.transferAmountValidationMessage, 71 | }); 72 | case ActionTypes.UPDATE_ACCOUNT_BALANCE_SUCCESS: 73 | let index = state.items.indexOf( 74 | state.items.find((a) => a.id === action.response.id) 75 | ); 76 | let account = Object.assign({}, state.items[index], { 77 | balance: action.response.balance, 78 | }); 79 | let result = Object.assign({}, state, { 80 | items: [ 81 | ...state.items.slice(0, index), 82 | account, 83 | ...state.items.slice(index + 1), 84 | ], 85 | }); 86 | return result; 87 | default: 88 | return state; 89 | } 90 | }; 91 | 92 | export default accounts; 93 | -------------------------------------------------------------------------------- /frontend/src/reducers/accounts.spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze' 2 | import accounts from './accounts' 3 | import * as actions from '../actions' 4 | 5 | describe('accounts reducer', () => { 6 | it('should have showTransferFundsButton set to false by default', () => { 7 | const defaultState = accounts(undefined, {}) 8 | 9 | const expectedState = { isFetching: false, showTransferFundsButton: false, items: [] } 10 | 11 | expect(defaultState).toEqual(expectedState) 12 | }) 13 | 14 | it('should handle REQUEST_ACCOUNTS', () => { 15 | const stateBefore = {} 16 | 17 | const expectedStateAfter = { 18 | isFetching: true 19 | } 20 | 21 | deepFreeze(stateBefore) 22 | 23 | const stateAfter = accounts(stateBefore, { type: 'REQUEST_ACCOUNTS'}) 24 | 25 | expect(stateAfter).toEqual(expectedStateAfter) 26 | }) 27 | 28 | describe('RECEIVE_ACCOUNTS', () => { 29 | it('should set showTransferFundsButton to false if there is less than 2 accounts', () => { 30 | const stateBefore = { 31 | isFetching: false 32 | } 33 | 34 | const items = [ 35 | { id: 1, name: 'Solo Account', balance: 123.45 } 36 | ] 37 | 38 | const expectedStateAfter = { 39 | isFetching: false, 40 | showTransferFundsButton: false, 41 | items 42 | } 43 | 44 | deepFreeze(stateBefore) 45 | 46 | const stateAfter = accounts(stateBefore, { 47 | type: 'RECEIVE_ACCOUNTS', 48 | response: items 49 | }) 50 | 51 | expect(stateAfter).toEqual(expectedStateAfter) 52 | }) 53 | 54 | it('should set showTransferFundsButton to true if there is more than 1 accounts', () => { 55 | const items = [ 56 | { id: 1, name: 'Solo Account', balance: 123.45 }, 57 | { id: 2, name: 'My Second Account', balance: 543.21 } 58 | ] 59 | 60 | const expectedStateAfter = { 61 | isFetching: false, 62 | showTransferFundsButton: true, 63 | items 64 | } 65 | 66 | const stateAfter = accounts(undefined, { 67 | type: 'RECEIVE_ACCOUNTS', 68 | response: items 69 | }) 70 | 71 | expect(stateAfter).toEqual(expectedStateAfter) 72 | }) 73 | }) 74 | 75 | it('should handle SHOW_NEW_ACCOUNTS_FORM', () => { 76 | const stateBefore = {} 77 | 78 | const expectedStateAfter = { 79 | showNewAccountForm: true 80 | } 81 | 82 | deepFreeze(stateBefore) 83 | 84 | const stateAfter = accounts(stateBefore, actions.showNewAccountForm()) 85 | 86 | expect(stateAfter).toEqual(expectedStateAfter) 87 | }) 88 | 89 | it('should handle HIDE_NEW_ACCOUNTS_FORM', () => { 90 | const stateBefore = { 91 | showNewAccountForm: true 92 | } 93 | 94 | const expectedStateAfter = { 95 | showNewAccountForm: false, 96 | nameValidationMessage: null, 97 | openingBalanceValidationMessage: null 98 | } 99 | 100 | deepFreeze(stateBefore) 101 | 102 | const stateAfter = accounts(stateBefore, actions.hideNewAccountForm()) 103 | 104 | expect(stateAfter).toEqual(expectedStateAfter) 105 | }) 106 | 107 | it('should handle CREATE_ACCOUNT_VALIDATION_FAILURE', () => { 108 | const stateBefore = {} 109 | 110 | const validationResult = { 111 | isValid: false, 112 | nameValidationMessage: 'Name validation message', 113 | openingBalanceValidationMessage: 'Opening Balance validation message' 114 | } 115 | 116 | const expectedStateAfter = { 117 | nameValidationMessage: 'Name validation message', 118 | openingBalanceValidationMessage: 'Opening Balance validation message' 119 | } 120 | 121 | deepFreeze(stateBefore) 122 | 123 | const stateAfter = accounts(stateBefore, actions.invalidCreateAccountRequest(validationResult)) 124 | 125 | expect(stateAfter).toEqual(expectedStateAfter) 126 | }) 127 | 128 | describe('CREATE_ACCOUNT_SUCCESS', () => { 129 | it('should showTransferFundsButton to true if there are multiple accounts', () => { 130 | const stateBefore = { 131 | items: [ 132 | { id: 1, name: 'My First Account', balance: 50.00 } 133 | ] 134 | } 135 | 136 | deepFreeze(stateBefore) 137 | 138 | const expectedStateAfter = { 139 | items: [ 140 | { id: 1, name: 'My First Account', balance: 50.00 }, 141 | { id: 2, name: 'My Second Account', balance: 100.00 } 142 | ], 143 | showNewAccountForm: false, 144 | showTransferFundsButton: true 145 | } 146 | 147 | const createAccountSuccess = { 148 | type: 'CREATE_ACCOUNT_SUCCESS', 149 | response: { 150 | id: 2, 151 | name: 'My Second Account', 152 | balance: 100.00 153 | } 154 | } 155 | 156 | const stateAfter = accounts(stateBefore, createAccountSuccess) 157 | 158 | expect(stateAfter).toEqual(expectedStateAfter) 159 | }) 160 | 161 | it('should showTransferFundsButton to false if there is only one account', () => { 162 | const stateBefore = { 163 | items: [] 164 | } 165 | 166 | deepFreeze(stateBefore) 167 | 168 | const expectedStateAfter = { 169 | items: [ 170 | { id: 1, name: 'My First Account', balance: 50.00 } 171 | ], 172 | showNewAccountForm: false, 173 | showTransferFundsButton: false 174 | } 175 | 176 | const createAccountSuccess = { 177 | type: 'CREATE_ACCOUNT_SUCCESS', 178 | response: { 179 | id: 1, 180 | name: 'My First Account', 181 | balance: 50.00 182 | } 183 | } 184 | 185 | const stateAfter = accounts(stateBefore, createAccountSuccess) 186 | 187 | expect(stateAfter).toEqual(expectedStateAfter) 188 | }) 189 | }) 190 | 191 | it('handles SHOW_TRANSFER_FUNDS', () => { 192 | const stateBefore = {} 193 | 194 | deepFreeze(stateBefore) 195 | 196 | const expectedStateAfter = { 197 | showTransferFunds: true 198 | } 199 | 200 | const stateAfter = accounts(stateBefore, actions.showTransferFunds()) 201 | 202 | expect(stateAfter).toEqual(expectedStateAfter) 203 | }) 204 | 205 | it('handles HIDE_TRANSFER_FUNDS', () => { 206 | const stateBefore = {} 207 | 208 | deepFreeze(stateBefore) 209 | 210 | const expectedStateAfter = { 211 | showTransferFunds: false, 212 | fromAccountValidationMessage: null, 213 | toAccountValidationMessage: null, 214 | transferAmountValidationMessage: null 215 | } 216 | 217 | const stateAfter = accounts(stateBefore, actions.hideTransferFunds()) 218 | 219 | expect(stateAfter).toEqual(expectedStateAfter) 220 | }) 221 | 222 | it('handles TRANSFER_FUNDS_VALIDATION_FAILURE', () => { 223 | const stateBefore = { 224 | showTransferFunds: true 225 | } 226 | 227 | deepFreeze(stateBefore) 228 | 229 | const validationResult = { 230 | isValid: false, 231 | fromAccountValidationMessage: 'foo', 232 | toAccountValidationMessage: 'bar', 233 | transferAmountValidationMessage: 'baz' 234 | } 235 | 236 | const expectedStateAfter = { 237 | showTransferFunds: true, 238 | fromAccountValidationMessage: 'foo', 239 | toAccountValidationMessage: 'bar', 240 | transferAmountValidationMessage: 'baz' 241 | } 242 | 243 | const stateAfter = accounts(stateBefore, actions.invalidTransferFundsRequest(validationResult)) 244 | 245 | expect(stateAfter).toEqual(expectedStateAfter) 246 | }) 247 | 248 | it('handles UPDATE_ACCOUNT_BALANCE_SUCCESS', () => { 249 | const stateBefore = { 250 | items: [ 251 | { id: 1, name: 'First Account', balance: 100.00 }, 252 | { id: 2, name: 'Second Account', balance: 200.00 }, 253 | { id: 3, name: 'Third Account', balance: 300.00 } 254 | ] 255 | } 256 | 257 | deepFreeze(stateBefore) 258 | 259 | const expectedStateAfter = { 260 | items: [ 261 | { id: 1, name: 'First Account', balance: 100.00 }, 262 | { id: 2, name: 'Second Account', balance: 250.00 }, 263 | { id: 3, name: 'Third Account', balance: 300.00 } 264 | ] 265 | } 266 | 267 | const action = { 268 | type: 'UPDATE_ACCOUNT_BALANCE_SUCCESS', 269 | response: { 270 | id: 2, 271 | name: 'Second Account', 272 | balance: 250.00 273 | } 274 | } 275 | 276 | const stateAfter = accounts(stateBefore, action) 277 | 278 | expect(stateAfter).toEqual(expectedStateAfter) 279 | }) 280 | 281 | it('handles unknown actions', () => { 282 | const stateBefore = { 283 | items: [ 284 | { id: 1, name: 'First Account', balance: 100.00 }, 285 | { id: 2, name: 'Second Account', balance: 250.00 }, 286 | { id: 3, name: 'Third Account', balance: 300.00 } 287 | ] 288 | } 289 | 290 | const expectedStateAfter = { 291 | items: [ 292 | { id: 1, name: 'First Account', balance: 100.00 }, 293 | { id: 2, name: 'Second Account', balance: 250.00 }, 294 | { id: 3, name: 'Third Account', balance: 300.00 } 295 | ] 296 | } 297 | 298 | deepFreeze(stateBefore) 299 | 300 | const stateAfter = accounts(stateBefore, {type: 'BOGUS_ACTION'}) 301 | 302 | expect(stateAfter).toEqual(expectedStateAfter) 303 | }) 304 | }) -------------------------------------------------------------------------------- /frontend/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as routing } from 'react-router-redux' 3 | import * as ActionTypes from '../actions/constants' 4 | import login from './login' 5 | import accounts from './accounts' 6 | import transactions from './transactions' 7 | 8 | const errorMessage = (state = null, action) => { 9 | const { type, error } = action 10 | if (type === ActionTypes.RESET_ERROR_MESSAGE) { 11 | return null 12 | } else if (error) { 13 | return error 14 | } 15 | return state 16 | } 17 | 18 | const rootReducer = combineReducers({ 19 | errorMessage, 20 | routing, 21 | login, 22 | accounts, 23 | transactions 24 | }) 25 | 26 | export default rootReducer -------------------------------------------------------------------------------- /frontend/src/reducers/login.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/constants' 2 | 3 | const login = (state = { 4 | authenticated: false 5 | }, action) => { 6 | switch (action.type) { 7 | case ActionTypes.REQUEST_LOGIN: 8 | return state; 9 | case ActionTypes.REQUEST_LOGIN_SUCCESS: 10 | return Object.assign({}, state, { 11 | authenticated: true, 12 | usernameValidationMessage: null, 13 | passwordValidationMessage: null 14 | }) 15 | case ActionTypes.REQUEST_LOGOUT_SUCCESS: 16 | return Object.assign({}, state, { 17 | authenticated: false, 18 | usernameValidationMessage: null, 19 | passwordValidationMessage: null 20 | }) 21 | case ActionTypes.REQUEST_LOGIN_FAILURE: 22 | return Object.assign({}, state, { 23 | authenticated: false, 24 | usernameValidationMessage: action.validationResult.usernameValidationMessage, 25 | passwordValidationMessage: action.validationResult.passwordValidationMessage 26 | }) 27 | default: 28 | return state 29 | } 30 | } 31 | 32 | export default login -------------------------------------------------------------------------------- /frontend/src/reducers/login.spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze' 2 | import login from './login' 3 | import * as actions from '../actions' 4 | 5 | describe('login reducer', () => { 6 | it('should handle REQUEST_LOGIN_SUCCESS', () => { 7 | const stateBefore = { 8 | authenticated: false 9 | } 10 | 11 | const expectedStateAfter = { 12 | authenticated: true, 13 | usernameValidationMessage: null, 14 | passwordValidationMessage: null 15 | } 16 | 17 | deepFreeze(stateBefore) 18 | 19 | const stateAfter = login(stateBefore, actions.loginSuccessful()) 20 | 21 | expect(stateAfter).toEqual(expectedStateAfter) 22 | }) 23 | 24 | it('should handle REQUEST_LOGOUT_SUCCESS', () => { 25 | const stateBefore = { 26 | authenticated: true 27 | } 28 | 29 | const expectedStateAfter = { 30 | authenticated: false, 31 | usernameValidationMessage: null, 32 | passwordValidationMessage: null 33 | } 34 | 35 | deepFreeze(stateBefore) 36 | 37 | const stateAfter = login(stateBefore, actions.logoutSuccessful()) 38 | 39 | expect(stateAfter).toEqual(expectedStateAfter) 40 | }) 41 | 42 | it('should handle REQUEST_LOGIN_FAILURE', () => { 43 | const stateBefore = { 44 | authenticated: true 45 | } 46 | 47 | const expectedStateAfter = { 48 | authenticated: false, 49 | usernameValidationMessage: 'Foo', 50 | passwordValidationMessage: 'Bar' 51 | } 52 | 53 | deepFreeze(stateBefore) 54 | 55 | const validationResult = { 56 | isValid: false, 57 | usernameValidationMessage: 'Foo', 58 | passwordValidationMessage: 'Bar' 59 | } 60 | const stateAfter = login(stateBefore, actions.loginFailed(validationResult)) 61 | 62 | expect(stateAfter).toEqual(expectedStateAfter) 63 | }) 64 | 65 | it('handles unknown actions', () => { 66 | const stateBefore = { 67 | authenticated: true 68 | } 69 | 70 | const expectedStateAfter = { 71 | authenticated: true 72 | } 73 | 74 | deepFreeze(stateBefore) 75 | 76 | const stateAfter = login(stateBefore, {type: 'BOGUS_ACTION'}) 77 | 78 | expect(stateAfter).toEqual(expectedStateAfter) 79 | }) 80 | }) -------------------------------------------------------------------------------- /frontend/src/reducers/transactions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/constants' 2 | const transactions = (state = { 3 | isFetching: false, 4 | items: [] 5 | }, action) => { 6 | switch (action.type) { 7 | case ActionTypes.REQUEST_TRANSACTIONS: 8 | return Object.assign({}, state, { 9 | isFetching: true 10 | }) 11 | case ActionTypes.RECEIVE_TRANSACTIONS: 12 | return Object.assign({}, state, { 13 | isFetching: false, 14 | items: action.response 15 | }) 16 | default: 17 | return state 18 | } 19 | } 20 | export default transactions -------------------------------------------------------------------------------- /frontend/src/reducers/transactions.spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze' 2 | import transactions from './transactions' 3 | 4 | describe('transactions reducer', () => { 5 | it('handles REQUEST_TRANSACTIONS', () => { 6 | const stateBefore = { 7 | isFetching: false, 8 | items: [] 9 | } 10 | 11 | const expectedStateAfter = { 12 | isFetching: true, 13 | items: [] 14 | } 15 | 16 | deepFreeze(stateBefore) 17 | 18 | const stateAfter = transactions(stateBefore, { type: 'REQUEST_TRANSACTIONS', accountId: 1 }) 19 | 20 | expect(stateAfter).toEqual(expectedStateAfter) 21 | }) 22 | 23 | it('handles RECEIVE_TRANSACTIONS', () => { 24 | const stateBefore = { 25 | isFetching: true, 26 | items: [] 27 | } 28 | 29 | const items = [ 30 | { id: 1, date: new Date(), description: 'My transaction description', debit: 100.00, credit: null, accountId: 1 } 31 | ] 32 | 33 | deepFreeze(stateBefore) 34 | 35 | const expectedStateAfter = { 36 | isFetching: false, 37 | items 38 | } 39 | 40 | const action = { 41 | type: 'RECEIVE_TRANSACTIONS', 42 | response: items 43 | } 44 | 45 | const stateAfter = transactions(stateBefore, action) 46 | 47 | expect(stateAfter).toEqual(expectedStateAfter) 48 | }) 49 | 50 | it('handles unknown actions', () => { 51 | const stateBefore = { 52 | items: [] 53 | } 54 | 55 | const expectedStateAfter = { 56 | items: [] 57 | } 58 | 59 | deepFreeze(stateBefore) 60 | 61 | const stateAfter = transactions(stateBefore, {type: 'BOGUS_ACTION'}) 62 | 63 | expect(stateAfter).toEqual(expectedStateAfter) 64 | }) 65 | }) -------------------------------------------------------------------------------- /frontend/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, IndexRoute } from "react-router"; 3 | import App from "./containers/App"; 4 | import WelcomePage from "./containers/WelcomePage"; 5 | import AccountsPage from "./containers/AccountsPage"; 6 | import UserAccountsPage from "./containers/UserAccountPage"; 7 | import TransactionsPage from "./containers/TransactionsPage"; 8 | 9 | export default ( 10 | 11 | 12 | 13 | 14 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /frontend/src/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import createLogger from 'redux-logger' 4 | import rootReducer from '../reducers' 5 | import DevTools from '../containers/DevTools' 6 | import api from '../middleware/api' 7 | 8 | const configureStore = preloadedState => { 9 | const store = createStore( 10 | rootReducer, 11 | preloadedState, 12 | compose( 13 | applyMiddleware(thunk, api, createLogger()), 14 | DevTools.instrument() 15 | ) 16 | ) 17 | 18 | return store 19 | } 20 | 21 | export default configureStore 22 | -------------------------------------------------------------------------------- /frontend/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod') 3 | } else { 4 | module.exports = require('./configureStore.dev') 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import rootReducer from '../reducers' 4 | import api from '../middleware/api' 5 | 6 | const configureStore = preloadedState => createStore( 7 | rootReducer, 8 | preloadedState, 9 | applyMiddleware(thunk, api) 10 | ) 11 | 12 | export default configureStore 13 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-bank", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-styles": { 8 | "version": "3.2.1", 9 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 10 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 11 | "dev": true, 12 | "requires": { 13 | "color-convert": "^1.9.0" 14 | } 15 | }, 16 | "axios": { 17 | "version": "0.19.2", 18 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", 19 | "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", 20 | "requires": { 21 | "follow-redirects": "1.5.10" 22 | } 23 | }, 24 | "body-parser": { 25 | "version": "1.19.0", 26 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 27 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 28 | "requires": { 29 | "bytes": "3.1.0", 30 | "content-type": "~1.0.4", 31 | "debug": "2.6.9", 32 | "depd": "~1.1.2", 33 | "http-errors": "1.7.2", 34 | "iconv-lite": "0.4.24", 35 | "on-finished": "~2.3.0", 36 | "qs": "6.7.0", 37 | "raw-body": "2.4.0", 38 | "type-is": "~1.6.17" 39 | } 40 | }, 41 | "bytes": { 42 | "version": "3.1.0", 43 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 44 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 45 | }, 46 | "chalk": { 47 | "version": "2.4.2", 48 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 49 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 50 | "dev": true, 51 | "requires": { 52 | "ansi-styles": "^3.2.1", 53 | "escape-string-regexp": "^1.0.5", 54 | "supports-color": "^5.3.0" 55 | }, 56 | "dependencies": { 57 | "supports-color": { 58 | "version": "5.5.0", 59 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 60 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 61 | "dev": true, 62 | "requires": { 63 | "has-flag": "^3.0.0" 64 | } 65 | } 66 | } 67 | }, 68 | "color-convert": { 69 | "version": "1.9.3", 70 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 71 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 72 | "dev": true, 73 | "requires": { 74 | "color-name": "1.1.3" 75 | } 76 | }, 77 | "color-name": { 78 | "version": "1.1.3", 79 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 80 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 81 | "dev": true 82 | }, 83 | "commander": { 84 | "version": "2.6.0", 85 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", 86 | "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", 87 | "dev": true 88 | }, 89 | "concurrently": { 90 | "version": "3.6.1", 91 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.6.1.tgz", 92 | "integrity": "sha512-/+ugz+gwFSEfTGUxn0KHkY+19XPRTXR8+7oUK/HxgiN1n7FjeJmkrbSiXAJfyQ0zORgJYPaenmymwon51YXH9Q==", 93 | "dev": true, 94 | "requires": { 95 | "chalk": "^2.4.1", 96 | "commander": "2.6.0", 97 | "date-fns": "^1.23.0", 98 | "lodash": "^4.5.1", 99 | "read-pkg": "^3.0.0", 100 | "rx": "2.3.24", 101 | "spawn-command": "^0.0.2-1", 102 | "supports-color": "^3.2.3", 103 | "tree-kill": "^1.1.0" 104 | } 105 | }, 106 | "content-type": { 107 | "version": "1.0.4", 108 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 109 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 110 | }, 111 | "cors": { 112 | "version": "2.8.5", 113 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 114 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 115 | "requires": { 116 | "object-assign": "^4", 117 | "vary": "^1" 118 | } 119 | }, 120 | "date-fns": { 121 | "version": "1.30.1", 122 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", 123 | "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", 124 | "dev": true 125 | }, 126 | "debug": { 127 | "version": "2.6.9", 128 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 129 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 130 | "requires": { 131 | "ms": "2.0.0" 132 | } 133 | }, 134 | "depd": { 135 | "version": "1.1.2", 136 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 137 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 138 | }, 139 | "ee-first": { 140 | "version": "1.1.1", 141 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 142 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 143 | }, 144 | "error-ex": { 145 | "version": "1.3.2", 146 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 147 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 148 | "dev": true, 149 | "requires": { 150 | "is-arrayish": "^0.2.1" 151 | } 152 | }, 153 | "escape-string-regexp": { 154 | "version": "1.0.5", 155 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 156 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 157 | "dev": true 158 | }, 159 | "follow-redirects": { 160 | "version": "1.5.10", 161 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 162 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 163 | "requires": { 164 | "debug": "=3.1.0" 165 | }, 166 | "dependencies": { 167 | "debug": { 168 | "version": "3.1.0", 169 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 170 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 171 | "requires": { 172 | "ms": "2.0.0" 173 | } 174 | } 175 | } 176 | }, 177 | "graceful-fs": { 178 | "version": "4.2.4", 179 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", 180 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", 181 | "dev": true 182 | }, 183 | "has-flag": { 184 | "version": "3.0.0", 185 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 186 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 187 | "dev": true 188 | }, 189 | "hosted-git-info": { 190 | "version": "2.8.8", 191 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", 192 | "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", 193 | "dev": true 194 | }, 195 | "http-errors": { 196 | "version": "1.7.2", 197 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 198 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 199 | "requires": { 200 | "depd": "~1.1.2", 201 | "inherits": "2.0.3", 202 | "setprototypeof": "1.1.1", 203 | "statuses": ">= 1.5.0 < 2", 204 | "toidentifier": "1.0.0" 205 | } 206 | }, 207 | "iconv-lite": { 208 | "version": "0.4.24", 209 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 210 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 211 | "requires": { 212 | "safer-buffer": ">= 2.1.2 < 3" 213 | } 214 | }, 215 | "inherits": { 216 | "version": "2.0.3", 217 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 218 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 219 | }, 220 | "is-arrayish": { 221 | "version": "0.2.1", 222 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 223 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 224 | "dev": true 225 | }, 226 | "json-parse-better-errors": { 227 | "version": "1.0.2", 228 | "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", 229 | "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", 230 | "dev": true 231 | }, 232 | "load-json-file": { 233 | "version": "4.0.0", 234 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", 235 | "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", 236 | "dev": true, 237 | "requires": { 238 | "graceful-fs": "^4.1.2", 239 | "parse-json": "^4.0.0", 240 | "pify": "^3.0.0", 241 | "strip-bom": "^3.0.0" 242 | } 243 | }, 244 | "lodash": { 245 | "version": "4.17.15", 246 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 247 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", 248 | "dev": true 249 | }, 250 | "media-typer": { 251 | "version": "0.3.0", 252 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 253 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 254 | }, 255 | "mime-db": { 256 | "version": "1.44.0", 257 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 258 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 259 | }, 260 | "mime-types": { 261 | "version": "2.1.27", 262 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 263 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 264 | "requires": { 265 | "mime-db": "1.44.0" 266 | } 267 | }, 268 | "ms": { 269 | "version": "2.0.0", 270 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 271 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 272 | }, 273 | "normalize-package-data": { 274 | "version": "2.5.0", 275 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 276 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 277 | "dev": true, 278 | "requires": { 279 | "hosted-git-info": "^2.1.4", 280 | "resolve": "^1.10.0", 281 | "semver": "2 || 3 || 4 || 5", 282 | "validate-npm-package-license": "^3.0.1" 283 | } 284 | }, 285 | "object-assign": { 286 | "version": "4.1.1", 287 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 288 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 289 | }, 290 | "on-finished": { 291 | "version": "2.3.0", 292 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 293 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 294 | "requires": { 295 | "ee-first": "1.1.1" 296 | } 297 | }, 298 | "parse-json": { 299 | "version": "4.0.0", 300 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", 301 | "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", 302 | "dev": true, 303 | "requires": { 304 | "error-ex": "^1.3.1", 305 | "json-parse-better-errors": "^1.0.1" 306 | } 307 | }, 308 | "path-parse": { 309 | "version": "1.0.6", 310 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 311 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 312 | "dev": true 313 | }, 314 | "path-type": { 315 | "version": "3.0.0", 316 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", 317 | "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", 318 | "dev": true, 319 | "requires": { 320 | "pify": "^3.0.0" 321 | } 322 | }, 323 | "pify": { 324 | "version": "3.0.0", 325 | "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", 326 | "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", 327 | "dev": true 328 | }, 329 | "qs": { 330 | "version": "6.7.0", 331 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 332 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 333 | }, 334 | "raw-body": { 335 | "version": "2.4.0", 336 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 337 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 338 | "requires": { 339 | "bytes": "3.1.0", 340 | "http-errors": "1.7.2", 341 | "iconv-lite": "0.4.24", 342 | "unpipe": "1.0.0" 343 | } 344 | }, 345 | "read-pkg": { 346 | "version": "3.0.0", 347 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", 348 | "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", 349 | "dev": true, 350 | "requires": { 351 | "load-json-file": "^4.0.0", 352 | "normalize-package-data": "^2.3.2", 353 | "path-type": "^3.0.0" 354 | } 355 | }, 356 | "resolve": { 357 | "version": "1.17.0", 358 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 359 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 360 | "dev": true, 361 | "requires": { 362 | "path-parse": "^1.0.6" 363 | } 364 | }, 365 | "rx": { 366 | "version": "2.3.24", 367 | "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", 368 | "integrity": "sha1-FPlQpCF9fjXapxu8vljv9o6ksrc=", 369 | "dev": true 370 | }, 371 | "safer-buffer": { 372 | "version": "2.1.2", 373 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 374 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 375 | }, 376 | "semver": { 377 | "version": "5.7.1", 378 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 379 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 380 | "dev": true 381 | }, 382 | "setprototypeof": { 383 | "version": "1.1.1", 384 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 385 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 386 | }, 387 | "spawn-command": { 388 | "version": "0.0.2-1", 389 | "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", 390 | "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", 391 | "dev": true 392 | }, 393 | "spdx-correct": { 394 | "version": "3.1.0", 395 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", 396 | "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", 397 | "dev": true, 398 | "requires": { 399 | "spdx-expression-parse": "^3.0.0", 400 | "spdx-license-ids": "^3.0.0" 401 | } 402 | }, 403 | "spdx-exceptions": { 404 | "version": "2.3.0", 405 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", 406 | "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", 407 | "dev": true 408 | }, 409 | "spdx-expression-parse": { 410 | "version": "3.0.0", 411 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", 412 | "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", 413 | "dev": true, 414 | "requires": { 415 | "spdx-exceptions": "^2.1.0", 416 | "spdx-license-ids": "^3.0.0" 417 | } 418 | }, 419 | "spdx-license-ids": { 420 | "version": "3.0.5", 421 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", 422 | "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", 423 | "dev": true 424 | }, 425 | "statuses": { 426 | "version": "1.5.0", 427 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 428 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 429 | }, 430 | "strip-bom": { 431 | "version": "3.0.0", 432 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 433 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 434 | "dev": true 435 | }, 436 | "supports-color": { 437 | "version": "3.2.3", 438 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", 439 | "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", 440 | "dev": true, 441 | "requires": { 442 | "has-flag": "^1.0.0" 443 | }, 444 | "dependencies": { 445 | "has-flag": { 446 | "version": "1.0.0", 447 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", 448 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", 449 | "dev": true 450 | } 451 | } 452 | }, 453 | "toidentifier": { 454 | "version": "1.0.0", 455 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 456 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 457 | }, 458 | "tree-kill": { 459 | "version": "1.2.2", 460 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 461 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 462 | "dev": true 463 | }, 464 | "type-is": { 465 | "version": "1.6.18", 466 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 467 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 468 | "requires": { 469 | "media-typer": "0.3.0", 470 | "mime-types": "~2.1.24" 471 | } 472 | }, 473 | "unpipe": { 474 | "version": "1.0.0", 475 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 476 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 477 | }, 478 | "validate-npm-package-license": { 479 | "version": "3.0.4", 480 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 481 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 482 | "dev": true, 483 | "requires": { 484 | "spdx-correct": "^3.0.0", 485 | "spdx-expression-parse": "^3.0.0" 486 | } 487 | }, 488 | "vary": { 489 | "version": "1.1.2", 490 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 491 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 492 | } 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-bank", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "setup": "./setup.sh", 6 | "start": "concurrently \"npm run frontend\" \"npm run backend\"", 7 | "frontend": "cd frontend && npm start", 8 | "backend": "cd backend && npm run dev", 9 | "test": "cd ui && npm test" 10 | }, 11 | "author": "Mark Pritchett", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "concurrently": "^3.1.0" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.19.2", 18 | "body-parser": "^1.19.0", 19 | "cors": "^2.8.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install 4 | cd ui 5 | npm install 6 | cd .. 7 | cd api 8 | npm install --------------------------------------------------------------------------------