From eecc8ce3df23ff50429c61b010228fad0ef37387 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 00:05:11 -0400 Subject: [PATCH 01/63] ignore lock files --- .gitignore | 2 + package-lock.json | 2056 --------------------------------------------- 2 files changed, 2 insertions(+), 2056 deletions(-) delete mode 100755 package-lock.json diff --git a/.gitignore b/.gitignore index d8223fc..a86b657 100755 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +package-lock.json +yarn.lock # Typescript v1 declaration files typings/ diff --git a/package-lock.json b/package-lock.json deleted file mode 100755 index b888a7c..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2056 +0,0 @@ -{ - "name": "week-10", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", - "dev": true, - "requires": { - "string-width": "^3.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "archiver": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.0.2.tgz", - "integrity": "sha512-Tq3yV/T4wxBsD2Wign8W9VQKhaUxzzRmjEiSoOK0SLqPgDP/N1TKdYyBeIEu56T4I9iO4fKTTR0mN9NWkBA0sg==", - "requires": { - "archiver-utils": "^2.1.0", - "async": "^3.2.0", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.0.0", - "tar-stream": "^2.1.4", - "zip-stream": "^4.0.0" - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true - }, - "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - } - } - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - } - }, - "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "dev": true, - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "bson": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", - "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - } - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "cli-boxes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", - "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==", - "dev": true - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "compress-commons": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.1.tgz", - "integrity": "sha512-xZm9o6iikekkI0GnXCmAl3LQGZj5TBDj0zLowsqi7tJtEa3FMGSEcHcqrSJIrOAk1UG/NBbDn/F1q+MG/p/EsA==", - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-security-policy-builder": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz", - "integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.0.tgz", - "integrity": "sha512-tyMw2IeUX6t9jhgXI6um0eKfWq4EIDpfv5m7GX4Jzp7eVelQ360xd8EPXJhp2mHwLQIkqlnMLjzqSZI3a+0wRw==", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^3.4.0" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true - }, - "dasherize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", - "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "dns-prefetch-control": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz", - "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI=" - }, - "dont-sniff-mimetype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz", - "integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=" - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "env-cmd": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", - "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", - "requires": { - "commander": "^4.0.0", - "cross-spawn": "^7.0.0" - } - }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "expect-ct": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.1.1.tgz", - "integrity": "sha512-ngXzTfoRGG7fYens3/RMb6yYoVLvLMfmsSllP/mZPxNHgFq41TmPSLF/nLY7fwoclI2vElvAmILFWGUYqdjfCg==" - }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "feature-policy": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.2.0.tgz", - "integrity": "sha512-2hGrlv6efG4hscYVZeaYjpzpT6I2OZgYqE2yDUzeAcKj2D1SH0AsEzqJNXzdoglEddcIXQQYop3lD97XpG75Jw==" - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "frameguard": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.0.0.tgz", - "integrity": "sha1-e8rUae57lukdEs6zlZx4I1qScuk=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "git-config-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-1.0.1.tgz", - "integrity": "sha1-bTP37WPbDQ4RgTFQO6s6ykfVRmQ=", - "requires": { - "extend-shallow": "^2.0.1", - "fs-exists-sync": "^0.1.0", - "homedir-polyfill": "^1.0.0" - } - }, - "git-user-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-user-name/-/git-user-name-2.0.0.tgz", - "integrity": "sha512-1DC8rUNm2I5V9v4eIpK6PSjKCp9bI0t6Wl05WSk+xEMS8GhR8GWzxM3aGZfPrfuqEfWxSbui5/pQJryJFXqCzQ==", - "requires": { - "extend-shallow": "^2.0.1", - "git-config-path": "^1.0.1", - "parse-git-config": "^1.1.1" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", - "dev": true, - "requires": { - "ini": "^1.3.5" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true - }, - "helmet": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.15.1.tgz", - "integrity": "sha512-hgoNe/sjKlKNvJ3g9Gz149H14BjMMWOCmW/DTXl7IfyKGtIK37GePwZrHNfr4aPXdKVyXcTj26RgRFbPKDy9lw==", - "requires": { - "depd": "2.0.0", - "dns-prefetch-control": "0.1.0", - "dont-sniff-mimetype": "1.0.0", - "expect-ct": "0.1.1", - "feature-policy": "0.2.0", - "frameguard": "3.0.0", - "helmet-crossdomain": "0.3.0", - "helmet-csp": "2.7.1", - "hide-powered-by": "1.0.0", - "hpkp": "2.0.0", - "hsts": "2.1.0", - "ienoopen": "1.0.0", - "nocache": "2.0.0", - "referrer-policy": "1.1.0", - "x-xss-protection": "1.1.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, - "helmet-crossdomain": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.3.0.tgz", - "integrity": "sha512-YiXhj0E35nC4Na5EPE4mTfoXMf9JTGpN4OtB4aLqShKuH9d2HNaJX5MQoglO6STVka0uMsHyG5lCut5Kzsy7Lg==" - }, - "helmet-csp": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.1.tgz", - "integrity": "sha512-sCHwywg4daQ2mY0YYwXSZRsgcCeerUwxMwNixGA7aMLkVmPTYBl7gJoZDHOZyXkqPrtuDT3s2B1A+RLI7WxSdQ==", - "requires": { - "camelize": "1.0.0", - "content-security-policy-builder": "2.0.0", - "dasherize": "2.0.0", - "platform": "1.3.5" - } - }, - "hide-powered-by": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.0.0.tgz", - "integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=" - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hpkp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", - "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" - }, - "hsts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.1.0.tgz", - "integrity": "sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA==" - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ienoopen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.0.0.tgz", - "integrity": "sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms=" - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dev": true, - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true - }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "requires": { - "package-json": "^6.3.0" - } - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" - }, - "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", - "requires": { - "mime-db": "~1.38.0" - } - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mongodb": { - "version": "3.1.13", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", - "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==", - "requires": { - "mongodb-core": "3.1.11", - "safe-buffer": "^5.1.2" - } - }, - "mongodb-core": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz", - "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==", - "requires": { - "bson": "^1.1.0", - "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" - } - }, - "morgan": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", - "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", - "requires": { - "basic-auth": "~2.0.0", - "debug": "2.6.9", - "depd": "~1.1.2", - "on-finished": "~2.3.0", - "on-headers": "~1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "nocache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", - "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" - }, - "nodemon": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", - "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==", - "dev": true, - "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^4.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true - }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "parse-git-config": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-1.1.1.tgz", - "integrity": "sha1-06mYQxcTL1c5hxK7pDjhKVkN34w=", - "requires": { - "extend-shallow": "^2.0.1", - "fs-exists-sync": "^0.1.0", - "git-config-path": "^1.0.1", - "ini": "^1.3.4" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" - } - }, - "pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", - "dev": true, - "requires": { - "escape-goat": "^2.0.0" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdir-glob": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.0.tgz", - "integrity": "sha512-KgT0oXPIDQRRRYFf+06AUaodICTep2Q5635BORLzTEzp7rEqcR14a47j3Vzm3ix7FeI1lp8mYyG7r8lTB06Pyg==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "referrer-policy": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz", - "integrity": "sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk=" - }, - "registry-auth-token": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", - "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "saslprep": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", - "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", - "optional": true, - "requires": { - "sparse-bitfield": "^3.0.3" - } - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" - }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", - "optional": true, - "requires": { - "memory-pager": "^1.0.2" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "tar-stream": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", - "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "term-size": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", - "dev": true - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "update-notifier": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", - "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", - "dev": true, - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - } - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "dev": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "requires": { - "string-width": "^4.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "x-xss-protection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.1.0.tgz", - "integrity": "sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg==" - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, - "zip-a-folder": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/zip-a-folder/-/zip-a-folder-0.0.12.tgz", - "integrity": "sha512-wZGiWgp3z2TocBlzx3S5tsLgPbT39qG2uIZmn2MhYLVjhKIr2nMhg7i4iPDL4W3XvMDaOEEVU5ZB0Y/Pt6BLvA==", - "requires": { - "archiver": "^3.1.1" - }, - "dependencies": { - "archiver": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", - "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", - "requires": { - "archiver-utils": "^2.1.0", - "async": "^2.6.3", - "buffer-crc32": "^0.2.1", - "glob": "^7.1.4", - "readable-stream": "^3.4.0", - "tar-stream": "^2.1.0", - "zip-stream": "^2.1.2" - } - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "compress-commons": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", - "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^3.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^2.3.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "crc32-stream": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", - "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^3.4.0" - } - }, - "zip-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", - "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", - "requires": { - "archiver-utils": "^2.1.0", - "compress-commons": "^2.1.1", - "readable-stream": "^3.4.0" - } - } - } - }, - "zip-stream": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.2.tgz", - "integrity": "sha512-TGxB2g+1ur6MHkvM644DuZr8Uzyz0k0OYWtS3YlpfWBEmK4woaC2t3+pozEL3dBfIPmpgmClR5B2QRcMgGt22g==", - "requires": { - "archiver-utils": "^2.1.0", - "compress-commons": "^4.0.0", - "readable-stream": "^3.6.0" - } - } - } -} From 6664708788635bfdaa76c518a7f6617a748ea608 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 00:14:30 -0400 Subject: [PATCH 02/63] update documenation --- README.md | 97 ++++++++++------------------------------------------ package.json | 2 +- 2 files changed, 19 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 417953d..6f2eec8 100755 --- a/README.md +++ b/README.md @@ -1,81 +1,20 @@ -# Node.js and Express Tutorial: Building and RESTful APIs +# Node.js and Express Backend ## Requirements - -Identify your mongo db url used previously. Ask JR for a shared mongo db url if needed. - -We will configure the mongo db url on aws as well as in our local environment. - -## Objectives - -Be able to do the following: - -### AWS - -- understand changes required for production deployment to aws - -- update your week-10 storefront api for deployment to aws - -- deploy your updated repository to aws - -- add your mongo db url as an environment variable on aws - -### Heroku - -- understand heroku deployment - -- install the heroku cli - -- login to heroku from the cli - -- configure environment variables on heroku - -## Overview - -This readme outlines all of the changes included in this repository that allow aws deployment. - -After an overview, you will make changes to your week 10 repository and deploy it to aws. - -You will commit your changes to your week 10 repository and create a new PR. - -## AWS and Heroku Deployment - -New this week is AWS and Heroku deployment and the changes that were made to support the deployment process. - -Below we list out changes required from the original development-only version we previously built. - -### package.json - -- Changed the "start" script to use "node" in place of "nodemon". Nodemon is for development only. - -- Added a "dev" script to support using nodemon in development. - -- Added a "zip-for-aws" script to zip content for deployment to aws. - -- Installed `env-cmd` and `archiver` node modules - -`env-cmd` allows us to have a `.env` file in development to configure our MONGO db url safely where the setting is not shared in git. - -`archiver` supports a script to run to generate the `zip` file that aws requires for the aws web console upload. - -### index.js - -- Updated "port" settings to allow the production server to set the port value. - -- Added configuration check for MONGO_URL environment variable and start DB only when configured. - -### AWS and Heroku - -See AWS.md and HEROKU.md - -## Previously in the repo - -We built this repo in week 10 and then updated it for week 12 as an introduction to connecting an express node app to a mongo database. - -This app was configured originally only for development and required additional work this week for production readiness. - -Previous versions: - -- Week 10: - -- Week 12: +Identify your MongoDB URL. Visit MongoDB to sign up and get started. + +Environmental Variables: +1. MONGO_URL +2. PORT (optional) + +## Getting Started + +Scripts: +```json +"scripts": { + "start": "node ./src/index.js", + "dev": "env-cmd nodemon ./src/index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "zip-for-aws": "node make-zip-for-aws.js" + }, +``` \ No newline at end of file diff --git a/package.json b/package.json index 294a56a..36eb6c8 100755 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node ./src/index.js", "dev": "env-cmd nodemon ./src/index.js", - "test": "node ./src/index.js", + "test": "echo \"Error: no test specified\" && exit 1", "zip-for-aws": "node make-zip-for-aws.js" }, "keywords": [], From 29a4f866d6cc9695773c755d230aadd1289963a1 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 00:20:08 -0400 Subject: [PATCH 03/63] chore: remove unused/old documentation --- .circleci | 39 ---- AWS.md | 25 --- HEROKU.md | 73 ------- aws-api-request.js | 178 ----------------- beanstalk-deploy.js | 478 -------------------------------------------- make-zip-for-aws.js | 53 ----- 6 files changed, 846 deletions(-) delete mode 100644 .circleci delete mode 100644 AWS.md delete mode 100644 HEROKU.md delete mode 100644 aws-api-request.js delete mode 100644 beanstalk-deploy.js delete mode 100644 make-zip-for-aws.js diff --git a/.circleci b/.circleci deleted file mode 100644 index 9b06d3c..0000000 --- a/.circleci +++ /dev/null @@ -1,39 +0,0 @@ -version: 2.1 -orbs: - node: circleci/node@1.1.6 -jobs: - build-and-test: - executor: - name: node/default - steps: - - checkout - - node/with-cache: - steps: - - run: npm install - deploy: - executor: - name: node/default - steps: - - checkout - - node/with-cache: - steps: - - run: sudo apt-get update && sudo apt-get install python-pip python-dev build-essential - - run: sudo pip install awscli - - run: aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile superhero-deploy - - run: aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile superhero-deploy - - run: aws configure set region $AWS_DEFAULT_REGION --profile superhero-deploy - - - run: npm install - - run: npm run deploy - -workflows: - build-and-test: - jobs: - - build-and-test - deploy-master: - jobs: - - deploy: - filters: - branches: - only: - - master diff --git a/AWS.md b/AWS.md deleted file mode 100644 index 9f3612c..0000000 --- a/AWS.md +++ /dev/null @@ -1,25 +0,0 @@ -# AWS Deployment - -This file describes the process of deploying this repository to AWS. - -## Add Environment Variables on AWS for you environment "Configuration" - -In the aws web console, navigate to beanstalk environments. - -- Click on your environment - -- Click "Configuration" - -- Under the "Category" column is a row with the value of "Software" - -- On the right side of that row is and "Edit" button. Click it to get to the "Modify Software" page. - -- On the "Modify Software" page, at the bottom is "Environment properties" where we will add our name/value pair. - -- We will use `MONGO_URL` as the name and we will add our mongo url as the value. - -## AWS eb cli - -Mac `brew update && brew install awsebcli && eb --version` - -Windows diff --git a/HEROKU.md b/HEROKU.md deleted file mode 100644 index a06e5ba..0000000 --- a/HEROKU.md +++ /dev/null @@ -1,73 +0,0 @@ -# Deploy to Heroku - -Be sure you have the `heroku` cli installed. - -- Mac `brew tap heroku/brew && brew install heroku` - -- Use installer - -## Heroku Login - -`heroku login` is required to authenticate your machine to connect to your heroku account. - -## Heroku Commands - -Run a local heroku server using your code. - -`heroku local web` - -## Deploy to Heroku as Public Web App - -First observer what your git remotes are. - -`git remote -v` - -Then connect your repo with heroku using - -`heroku create` - -Observer that you now have a new remote after executing `heroku create` - -`git remote -v` - -Amazing! - -Push to the remote heroku - -`git push heroku` - -Open the remotely hosted app in your browser. - -`heroku open` - -Amazing! But we have one more command to run to configure the environment variable for MONGO. - -First confirm that the environment variable is not set: - -`heroku config:get MONGO_URL` - -Our mongo url which we have configured in the `.env` file will be the same url we use. - -To set the value on the server while keeping the setting private we will use the `heroku config` command. - -Note that the url value on the right side of this assignment must be enclosed in quotes. - -If you do not have access to your own mongo db url, for now you can use the shared mongo url that is included here for demonstration purposes only. - -`heroku config:set MONGO_URL='mongodb://mlab2020:abc123def!@ds031617.mlab.com:31617/learningmongo'` - -Heroku will restart your web server. - -In your browser, refresh the app web page or run the command to open the heroku-host web url for your rep. - -`heroku open` - -## Connect to Watch the Log file from the remote Heroku Server - -`heroku logs --tail` - -## In Case of Issues - -Be sure there is a process on heroku setup to run your server by running this command: - -`heroku ps:scale web=1` diff --git a/aws-api-request.js b/aws-api-request.js deleted file mode 100644 index 8101169..0000000 --- a/aws-api-request.js +++ /dev/null @@ -1,178 +0,0 @@ -const crypto = require('crypto'), - https = require('https'), - zlib = require('zlib'); -const { encode } = require('punycode'); - -function awsApiRequest(options) { - return new Promise((resolve, reject) => { - let region = options.region || awsApiRequest.region || process.env.AWS_DEFAULT_REGION, - service = options.service, - accessKey = options.accessKey || awsApiRequest.accessKey || process.env.AWS_ACCESS_KEY_ID, - secretKey = options.secretKey || awsApiRequest.secretKey || process.env.AWS_SECRET_ACCESS_KEY, - sessionToken = options.sessionToken || awsApiRequest.sessionToken || process.env.AWS_SESSION_TOKEN, - method = options.method || 'GET', - path = options.path || '/', - querystring = options.querystring || {}, - payload = options.payload || '', - host = options.host || `${service}.${region}.amazonaws.com`, - headers = options.headers || {}; - - function hmacSha256(data, key, hex=false) { - return crypto.createHmac('sha256', key).update(data).digest(hex ? 'hex' : undefined); - } - - function sha256(data) { - return crypto.createHash('sha256').update(data).digest('hex'); - } - - //Thanks to https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-javascript - function createSigningKey(secretKey, dateStamp, region, serviceName) { - let kDate = hmacSha256(dateStamp, 'AWS4' + secretKey); - let kRegion = hmacSha256(region, kDate); - let kService = hmacSha256(serviceName, kRegion); - let kSigning = hmacSha256('aws4_request', kService); - return kSigning; - } - - function createSignedHeaders(headers) { - return Object.keys(headers).sort().map(h => h.toLowerCase()).join(';'); - } - - function createStringToSign(timestamp, region, service, canonicalRequest) { - let stringToSign = 'AWS4-HMAC-SHA256\n'; - stringToSign += timestamp + '\n'; - stringToSign += timestamp.substr(0,8) + '/' + region + '/' + service + '/aws4_request\n'; - stringToSign += sha256(canonicalRequest); - return stringToSign; - } - - function createCanonicalRequest(method, path, querystring, headers, payload) { - let canonical = method + '\n'; - - //Changed this from double encoding the path to single encoding it, to make S3 paths with spaces work. However, the documentation said to - //double encode it...? The only time we actually encode a path other than / is when uploading to S3 so just change this to single encoding here - //but it's possible it will mess up if the path has some weird characters that should be double encoded maybe??? If you had weird symbols in your version number? - canonical += encodeURI(path) + '\n'; - - let qsKeys = Object.keys(querystring); - qsKeys.sort(); - - //encodeURIComponent does NOT encode ', but we need it to be encoded. escape() is considered deprecated, so encode ' - //manually - function encodeValue(v) { - return encodeURIComponent(v).replace(/'/g,'%27'); - } - - let qsEntries = qsKeys.map(k => `${k}=${encodeValue(querystring[k])}`); - canonical += qsEntries.join('&') + '\n'; - - let headerKeys = Object.keys(headers).sort(); - let headerEntries = headerKeys.map(h => h.toLowerCase() + ':' + headers[h].replace(/^\s*|\s*$/g, '').replace(' +', ' ')); - canonical += headerEntries.join('\n') + '\n\n'; - - canonical += createSignedHeaders(headers) + '\n'; - canonical += sha256(payload); - - return canonical; - } - - function createAuthHeader(accessKey, timestamp, region, service, headers, signature) { - let date = timestamp.substr(0,8); - let signedHeaders = createSignedHeaders(headers); - return `AWS4-HMAC-SHA256 Credential=${accessKey}/${date}/${region}/${service}/aws4_request, SignedHeaders=${signedHeaders}, Signature=${signature}`; - } - - let timestamp = new Date().toISOString().replace(/(-|:|\.\d\d\d)/g, ''); // YYYYMMDD'T'HHmmSS'Z' - let datestamp = timestamp.substr(0,8); - - let sessionTokenHeader = sessionToken ? {'x-amz-security-token': sessionToken} : {}; - - let reqHeaders = Object.assign({ - Accept : 'application/json', - Host : host, - 'Content-Type' : 'application/json', - 'x-amz-date' : timestamp, - 'x-amz-content-sha256' : sha256(payload) - }, sessionTokenHeader, headers); // Passed in headers override these... - - let canonicalRequest = createCanonicalRequest(method, path, querystring, reqHeaders, payload); - let stringToSign = createStringToSign(timestamp, region, service, canonicalRequest); - let signingKey = createSigningKey(secretKey, datestamp, region, service); - let signature = hmacSha256(stringToSign, signingKey, true); - let authHeader = createAuthHeader(accessKey, timestamp, region, service, reqHeaders, signature); - - reqHeaders.Authorization = authHeader; - - //Now, lets finally do a HTTP REQUEST!!! - request(method, encodeURI(path), reqHeaders, querystring, payload, (err, result) => { - if (err) { - reject(err); - } else { - if (result.statusCode >= 300 && result.statusCode < 400 && result.headers.location) { - const url = new URL(result.headers.location); - headers.Host = url.hostname; - resolve(awsApiRequest({ - ...options, - host: url.hostname - })); - } else { - resolve(result); - } - } - }); - }); -} - -function createResult(data, res) { - if (!data || data.length === 0) { - return { statusCode: res.statusCode, headers: res.headers, data:''}; - } - if (data && data.length > 0 && res.headers['content-type'] === 'application/json') { - return { statusCode : res.statusCode, headers: res.headers, data : JSON.parse(data)}; - } else { - return { statusCode : res.statusCode, headers: res.headers, data}; - } -} - -function request(method, path, headers, querystring, data, callback) { - - let qs = Object.keys(querystring).map(k => `${k}=${encodeURIComponent(querystring[k])}`).join('&'); - path += '?' + qs; - let hostname = headers.Host; - delete headers.Host; - headers['Content-Length'] = data.length; - const port = 443; - try { - const options = { hostname, port, path, method, headers }; - const req = https.request(options, res => { - - let chunks = []; - res.on('data', d => chunks.push(d)); - res.on('end', () => { - let buffer = Buffer.concat(chunks); - if (res.headers['content-encoding'] === 'gzip') { - zlib.gunzip(buffer, (err, decoded) => { - if (err) { - callback(err); - } else { - callback(null, createResult(decoded, res)); - } - }); - } else { - callback(null, createResult(buffer, res)); - } - }); - - }); - req.on('error', err => callback(err)); - - if (data) { - req.write(data); - } - req.end(); - } catch(err) { - callback(err); - } -} - -module.exports = awsApiRequest; diff --git a/beanstalk-deploy.js b/beanstalk-deploy.js deleted file mode 100644 index 007ad1b..0000000 --- a/beanstalk-deploy.js +++ /dev/null @@ -1,478 +0,0 @@ -#!/usr/bin/env node -// Author: Einar Egilsson, https://github.com/einaregilsson/beanstalk-deploy - -const awsApiRequest = require('./aws-api-request'); -const fs = require('fs'); - -const IS_GITHUB_ACTION = !!process.env.GITHUB_ACTIONS; - -if (IS_GITHUB_ACTION) { - console.error = msg => console.log(`::error::${msg}`); - console.warn = msg => console.log(`::warning::${msg}`); -} - -function createStorageLocation() { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: {Operation: 'CreateStorageLocation', Version: '2010-12-01'} - }); -} - -function checkIfFileExistsInS3(bucket, s3Key) { - - return awsApiRequest({ - service : 's3', - host: `${bucket}.s3.amazonaws.com`, - path : s3Key, - method: 'HEAD' - }); -} - -function readFile(path) { - return new Promise((resolve, reject) => { - fs.readFile(path, (err, data) => { - if (err) { - reject(err); - } - resolve(data); - }); - }); -} - -function uploadFileToS3(bucket, s3Key, filebuffer) { - return awsApiRequest({ - service : 's3', - host: `${bucket}.s3.amazonaws.com`, - path : s3Key, - method: 'PUT', - headers: { 'Content-Type' : 'application/octet-stream'}, - payload: filebuffer - }); -} - -function createBeanstalkVersion(application, bucket, s3Key, versionLabel, versionDescription) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'CreateApplicationVersion', - Version: '2010-12-01', - ApplicationName : application, - VersionLabel : versionLabel, - Description : versionDescription, - 'SourceBundle.S3Bucket' : bucket, - 'SourceBundle.S3Key' : s3Key.substr(1) //Don't want leading / here - } - }); -} - -function deployBeanstalkVersion(application, environmentName, versionLabel) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'UpdateEnvironment', - Version: '2010-12-01', - ApplicationName : application, - EnvironmentName : environmentName, - VersionLabel : versionLabel - } - }); -} - -function describeEvents(application, environmentName, startTime) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'DescribeEvents', - Version: '2010-12-01', - ApplicationName : application, - Severity : 'TRACE', - EnvironmentName : environmentName, - StartTime : startTime.toISOString().replace(/(-|:|\.\d\d\d)/g, '') - } - }); -} - -function describeEnvironments(application, environmentName) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'DescribeEnvironments', - Version: '2010-12-01', - ApplicationName : application, - 'EnvironmentNames.members.1' : environmentName //Yes, that's the horrible way to pass an array... - } - }); -} - -function getApplicationVersion(application, versionLabel) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'DescribeApplicationVersions', - Version: '2010-12-01', - ApplicationName : application, - 'VersionLabels.members.1' : versionLabel //Yes, that's the horrible way to pass an array... - } - }); -} - -function expect(status, result, extraErrorMessage) { - if (status !== result.statusCode) {  - if (extraErrorMessage) { - console.log(extraErrorMessage); - } - if (result.headers['content-type'] !== 'application/json') { - throw new Error(`Status: ${result.statusCode}. Message: ${result.data}`); - } else { - throw new Error(`Status: ${result.statusCode}. Code: ${result.data.Error.Code}, Message: ${result.data.Error.Message}`); - } - } -} - -//Uploads zip file, creates new version and deploys it -function deployNewVersion(application, environmentName, versionLabel, versionDescription, file, waitUntilDeploymentIsFinished, waitForRecoverySeconds) { - - let s3Key = `/${application}/${versionLabel}.zip`; - let bucket, deployStart, fileBuffer; - - readFile(file).then(result => { - fileBuffer = result; - return createStorageLocation(); - }).then(result => { - expect(200, result ); - bucket = result.data.CreateStorageLocationResponse.CreateStorageLocationResult.S3Bucket; - console.log(`Uploading file to bucket ${bucket}`); - return checkIfFileExistsInS3(bucket, s3Key); - }).then(result => { - if (result.statusCode === 200) { - throw new Error(`Version ${versionLabel} already exists in S3!`); - } - expect(404, result); - return uploadFileToS3(bucket, s3Key, fileBuffer); - }).then(result => { - expect(200, result); - console.log(`New build successfully uploaded to S3, bucket=${bucket}, key=${s3Key}`); - return createBeanstalkVersion(application, bucket, s3Key, versionLabel, versionDescription); - }).then(result => { - expect(200, result); - console.log(`Created new application version ${versionLabel} in Beanstalk.`); - if (!environmentName) { - console.log(`No environment name given, so exiting now without deploying the new version ${versionLabel} anywhere.`); - process.exit(0); - } - deployStart = new Date(); - console.log(`Starting deployment of version ${versionLabel} to environment ${environmentName}`); - return deployBeanstalkVersion(application, environmentName, versionLabel, waitForRecoverySeconds); - }).then(result => { - expect(200, result); - - if (waitUntilDeploymentIsFinished) { - console.log('Deployment started, "wait_for_deployment" was true...\n'); - return waitForDeployment(application, environmentName, versionLabel, deployStart, waitForRecoverySeconds); - } else { - console.log('Deployment started, parameter "wait_for_deployment" was false, so action is finished.'); - console.log('**** IMPORTANT: Please verify manually that the deployment succeeds!'); - process.exit(0); - } - - }).then(envAfterDeployment => { - if (envAfterDeployment.Health === 'Green') { - console.log('Environment update successful!'); - process.exit(0); - } else { - console.warn(`Environment update finished, but environment health is: ${envAfterDeployment.Health}, HealthStatus: ${envAfterDeployment.HealthStatus}`); - process.exit(1); - } - }).catch(err => { - console.error(`Deployment failed: ${err}`); - process.exit(2); - }); -} - -//Deploys existing version in EB -function deployExistingVersion(application, environmentName, versionLabel, waitUntilDeploymentIsFinished, waitForRecoverySeconds) { - let deployStart = new Date(); - console.log(`Deploying existing version ${versionLabel}`); - - deployBeanstalkVersion(application, environmentName, versionLabel).then(result => { - expect(200, result); - if (waitUntilDeploymentIsFinished) { - console.log('Deployment started, "wait_for_deployment" was true...\n'); - return waitForDeployment(application, environmentName, versionLabel, deployStart, waitForRecoverySeconds); - } else { - console.log('Deployment started, parameter "wait_for_deployment" was false, so action is finished.'); - console.log('**** IMPORTANT: Please verify manually that the deployment succeeds!'); - process.exit(0); - } - }).then(envAfterDeployment => { - if (envAfterDeployment.Health === 'Green') { - console.log('Environment update successful!'); - process.exit(0); - } else { - console.warn(`Environment update finished, but environment health is: ${envAfterDeployment.Health}, HealthStatus: ${envAfterDeployment.HealthStatus}`); - process.exit(1); - } - }).catch(err => { - console.error(`Deployment failed: ${err}`); - process.exit(2); - }); -} - - -function strip(val) { - //Strip leadig or trailing whitespace - return (val || '').replace(/^\s*|\s*$/g, ''); -} - -function main() { - - let application, - environmentName, - versionLabel, - versionDescription, - region, - file, - useExistingVersionIfAvailable, - waitForRecoverySeconds = 30, - waitUntilDeploymentIsFinished = true; //Whether or not to wait for the deployment to complete... - - if (IS_GITHUB_ACTION) { //Running in GitHub Actions - application = strip(process.env.INPUT_APPLICATION_NAME); - environmentName = strip(process.env.INPUT_ENVIRONMENT_NAME); - versionLabel = strip(process.env.INPUT_VERSION_LABEL); - versionDescription = strip(process.env.INPUT_VERSION_DESCRIPTION); - file = strip(process.env.INPUT_DEPLOYMENT_PACKAGE); - - awsApiRequest.accessKey = strip(process.env.INPUT_AWS_ACCESS_KEY); - awsApiRequest.secretKey = strip(process.env.INPUT_AWS_SECRET_KEY); - awsApiRequest.sessionToken = strip(process.env.INPUT_AWS_SESSION_TOKEN); - awsApiRequest.region = strip(process.env.INPUT_REGION); - - if ((process.env.INPUT_WAIT_FOR_DEPLOYMENT || '').toLowerCase() == 'false') { - waitUntilDeploymentIsFinished = false; - } - - if (process.env.INPUT_WAIT_FOR_ENVIRONMENT_RECOVERY) { - waitForRecoverySeconds = parseInt(process.env.INPUT_WAIT_FOR_ENVIRONMENT_RECOVERY); - } - useExistingVersionIfAvailable = process.env.INPUT_USE_EXISTING_VERSION_IF_AVAILABLE == 'true' || process.env.INPUT_USE_EXISTING_VERSION_IF_AVAILABLE == 'True'; - - } else { //Running as command line script - if (process.argv.length < 6) { - console.log('\nbeanstalk-deploy: Deploy a zip file to AWS Elastic Beanstalk'); - console.log('https://github.com/einaregilsson/beanstalk-deploy\n'); - console.log('Usage: beanstalk-deploy.js []\n'); - console.log('Environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be defined for the program to work.'); - console.log('If is skipped the script will attempt to deploy an existing version named .\n'); - process.exit(1); - } - - [application, environmentName, versionLabel, region, file] = process.argv.slice(2); - versionDescription = ''; //Not available for this. - useExistingVersionIfAvailable = false; //This option is not available in the console version - - awsApiRequest.accessKey = strip(process.env.AWS_ACCESS_KEY_ID); - awsApiRequest.secretKey = strip(process.env.AWS_SECRET_ACCESS_KEY); - awsApiRequest.sessionToken = strip(process.env.AWS_SESSION_TOKEN); - awsApiRequest.region = strip(region); - } - - console.log('Beanstalk-Deploy: GitHub Action for deploying to Elastic Beanstalk.'); - console.log('https://github.com/einaregilsson/beanstalk-deploy'); - console.log(''); - - if (!awsApiRequest.region) { - console.error('Deployment failed: Region not specified!'); - process.exit(2); - } - if (!awsApiRequest.accessKey) { - console.error('Deployment failed: AWS Access Key not specified!'); - process.exit(2); - } - if (!awsApiRequest.secretKey) { - console.error('Deployment failed: AWS Secret Key not specified!'); - process.exit(2); - } - - - console.log(' ***** Input parameters were: ***** '); - console.log(' Application: ' + application); - console.log(' Environment: ' + environmentName); - console.log(' Version Label: ' + versionLabel); - console.log(' Version description: ' + versionDescription); - console.log(' AWS Region: ' + awsApiRequest.region); - console.log(' File: ' + file); - console.log(' AWS Access Key: ' + awsApiRequest.accessKey.length + ' characters long, starts with ' + awsApiRequest.accessKey.charAt(0)); - console.log(' AWS Secret Key: ' + awsApiRequest.secretKey.length + ' characters long, starts with ' + awsApiRequest.secretKey.charAt(0)); - console.log(' Wait for deployment: ' + waitUntilDeploymentIsFinished); - console.log(' Recovery wait time: ' + waitForRecoverySeconds); - console.log(''); - - getApplicationVersion(application, versionLabel).then(result => { - - expect(200, result); - - let versionsList = result.data.DescribeApplicationVersionsResponse.DescribeApplicationVersionsResult.ApplicationVersions; - let versionAlreadyExists = versionsList.length === 1; - - if (versionAlreadyExists) { - - if (!environmentName) { - console.error(`You have no environment set, so we are trying to only create version ${versionLabel}, but it already exists in Beanstalk!`); - process.exit(2); - } else if (file && !useExistingVersionIfAvailable) { - console.error(`Deployment failed: Version ${versionLabel} already exists. Either remove the "deployment_package" parameter to deploy existing version, or set the "use_existing_version_if_available" parameter to "true" to use existing version if it exists and deployment package if it doesn't.`); - process.exit(2); - } else { - if (file && useExistingVersionIfAvailable) { - console.log(`Ignoring deployment package ${file} since version ${versionLabel} already exists and "use_existing_version_if_available" is set to true.`); - } - console.log(`Deploying existing version ${versionLabel}, version info:`); - console.log(JSON.stringify(versionsList[0], null, 2)); - deployExistingVersion(application, environmentName, versionLabel, waitUntilDeploymentIsFinished, waitForRecoverySeconds); - } - } else { - if (file) { - deployNewVersion(application, environmentName, versionLabel, versionDescription, file, waitUntilDeploymentIsFinished, waitForRecoverySeconds); - } else { - console.error(`Deployment failed: No deployment package given but version ${versionLabel} doesn't exist, so nothing to deploy!`); - process.exit(2); - } - } - }).catch(err => { - console.error(`Deployment failed: ${err}`); - process.exit(2); - }); -} - -function formatTimespan(since) { - let elapsed = new Date().getTime() - since; - let seconds = Math.floor(elapsed / 1000); - let minutes = Math.floor(seconds / 60); - seconds -= (minutes * 60); - return `${minutes}m${seconds}s`; -} - -//Wait until the new version is deployed, printing any events happening during the wait... -function waitForDeployment(application, environmentName, versionLabel, start, waitForRecoverySeconds) { - let counter = 0; - let degraded = false; - let healThreshold; - let deploymentFailed = false; - - const SECOND = 1000; - const MINUTE = 60 * SECOND; - - let waitPeriod = 10 * SECOND; //Start at ten seconds, increase slowly, long deployments have been erroring with too many requests. - let waitStart = new Date().getTime(); - - let eventCalls = 0, environmentCalls = 0; // Getting throttled on these print out how many we're doing... - - let consecutiveThrottleErrors = 0; - - return new Promise((resolve, reject) => { - function update() { - - let elapsed = new Date().getTime() - waitStart; - - //Limit update requests for really long deploys - if (elapsed > (10 * MINUTE)) { - waitPeriod = 30 * SECOND; - } else if (elapsed > 5 * MINUTE) { - waitPeriod = 20 * SECOND; - } - - describeEvents(application, environmentName, start).then(result => { - eventCalls++; - - - //Allow a few throttling failures... - if (result.statusCode === 400 && result.data && result.data.Error && result.data.Error.Code == 'Throttling') { - consecutiveThrottleErrors++; - console.log(`Request to DescribeEvents was throttled, that's ${consecutiveThrottleErrors} throttle errors in a row...`); - return; - } - - consecutiveThrottleErrors = 0; //Reset the throttling count - - expect(200, result, `Failed in call to describeEvents, have done ${eventCalls} calls to describeEvents, ${environmentCalls} calls to describeEnvironments in ${formatTimespan(waitStart)}`); - let events = result.data.DescribeEventsResponse.DescribeEventsResult.Events.reverse(); //They show up in desc, we want asc for logging... - for (let ev of events) { - let date = new Date(ev.EventDate * 1000); //Seconds to milliseconds, - console.log(`${date.toISOString().substr(11,8)} ${ev.Severity}: ${ev.Message}`); - if (ev.Message.match(/Failed to deploy application/)) { - deploymentFailed = true; //wait until next iteration to finish, to get the final messages... - } - } - if (events.length > 0) { - start = new Date(events[events.length-1].EventDate * 1000 + 1000); //Add extra second so we don't get the same message next time... - } - }).catch(reject); - - describeEnvironments(application, environmentName).then(result => { - environmentCalls++; - - //Allow a few throttling failures... - if (result.statusCode === 400 && result.data && result.data.Error && result.data.Error.Code == 'Throttling') { - consecutiveThrottleErrors++; - console.log(`Request to DescribeEnvironments was throttled, that's ${consecutiveThrottleErrors} throttle errors in a row...`); - if (consecutiveThrottleErrors >= 5) { - throw new Error(`Deployment failed, got ${consecutiveThrottleErrors} throttling errors in a row while waiting for deployment`); - } - - setTimeout(update, waitPeriod); - return; - } - - expect(200, result, `Failed in call to describeEnvironments, have done ${eventCalls} calls to describeEvents, ${environmentCalls} calls to describeEnvironments in ${formatTimespan(waitStart)}`); - - consecutiveThrottleErrors = 0; - counter++; - let env = result.data.DescribeEnvironmentsResponse.DescribeEnvironmentsResult.Environments[0]; - if (env.VersionLabel === versionLabel && env.Status === 'Ready') { - if (!degraded) { - console.log(`Deployment finished. Version updated to ${env.VersionLabel}`); - console.log(`Status for ${application}-${environmentName} is ${env.Status}, Health: ${env.Health}, HealthStatus: ${env.HealthStatus}`); - - if (env.Health === 'Green') { - resolve(env); - } else { - console.warn(`Environment update finished, but health is ${env.Health} and health status is ${env.HealthStatus}. Giving it ${waitForRecoverySeconds} seconds to recover...`); - degraded = true; - healThreshold = new Date(new Date().getTime() + waitForRecoverySeconds * SECOND); - setTimeout(update, waitPeriod); - } - } else { - if (env.Health === 'Green') { - console.log(`Environment has recovered, health is now ${env.Health}, health status is ${env.HealthStatus}`); - resolve(env); - } else { - if (new Date().getTime() > healThreshold.getTime()) { - reject(new Error(`Environment still has health ${env.Health} ${waitForRecoverySeconds} seconds after update finished!`)); - } else { - let left = Math.floor((healThreshold.getTime() - new Date().getTime()) / 1000); - console.warn(`Environment still has health: ${env.Health} and health status ${env.HealthStatus}. Waiting ${left} more seconds before failing...`); - setTimeout(update, waitPeriod); - } - } - } - } else if (deploymentFailed) { - let msg = `Deployment failed! Current State: Version: ${env.VersionLabel}, Health: ${env.Health}, Health Status: ${env.HealthStatus}`; - console.log(`${new Date().toISOString().substr(11,8)} ERROR: ${msg}`); - reject(new Error(msg)); - } else { - if (counter % 6 === 0 && !deploymentFailed) { - console.log(`${new Date().toISOString().substr(11,8)} INFO: Still updating, status is "${env.Status}", health is "${env.Health}", health status is "${env.HealthStatus}"`); - } - setTimeout(update, waitPeriod); - } - }).catch(reject); - } - - update(); - }); -} - -main(); - - diff --git a/make-zip-for-aws.js b/make-zip-for-aws.js deleted file mode 100644 index e05d1e8..0000000 --- a/make-zip-for-aws.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - - Use archiver package to make a zip to use with aws - - https://www.npmjs.com/package/archiver - - https://github.com/archiverjs/node-archiver - - */ -// require modules -const fs = require('fs'); -const archiver = require('archiver'); - -// create a file to stream archive data to. -const output = fs.createWriteStream(__dirname + '/for-aws.zip'); -const archive = archiver('zip', { - zlib: { level: 9 } // Sets the compression level. -}); - -// listen for all archive data to be written -// 'close' event is fired only when a file descriptor is involved -output.on('close', function() { - console.log(archive.pointer() + ' total bytes zipped'); - console.log('Zip file is ready.'); -}); - -// good practice to catch warnings (ie stat failures and other non-blocking errors) -archive.on('warning', function(err) { - if (err.code === 'ENOENT') { - // log warning - console.log({err}) - } else { - // throw error - throw err; - } -}); - -// good practice to catch this error explicitly -archive.on('error', function(err) { - throw err; -}); - -// pipe archive data to the file -archive.pipe(output); - -// append both package json files -archive.glob('package*.json'); -// append all files in the src folder -archive.directory('src/'); - -// finalize the archive (ie we are done appending files but streams have to finish yet) -// 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand -archive.finalize(); From fbcc164d65d3fd9b3038f0b882c9f35a94af3f90 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 00:34:02 -0400 Subject: [PATCH 04/63] relete repeated code, move app code into app file --- src/app-common.js | 36 -------------------- src/app.js | 26 ++++++++++++++ src/database/categories.js | 56 ------------------------------- src/database/logos.js | 56 ------------------------------- src/database/product-types.js | 56 ------------------------------- src/database/products.js | 56 ------------------------------- src/database/variations.js | 56 ------------------------------- src/http_tests/categories.http | 27 --------------- src/http_tests/logos.http | 26 -------------- src/http_tests/product-types.http | 29 ---------------- src/http_tests/products.http | 25 -------------- src/http_tests/variations.http | 25 -------------- src/index.js | 28 +++------------- src/routes/categoriesRoutes.js | 30 ----------------- src/routes/logosRoutes.js | 31 ----------------- src/routes/product-typesRoutes.js | 38 --------------------- src/routes/productsRoutes.js | 39 --------------------- src/routes/variationsRoutes.js | 30 ----------------- 18 files changed, 30 insertions(+), 640 deletions(-) delete mode 100755 src/app-common.js create mode 100644 src/app.js delete mode 100755 src/database/categories.js delete mode 100755 src/database/logos.js delete mode 100755 src/database/product-types.js delete mode 100755 src/database/products.js delete mode 100755 src/database/variations.js delete mode 100755 src/http_tests/categories.http delete mode 100755 src/http_tests/logos.http delete mode 100755 src/http_tests/product-types.http delete mode 100755 src/http_tests/products.http delete mode 100755 src/http_tests/variations.http delete mode 100755 src/routes/categoriesRoutes.js delete mode 100755 src/routes/logosRoutes.js delete mode 100755 src/routes/product-typesRoutes.js delete mode 100755 src/routes/productsRoutes.js delete mode 100755 src/routes/variationsRoutes.js diff --git a/src/app-common.js b/src/app-common.js deleted file mode 100755 index 0898a10..0000000 --- a/src/app-common.js +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const cors = require('cors'); -const helmet = require('helmet'); -const morgan = require('morgan'); -const {startDatabase} = require('./database/mongo-common'); -// alternative: -// const mongo = require('./database/mongo-common'); -// mongo.startDatabase - -// Other entities: Logos, CustomizationOptions, Materials, Patterns - -// Bonus items: , Customer info, etc - -// defining the Express app -const app = express(); - -// adding Helmet to enhance your API's security -app.use(helmet()); - -// using bodyParser to parse JSON bodies into JS objects -// adds a `.body` property to the request so that our -// handler functions can easily work with that incoming data -app.use(bodyParser.json()); - -// enabling CORS for all requests (not very secure) -app.use(cors()); - -// adding morgan to log HTTP requests -app.use(morgan('combined')); - -module.exports = { - app, - startDatabase -} - diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..9267e83 --- /dev/null +++ b/src/app.js @@ -0,0 +1,26 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); + +const app = express(); + +app.use(helmet()); +app.use(express.json()); +app.use(cors()); +app.use(morgan('combined')); + +// endpoint to return top level api +// much like a switch statement +app.get('/', async (req, res) => { + res.send({ + message: "Storefront API. See documentation for use." + }); +}); + +app.use('/stores', require('./routes/storesRoutes')) + +module.exports = { + app +} + diff --git a/src/database/categories.js b/src/database/categories.js deleted file mode 100755 index 62cbce7..0000000 --- a/src/database/categories.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'categories'; - -async function createCategory(logo) { - const database = await getDatabase(); - logo.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(logo); - return insertedId; -} - -async function getCategories() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteCategory(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateCategory(id, logo) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete logo._id; - - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...logo, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createCategory, - getCategories, - deleteCategory, - updateCategory, -}; diff --git a/src/database/logos.js b/src/database/logos.js deleted file mode 100755 index bd29179..0000000 --- a/src/database/logos.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'logos'; - -async function createLogo(logo) { - const database = await getDatabase(); - logo.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(logo); - return insertedId; -} - -async function getLogos() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteLogo(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateLogo(id, logo) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete logo._id; - - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...logo, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createLogo, - getLogos, - deleteLogo, - updateLogo, -}; diff --git a/src/database/product-types.js b/src/database/product-types.js deleted file mode 100755 index 6d4dc24..0000000 --- a/src/database/product-types.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'product-types'; - -async function createProductType(productType) { - const database = await getDatabase(); - productType.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(productType); - return insertedId; -} - -async function getProductTypes() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteProductType(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateProductType(id, productType) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete productType._id; - productType.updatedBy = getUserName() - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...productType, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createProductType, - getProductTypes, - deleteProductType, - updateProductType, -}; \ No newline at end of file diff --git a/src/database/products.js b/src/database/products.js deleted file mode 100755 index cf2c6d2..0000000 --- a/src/database/products.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'products'; - -async function createProduct(product) { - const database = await getDatabase(); - product.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(product); - return insertedId; -} - -async function getProducts() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteProduct(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateProduct(id, product) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete product._id; - product.updatedBy = getUserName() - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...product, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createProduct, - getProducts, - deleteProduct, - updateProduct, -}; diff --git a/src/database/variations.js b/src/database/variations.js deleted file mode 100755 index 5fddc4b..0000000 --- a/src/database/variations.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'variations'; - -async function createVariation(variation) { - const database = await getDatabase(); - variation.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(variation); - return insertedId; -} - -async function getVariations() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteVariation(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateVariation(id, variation) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete variation._id; - - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...variation, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createVariation, - getVariations, - deleteVariation, - updateVariation, -}; \ No newline at end of file diff --git a/src/http_tests/categories.http b/src/http_tests/categories.http deleted file mode 100755 index 81d700a..0000000 --- a/src/http_tests/categories.http +++ /dev/null @@ -1,27 +0,0 @@ -### Test the categories -### categories are the labels under which products are organized - -GET http://localhost:3001/categories - -### Test creating a category after you build the code for adding a category -POST http://localhost:3001/categories -Content-Type: application/json - -{ - "category": "Men's Apparel" -} - -### Test the PUT which should change a category -PUT http://localhost:3001/categories/5f30519cdd1f2c20488d027c -Content-Type: application/json - -{ - "category": "Unisex Apparel" -} - -### Test the DELETE which should remove a logo - - -DELETE http://localhost:3001/categories/5f30519cdd1f2c20488d027c - - diff --git a/src/http_tests/logos.http b/src/http_tests/logos.http deleted file mode 100755 index 7d33c00..0000000 --- a/src/http_tests/logos.http +++ /dev/null @@ -1,26 +0,0 @@ -### Test the home page endpoint - -GET http://localhost:3001/logos - -### Test creating a logo after you build the code for adding a logo -POST http://localhost:3001/logos -Content-Type: application/json - -{ - "name": "Titleist", - "descirption": "As a child i thought this was pronounced Tit Leist", - "colors": ["Black", "Red"] -} - -### Test the PUT which should change a logo -PUT http://localhost:3001/logos/5f2c919f77f3d857d0566ee6 -Content-Type: application/json - -{ - "name": "Super Logo" -} - -### Test the DELETE which should remove a logo - - -DELETE http://localhost:3001/logos/5f2c920ed615b533c42c9d0d diff --git a/src/http_tests/product-types.http b/src/http_tests/product-types.http deleted file mode 100755 index 283b7ce..0000000 --- a/src/http_tests/product-types.http +++ /dev/null @@ -1,29 +0,0 @@ -### Test the types -### types are the product types -# Product types (or product classes) are groups of products which share the same attributes. - -# Product attributes contain additional product information, e.g. ISBN, UPC, Brand, which is displayed in storefront and included in product feeds when exporting to marketplaces like Google Shopping, eBay, Amazon ads etc. - -GET http://localhost:3001/product-types - -### Test creating a type - -POST http://localhost:3001/product-types -Content-Type: application/json - -{ - "Type": "Polo Shirt" -} - -### Test the PUT which should change a product type - -PUT http://localhost:3001/product-types/5f30598df68fc848702a50f5 -Content-Type: application/json - -{ - "Type": "Yolo Shirt" -} - -### Test the DELETE which should delete a product type - -DELETE http://localhost:3001/product-types/5f30598df68fc848702a50f5 \ No newline at end of file diff --git a/src/http_tests/products.http b/src/http_tests/products.http deleted file mode 100755 index 2c1e39f..0000000 --- a/src/http_tests/products.http +++ /dev/null @@ -1,25 +0,0 @@ -### Test the home page endpoint - -GET http://localhost:3001/products - -### Test creating a product -POST http://localhost:3001/products -Content-Type: application/json - -{ - "title": "Beer Coozie", - "description": "The most important part of any round of golf" -} - -### Test the PUT which should change a product -### id of 5f2b54de320478eb4b1603eb is temporary while server is running -PUT http://localhost:3001/products/5f2c7a7832d9964f9d97826e -Content-Type: application/json - -{ - "title": "Amazing UV Shirt" -} - -### Test the DELETE which should delete a product -DELETE http://localhost:3001/products/5f2c7a7832d9964f9d97826e - diff --git a/src/http_tests/variations.http b/src/http_tests/variations.http deleted file mode 100755 index 5459d19..0000000 --- a/src/http_tests/variations.http +++ /dev/null @@ -1,25 +0,0 @@ -### Test the variations -### variations are the different styles that can be applied to a product - -GET http://localhost:3001/variations - -### Test creating a variation - -POST http://localhost:3001/variations -Content-Type: application/json - -{ - "variation": "Red" -} - -### Test the PUT which should change a variation - -PUT http://localhost:3001/variations/5f305ed5a159c55254bb16d5 -Content-Type: application/json - -{ - "variation": "Dark Red" -} - -### Test DELETE which should delete a variation -DELETE http://localhost:3001/variations/5f305ed5a159c55254bb16d5 \ No newline at end of file diff --git a/src/index.js b/src/index.js index 1d3d126..df6e268 100755 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,5 @@ -// This file is: ./src/index.js - -//importing the dependencies -const { - app, - startDatabase -} = require('./app-common.js'); -const { request } = require('express'); +const { app } = require('./app.js'); +const { startDatabase } = require('./database/mongo-common.js'); // support production deployment on a port configured on the hosting server // default to the dev port number otherwise @@ -17,20 +11,6 @@ const MONGO_URL = process.env.MONGO_URL; // https://www.mongodb.com/ if (MONGO_URL) { - // endpoint to return top level api - // much like a switch statement - app.get('/', async (req, res) => { - res.send({ - message: "Storefront API. See documentation for use." - }); - }); - - app.use('/products', require('./routes/productsRoutes')) - app.use('/logos', require('./routes/logosRoutes')) - app.use('/stores', require('./routes/storesRoutes')) - app.use('/categories', require('./routes/categoriesRoutes')) - app.use('/product-types', require('./routes/product-typesRoutes')) - app.use('/variations', require('./routes/variationsRoutes')) startDatabase().then(async () => { // `then` start the web server after the database starts @@ -48,7 +28,7 @@ if (MONGO_URL) { }); }); - app.listen(port, async () => { - console.log(`Web server has started on port ${port}`); + app.listen(port, async () => { + console.log(`Web server has started on port ${port}`); }); } diff --git a/src/routes/categoriesRoutes.js b/src/routes/categoriesRoutes.js deleted file mode 100755 index 70b57d2..0000000 --- a/src/routes/categoriesRoutes.js +++ /dev/null @@ -1,30 +0,0 @@ -const router = require('express').Router(); -const {deleteCategory, updateCategory, createCategory, getCategories} = require('../database/categories'); - -router.get('/', async (req, res) => { - res.send(await getCategories()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newCategory = apiRequest.body; - await createCategory(newCategory); - apiResponse.send({ - message: 'New Category created.', - allCategories: await getCategories(), - }); -}); - -router.delete('/:categoryId', async (apiRequest, apiResponse) => { - await deleteCategory(apiRequest.params.categoryId); - apiResponse.send({ message: 'Category deleted.' }); -}); - -// endpoint to update a Category -router.put('/:id', async (apiRequest, apiResponse) => { - const updatedCategory = apiRequest.body; - console.log({ updatedCategory}) - await updateCategory(apiRequest.params.id, updatedCategory); - apiResponse.send({ message: 'Category updated.' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/logosRoutes.js b/src/routes/logosRoutes.js deleted file mode 100755 index 4331801..0000000 --- a/src/routes/logosRoutes.js +++ /dev/null @@ -1,31 +0,0 @@ - -const router = require('express').Router(); -const {deleteLogo, updateLogo, createLogo, getLogos} = require('../database/logos'); - -router.get('/', async (req, res) => { - res.send(await getLogos()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newLogo = apiRequest.body; - await createLogo(newLogo); - apiResponse.send({ - message: 'New logo created.', - allLogos: await getLogos(), - }); -}); - -router.delete('/:logoId', async (apiRequest, apiResponse) => { - await deleteLogo(apiRequest.params.logoId); - apiResponse.send({ message: 'logo deleted.' }); -}); - -// endpoint to update a logo -router.put('/:id', async (apiRequest, apiResponse) => { - const updatedLogo = apiRequest.body; - console.log({ updateLogo }) - await updateLogo(apiRequest.params.id, updatedLogo); - apiResponse.send({ message: 'logo updated.' }); -}); - -module.exports = router; diff --git a/src/routes/product-typesRoutes.js b/src/routes/product-typesRoutes.js deleted file mode 100755 index 70dc55e..0000000 --- a/src/routes/product-typesRoutes.js +++ /dev/null @@ -1,38 +0,0 @@ -const router = require('express').Router(); -const {deleteProductType, updateProductType, createProductType, getProductTypes} = require('../database/product-types'); - -router.get('/', async (apiRequest, apiResponse) => { - apiResponse.send(await getProductTypes()); -}); - -// we name our parameters apiRequest and apiResponse here but -// there is no strong reason these variables could not be named `req` and `res` or `request` and `response` -// the reason for this naming is so we are thinking about "api" tonight -router.post('/', async (apiRequest, apiResponse) => { - const newProductType = apiRequest.body; - await createProductType(newProductType); - apiResponse.send({ - message: 'New product type created.', - allProductTypes: await getProductTypes(), - thanks: true - }); -}); - -// endpoint to delete a product -router.delete('/:productTypeId', async (apiRequest, apiResponse) => { - await deleteProductType(apiRequest.params.productTypeId); - apiResponse.send({ message: 'Product type deleted.' }); -}); - -// endpoint to update a product -router.put('/:productTypeId', async (apiRequest, apiResponse) => { - const updatedProductType = apiRequest.body; - console.log({ updatedProductType}) - await updateProductType(apiRequest.params.productTypeId, updatedProductType); - apiResponse.send({ message: 'Product type updated.' }); -}); - -module.exports = router; - - - diff --git a/src/routes/productsRoutes.js b/src/routes/productsRoutes.js deleted file mode 100755 index b50ca1d..0000000 --- a/src/routes/productsRoutes.js +++ /dev/null @@ -1,39 +0,0 @@ - -const router = require('express').Router(); -const {deleteProduct, updateProduct, createProduct, getProducts} = require('../database/products'); - -router.get('/', async (apiRequest, apiResponse) => { - apiResponse.send(await getProducts()); -}); - -// we name our parameters apiRequest and apiResponse here but -// there is no strong reason these variables could not be named `req` and `res` or `request` and `response` -// the reason for this naming is so we are thinking about "api" tonight -router.post('/', async (apiRequest, apiResponse) => { - const newProduct = apiRequest.body; - await createProduct(newProduct); - apiResponse.send({ - message: 'New product created.', - allProducts: await getProducts(), - thanks: true - }); -}); - -// endpoint to delete a product -router.delete('/:productId', async (apiRequest, apiResponse) => { - await deleteProduct(apiRequest.params.productId); - apiResponse.send({ message: 'Product deleted.' }); -}); - -// endpoint to update a product -router.put('/:id', async (apiRequest, apiResponse) => { - const updatedProduct = apiRequest.body; - console.log({ updatedProduct}) - await updateProduct(apiRequest.params.id, updatedProduct); - apiResponse.send({ message: 'Product updated.' }); -}); - -module.exports = router; - - - diff --git a/src/routes/variationsRoutes.js b/src/routes/variationsRoutes.js deleted file mode 100755 index c1b2f8b..0000000 --- a/src/routes/variationsRoutes.js +++ /dev/null @@ -1,30 +0,0 @@ -const router = require('express').Router(); -const {deleteVariation, updateVariation, createVariation, getVariations} = require('../database/variations'); - -router.get('/', async (req, res) => { - res.send(await getVariations()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newVariation = apiRequest.body; - await createVariation(newVariation); - apiResponse.send({ - message: 'New variation created.', - allStores: await getVariations(), - }); -}); - -router.delete('/:variationId', async (apiRequest, apiResponse) => { - await deleteVariation(apiRequest.params.variationId); - apiResponse.send({ message: 'Variation deleted.' }); -}); - -// endpoint to update a Store -router.put('/:variationId', async (apiRequest, apiResponse) => { - const updatedVariation = apiRequest.body; - console.log({ updateVariation }) - await updateVariation(apiRequest.params.variationId, updatedVariation); - apiResponse.send({ message: 'Variation updated.' }); -}); - -module.exports = router; \ No newline at end of file From 29683c5165ca3cf76d2ca92ff37ffc270364fa7f Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 00:37:24 -0400 Subject: [PATCH 05/63] refactor: improve server startup logic with async/await and error handling --- src/index.js | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/index.js b/src/index.js index df6e268..580afa9 100755 --- a/src/index.js +++ b/src/index.js @@ -1,34 +1,34 @@ const { app } = require('./app.js'); const { startDatabase } = require('./database/mongo-common.js'); -// support production deployment on a port configured on the hosting server -// default to the dev port number otherwise -const port = process.env.PORT || 3001; - +const PORT = process.env.PORT || 3001; const MONGO_URL = process.env.MONGO_URL; -// connect to our database then start the web server -// https://www.mongodb.com/ -if (MONGO_URL) { - - - startDatabase().then(async () => { - // `then` start the web server after the database starts - app.listen(port, async () => { - console.log(`Web server has started on port ${port}`); +async function startServer() { + if (!MONGO_URL) { + // Gracefully handle missing DB config + app.all('*', (req, res) => { + res.status(500).send({ + message: 'MONGO_URL not configured. See documentation.', + }); }); - }); -} else { - // endpoint to return top level api - // much like a switch statement - app.all('*', async (req, res) => { - res.send({ - message: "MONGO_URL not configured. See documentation." + app.listen(PORT, () => { + console.log(`Server running without DB on port ${PORT}`); }); - }); - app.listen(port, async () => { - console.log(`Web server has started on port ${port}`); - }); + return; + } + + try { + await startDatabase(); + app.listen(PORT, () => { + console.log(`Server started on port ${PORT}`); + }); + } catch (err) { + console.error('Failed to start database:', err); + process.exit(1); // Exit if DB connection fails + } } + +startServer(); \ No newline at end of file From 39b2d48cc6775a92a33323f06d83f113f82a4011 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 00:50:35 -0400 Subject: [PATCH 06/63] clean up --- src/app.js | 31 +++++++++++++++++++++---------- src/database/mongo-common.js | 4 ++-- src/index.js | 2 +- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/app.js b/src/app.js index 9267e83..47d3e38 100644 --- a/src/app.js +++ b/src/app.js @@ -10,17 +10,28 @@ app.use(express.json()); app.use(cors()); app.use(morgan('combined')); -// endpoint to return top level api -// much like a switch statement -app.get('/', async (req, res) => { - res.send({ - message: "Storefront API. See documentation for use." - }); +// Redirect root to /health +app.get('/', (req, res) => { + res.redirect('/health'); }); -app.use('/stores', require('./routes/storesRoutes')) +// Health check endpoint +app.get('/health', (req, res) => { + res.status(200).send('OK'); +}); + +// Routes +app.use('/stores', require('./routes/storesRoutes')); -module.exports = { - app -} +// 404 handler +app.use('*', (req, res) => { + res.status(404).send({ message: 'Route not found' }); +}); + +// Global error handler +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).send({ message: 'Internal Server Error' }); +}); +module.exports = app; \ No newline at end of file diff --git a/src/database/mongo-common.js b/src/database/mongo-common.js index 3630dd2..4d2e77d 100755 --- a/src/database/mongo-common.js +++ b/src/database/mongo-common.js @@ -1,13 +1,13 @@ /** All configuration that is required for a shared mongo server hosted in the cloud */ -const {MongoClient} = require('mongodb'); +const { MongoClient } = require('mongodb'); let database = null; const mongoDBURL = process.env.MONGO_URL; async function startDatabase() { - const connection = await MongoClient.connect(mongoDBURL, {useNewUrlParser: true}); + const connection = await MongoClient.connect(mongoDBURL, { useNewUrlParser: true, useUnifiedTopology: true }); database = connection.db(); } diff --git a/src/index.js b/src/index.js index 580afa9..3282a95 100755 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -const { app } = require('./app.js'); +const app = require('./app.js'); const { startDatabase } = require('./database/mongo-common.js'); const PORT = process.env.PORT || 3001; From ae90b63c6657090f4eaa2aed2e9b3d6265a4032b Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 00:55:19 -0400 Subject: [PATCH 07/63] chore: update mongo to latest --- package.json | 2 +- src/database/mongo-common.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 36eb6c8..6d1a9d9 100755 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "express": "^4.16.4", "git-user-name": "^2.0.0", "helmet": "^3.15.1", - "mongodb": "^3.1.13", + "mongodb": "^6.16.0", "morgan": "^1.9.1", "zip-a-folder": "0.0.12" }, diff --git a/src/database/mongo-common.js b/src/database/mongo-common.js index 4d2e77d..baf66bb 100755 --- a/src/database/mongo-common.js +++ b/src/database/mongo-common.js @@ -7,7 +7,8 @@ let database = null; const mongoDBURL = process.env.MONGO_URL; async function startDatabase() { - const connection = await MongoClient.connect(mongoDBURL, { useNewUrlParser: true, useUnifiedTopology: true }); + const client = new MongoClient(mongoDBURL); + const connection = await client.connect(); database = connection.db(); } From 11216f972ae2c03f455bed64e87b98dde62fb283 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 01:20:50 -0400 Subject: [PATCH 08/63] chore: reorganize files into their own files --- src/app.js | 34 +++++++++++----------------------- src/database/stores.js | 6 +++--- src/http_tests/index.http | 6 ++++-- src/http_tests/stores.http | 6 +++--- src/midleware/errorHandler.js | 17 +++++++++++++++++ src/routes/healthRoute.js | 11 +++++++++++ src/routes/notFoundRoute.js | 12 ++++++++++++ src/routes/rootRoute.js | 7 +++++++ 8 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 src/midleware/errorHandler.js create mode 100644 src/routes/healthRoute.js create mode 100644 src/routes/notFoundRoute.js create mode 100644 src/routes/rootRoute.js diff --git a/src/app.js b/src/app.js index 47d3e38..f22e62b 100644 --- a/src/app.js +++ b/src/app.js @@ -3,35 +3,23 @@ const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); +const errorHandler = require('./midleware/errorHandler'); +const notFoundRouter = require('./routes/notFoundRoute') +const healthRouter = require('./routes/healthRoute') +const storesRouter = require('./routes/storesRoutes'); +const rootRouter = require('./routes/rootRoute'); + const app = express(); app.use(helmet()); app.use(express.json()); app.use(cors()); app.use(morgan('combined')); +app.use(errorHandler); -// Redirect root to /health -app.get('/', (req, res) => { - res.redirect('/health'); -}); - -// Health check endpoint -app.get('/health', (req, res) => { - res.status(200).send('OK'); -}); - -// Routes -app.use('/stores', require('./routes/storesRoutes')); - -// 404 handler -app.use('*', (req, res) => { - res.status(404).send({ message: 'Route not found' }); -}); - -// Global error handler -app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).send({ message: 'Internal Server Error' }); -}); +app.use('/', rootRouter); +app.use('/health', healthRouter); +app.use('/stores', storesRouter); +app.use('*', notFoundRouter); module.exports = app; \ No newline at end of file diff --git a/src/database/stores.js b/src/database/stores.js index f7694be..09313e5 100755 --- a/src/database/stores.js +++ b/src/database/stores.js @@ -1,6 +1,6 @@ const {getDatabase} = require('./mongo-common'); // https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); +const {ObjectID, ObjectId} = require('mongodb'); const getUserName = require('git-user-name'); @@ -26,7 +26,7 @@ async function deleteStore(id) { // https://docs.mongodb.com/manual/reference/method/ObjectId/ // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), + _id: id, }); } @@ -38,7 +38,7 @@ async function updateStore(id, store) { // https://docs.mongodb.com/manual/reference/method/db.collection.update/ await database.collection(collectionName).update( - { _id: new ObjectID(id), }, + { _id: id }, { $set: { ...store, diff --git a/src/http_tests/index.http b/src/http_tests/index.http index 5753bf9..10bc4e0 100755 --- a/src/http_tests/index.http +++ b/src/http_tests/index.http @@ -1,6 +1,8 @@ -### Test the home page endpoint - +### Home page should redirect to /health GET http://localhost:3001 +### Get /health +GET http://localhost:3001/health + ### Test a 404 GET http://localhost:3001/testing-404 diff --git a/src/http_tests/stores.http b/src/http_tests/stores.http index 651d77a..d96477d 100755 --- a/src/http_tests/stores.http +++ b/src/http_tests/stores.http @@ -15,12 +15,12 @@ Content-Type: application/json ### Test the PUT which should change a store -PUT http://localhost:3001/stores/5f2caef3b78cd6525812e063 +PUT http://localhost:3001/stores/68203238d1857e2fae0b6093 Content-Type: application/json { - "Store Profile": "Nevada Golf Emporium" + "metadata": "68203238d1857e2fae0b6093", } ### Test DELETE which should delete a store -DELETE http://localhost:3001/stores/5f2caef3b78cd6525812e063 \ No newline at end of file +DELETE http://localhost:3001/stores/68203238d1857e2fae0b6093 \ No newline at end of file diff --git a/src/midleware/errorHandler.js b/src/midleware/errorHandler.js new file mode 100644 index 0000000..1bed0f9 --- /dev/null +++ b/src/midleware/errorHandler.js @@ -0,0 +1,17 @@ +// Centralized error-handling middleware for Express +// Logs the stack trace and sends a structured response to the client. + +const errorHandler = (err, req, res, next) => { + // Log detailed error for debugging + console.error(`[ERROR] ${err.stack}`); + + // Customize error response based on environment + const response = { + message: 'Internal Server Error', + ...(process.env.NODE_ENV !== 'production' && { error: err.message }), + }; + + res.status(500).json(response); +}; + +module.exports = errorHandler; \ No newline at end of file diff --git a/src/routes/healthRoute.js b/src/routes/healthRoute.js new file mode 100644 index 0000000..1ca936d --- /dev/null +++ b/src/routes/healthRoute.js @@ -0,0 +1,11 @@ +const router = require('express').Router(); + +router.get('/', (req, res) => { + res.status(200).json({ + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/notFoundRoute.js b/src/routes/notFoundRoute.js new file mode 100644 index 0000000..7c3deb2 --- /dev/null +++ b/src/routes/notFoundRoute.js @@ -0,0 +1,12 @@ +const router = require('express').Router(); + +router.all('*', (req, res) => { + res.status(404).send({ + message: 'Route not found', + method: req.method, + endpoint: req.originalUrl, + timestamp: new Date().toISOString(), + }); +}); + +module.exports = router; diff --git a/src/routes/rootRoute.js b/src/routes/rootRoute.js new file mode 100644 index 0000000..c0b60da --- /dev/null +++ b/src/routes/rootRoute.js @@ -0,0 +1,7 @@ +const router = require('express').Router(); + +router.get('/', (_, res) => { + res.redirect('/health'); +}); + +module.exports = router; \ No newline at end of file From cbe25d4aacaa007b062dd21e4d8c56441ccf2c08 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 01:34:52 -0400 Subject: [PATCH 09/63] add supertest tests --- package.json | 6 ++++-- src/database/stores.js | 4 ++-- src/http_tests/{index.http => app.http} | 0 src/http_tests/app.test.js | 25 +++++++++++++++++++++++++ src/routes/storesRoutes.js | 8 ++++---- 5 files changed, 35 insertions(+), 8 deletions(-) rename src/http_tests/{index.http => app.http} (100%) mode change 100755 => 100644 create mode 100644 src/http_tests/app.test.js diff --git a/package.json b/package.json index 6d1a9d9..f10e4af 100755 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node ./src/index.js", "dev": "env-cmd nodemon ./src/index.js", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest", "zip-for-aws": "node make-zip-for-aws.js" }, "keywords": [], @@ -25,6 +25,8 @@ "zip-a-folder": "0.0.12" }, "devDependencies": { - "nodemon": "^2.0.4" + "jest": "^29.7.0", + "nodemon": "^2.0.4", + "supertest": "^7.1.0" } } diff --git a/src/database/stores.js b/src/database/stores.js index 09313e5..ef176c4 100755 --- a/src/database/stores.js +++ b/src/database/stores.js @@ -21,12 +21,12 @@ async function getStores() { return await database.collection(collectionName).find({}).toArray(); } -async function deleteStore(id) { +async function deleteStore(_id) { const database = await getDatabase(); // https://docs.mongodb.com/manual/reference/method/ObjectId/ // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ await database.collection(collectionName).deleteOne({ - _id: id, + _id, }); } diff --git a/src/http_tests/index.http b/src/http_tests/app.http old mode 100755 new mode 100644 similarity index 100% rename from src/http_tests/index.http rename to src/http_tests/app.http diff --git a/src/http_tests/app.test.js b/src/http_tests/app.test.js new file mode 100644 index 0000000..dc77872 --- /dev/null +++ b/src/http_tests/app.test.js @@ -0,0 +1,25 @@ +const request = require('supertest'); +const app = require('../app'); + +describe('Health Check Endpoint', () => { + + it('should return 302 and redirect to /health', async () => { + const res = await request(app).get('/'); + expect(res.statusCode).toEqual(302); + expect(res.headers.location).toBe('/health'); + }) + + it('should return 200 and status OK', async () => { + const res = await request(app).get('/health'); + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('OK'); + }); + + it('should return 404 for non-existent endpoint', async () => { + const res = await request(app).get('/non-existent'); + expect(res.statusCode).toEqual(404); + expect(res.body.message).toBe('Route not found'); + }); + + +}); diff --git a/src/routes/storesRoutes.js b/src/routes/storesRoutes.js index f5671a1..1848a61 100755 --- a/src/routes/storesRoutes.js +++ b/src/routes/storesRoutes.js @@ -15,16 +15,16 @@ router.post('/', async (apiRequest, apiResponse) => { }); }); -router.delete('/:storeId', async (apiRequest, apiResponse) => { - await deleteStore(apiRequest.params.storeId); +router.delete('/:_id', async (apiRequest, apiResponse) => { + await deleteStore(apiRequest.params._id); apiResponse.send({ message: 'Store deleted.' }); }); // endpoint to update a Store -router.put('/:storeId', async (apiRequest, apiResponse) => { +router.put('/:_id', async (apiRequest, apiResponse) => { const updatedStore = apiRequest.body; console.log({ updateStore }) - await updateStore(apiRequest.params.storeId, updatedStore); + await updateStore(apiRequest.params._id, updatedStore); apiResponse.send({ message: 'Store updated.' }); }); From 799a4902d48e10934cc3111169e7843090bba029 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 10:26:37 -0400 Subject: [PATCH 10/63] fix: ObjectId updated to current --- src/database/stores.js | 18 ++++++++++++------ src/http_tests/stores.http | 6 +++--- src/routes/storesRoutes.js | 3 +-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/database/stores.js b/src/database/stores.js index ef176c4..5ca7184 100755 --- a/src/database/stores.js +++ b/src/database/stores.js @@ -1,6 +1,6 @@ -const {getDatabase} = require('./mongo-common'); +const { getDatabase } = require('./mongo-common'); // https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID, ObjectId} = require('mongodb'); +const { ObjectId } = require('mongodb'); const getUserName = require('git-user-name'); @@ -11,7 +11,7 @@ async function createStore(store) { const database = await getDatabase(); store.addedBy = getUserName() // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(store); + const { insertedId } = await database.collection(collectionName).insertOne(store); return insertedId; } @@ -25,9 +25,15 @@ async function deleteStore(_id) { const database = await getDatabase(); // https://docs.mongodb.com/manual/reference/method/ObjectId/ // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id, + const restuls = await database.collection(collectionName).deleteOne({ + _id: ObjectId.createFromHexString(_id), }); + + if (restuls.deletedCount === 0) { + return "No store found with that id"; + } + + return "Store deleted"; } async function updateStore(id, store) { @@ -38,7 +44,7 @@ async function updateStore(id, store) { // https://docs.mongodb.com/manual/reference/method/db.collection.update/ await database.collection(collectionName).update( - { _id: id }, + { _id: ObjectId.createFromHexString(id) }, { $set: { ...store, diff --git a/src/http_tests/stores.http b/src/http_tests/stores.http index d96477d..c4193ce 100755 --- a/src/http_tests/stores.http +++ b/src/http_tests/stores.http @@ -15,12 +15,12 @@ Content-Type: application/json ### Test the PUT which should change a store -PUT http://localhost:3001/stores/68203238d1857e2fae0b6093 +PUT http://localhost:3001/stores/6820ae08990eaee632a18472 Content-Type: application/json { - "metadata": "68203238d1857e2fae0b6093", + "metadata": "68203238d1857e2fae0b6093" } ### Test DELETE which should delete a store -DELETE http://localhost:3001/stores/68203238d1857e2fae0b6093 \ No newline at end of file +DELETE http://localhost:3001/stores/6820ae08990eaee632a18472 \ No newline at end of file diff --git a/src/routes/storesRoutes.js b/src/routes/storesRoutes.js index 1848a61..33a24cc 100755 --- a/src/routes/storesRoutes.js +++ b/src/routes/storesRoutes.js @@ -16,8 +16,7 @@ router.post('/', async (apiRequest, apiResponse) => { }); router.delete('/:_id', async (apiRequest, apiResponse) => { - await deleteStore(apiRequest.params._id); - apiResponse.send({ message: 'Store deleted.' }); + apiResponse.send({ message: await deleteStore(apiRequest.params._id) }); }); // endpoint to update a Store From 923431799217fb44536aab3ede7a3d7da7a93ddb Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 12:49:51 -0400 Subject: [PATCH 11/63] enhance: better structure, remove git-user-name to remove vulnerability --- package.json | 5 +++-- src/app.js | 4 ++++ src/database/stores.js | 2 +- src/midleware/compression.js | 3 +++ src/midleware/rateLimiter.js | 8 ++++++++ src/utils/git-user-name.js | 12 ++++++++++++ 6 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/midleware/compression.js create mode 100644 src/midleware/rateLimiter.js create mode 100644 src/utils/git-user-name.js diff --git a/package.json b/package.json index f10e4af..1973be5 100755 --- a/package.json +++ b/package.json @@ -15,18 +15,19 @@ "dependencies": { "archiver": "^5.0.2", "body-parser": "^1.18.3", + "compression": "^1.8.0", "cors": "^2.8.5", "env-cmd": "^10.1.0", "express": "^4.16.4", - "git-user-name": "^2.0.0", + "express-rate-limit": "^7.5.0", "helmet": "^3.15.1", "mongodb": "^6.16.0", "morgan": "^1.9.1", + "nodemon": "^3.1.10", "zip-a-folder": "0.0.12" }, "devDependencies": { "jest": "^29.7.0", - "nodemon": "^2.0.4", "supertest": "^7.1.0" } } diff --git a/src/app.js b/src/app.js index f22e62b..7c3cffd 100644 --- a/src/app.js +++ b/src/app.js @@ -4,6 +4,8 @@ const helmet = require('helmet'); const morgan = require('morgan'); const errorHandler = require('./midleware/errorHandler'); +const rateLimiter = require('./midleware/rateLimiter'); +const compression = require('./midleware/compression'); const notFoundRouter = require('./routes/notFoundRoute') const healthRouter = require('./routes/healthRoute') const storesRouter = require('./routes/storesRoutes'); @@ -16,6 +18,8 @@ app.use(express.json()); app.use(cors()); app.use(morgan('combined')); app.use(errorHandler); +app.use(rateLimiter); +app.use(compression); app.use('/', rootRouter); app.use('/health', healthRouter); diff --git a/src/database/stores.js b/src/database/stores.js index 5ca7184..8f63412 100755 --- a/src/database/stores.js +++ b/src/database/stores.js @@ -2,7 +2,7 @@ const { getDatabase } = require('./mongo-common'); // https://docs.mongodb.com/manual/reference/method/ObjectId/ const { ObjectId } = require('mongodb'); -const getUserName = require('git-user-name'); +const getUserName = require('../utils/git-user-name'); // a "collection" in mongo is a lot like a list which is a lot like an Array const collectionName = 'stores'; diff --git a/src/midleware/compression.js b/src/midleware/compression.js new file mode 100644 index 0000000..2903485 --- /dev/null +++ b/src/midleware/compression.js @@ -0,0 +1,3 @@ +const compression = require('compression'); + +module.exports = compression(); diff --git a/src/midleware/rateLimiter.js b/src/midleware/rateLimiter.js new file mode 100644 index 0000000..50eac64 --- /dev/null +++ b/src/midleware/rateLimiter.js @@ -0,0 +1,8 @@ +const rateLimit = require('express-rate-limit'); + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per window +}); + +module.exports = limiter; \ No newline at end of file diff --git a/src/utils/git-user-name.js b/src/utils/git-user-name.js new file mode 100644 index 0000000..272da89 --- /dev/null +++ b/src/utils/git-user-name.js @@ -0,0 +1,12 @@ +const { execSync } = require('child_process'); + +function getGitUserName() { + try { + const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); + return name || 'unknown'; + } catch (err) { + return 'unknown'; + } +} + +module.exports = getGitUserName; \ No newline at end of file From d0f3ed22168f9d831273dc6b3b1e806bd0ff10b5 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 12:55:00 -0400 Subject: [PATCH 12/63] Hooked into SIGINT and SIGTERM signals to gracefully close the HTTP server and MongoDB connection using stopDatabase(). This improves stability in dev and production environments. --- src/database/mongo-common.js | 38 ++++++++++++++++++++++++++++-------- src/index.js | 30 ++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/database/mongo-common.js b/src/database/mongo-common.js index baf66bb..4efd027 100755 --- a/src/database/mongo-common.js +++ b/src/database/mongo-common.js @@ -1,23 +1,45 @@ /** - All configuration that is required for a shared mongo server hosted in the cloud + * Shared MongoDB configuration for cloud-hosted MongoDB instance. + * Documentation: https://mongodb.github.io/node-mongodb-native/6.16/classes/MongoClient.html */ + const { MongoClient } = require('mongodb'); -let database = null; const mongoDBURL = process.env.MONGO_URL; +if (!mongoDBURL) { + throw new Error('MONGO_URL environment variable is not set'); +} + +let client; +let database; + async function startDatabase() { - const client = new MongoClient(mongoDBURL); - const connection = await client.connect(); - database = connection.db(); + if (client && client.topology?.isConnected?.()) { + return database; // already connected + } + + client = new MongoClient(mongoDBURL); + + await client.connect(); + database = client.db(); + return database; } async function getDatabase() { - if (!database) await startDatabase(); - return database; + return database || startDatabase(); +} + +async function stopDatabase() { + if (client) { + await client.close(); + client = null; + database = null; + } } module.exports = { getDatabase, startDatabase, -}; + stopDatabase, +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 3282a95..f22ed91 100755 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,11 @@ const app = require('./app.js'); -const { startDatabase } = require('./database/mongo-common.js'); +const { startDatabase, stopDatabase } = require('./database/mongo-common.js'); const PORT = process.env.PORT || 3001; const MONGO_URL = process.env.MONGO_URL; +let server; + async function startServer() { if (!MONGO_URL) { // Gracefully handle missing DB config @@ -13,7 +15,7 @@ async function startServer() { }); }); - app.listen(PORT, () => { + server = app.listen(PORT, () => { console.log(`Server running without DB on port ${PORT}`); }); @@ -22,13 +24,33 @@ async function startServer() { try { await startDatabase(); - app.listen(PORT, () => { + + server = app.listen(PORT, () => { console.log(`Server started on port ${PORT}`); }); } catch (err) { console.error('Failed to start database:', err); - process.exit(1); // Exit if DB connection fails + process.exit(1); } } +function gracefulShutdown(signal) { + console.log(`\nReceived ${signal}, shutting down...`); + if (server) { + server.close(async () => { + console.log('HTTP server closed'); + await stopDatabase(); + console.log('Database connection closed'); + process.exit(0); + }); + } else { + process.exit(0); + } +} + +// Listen for shutdown signals +['SIGINT', 'SIGTERM'].forEach(signal => { + process.on(signal, () => gracefulShutdown(signal)); +}); + startServer(); \ No newline at end of file From a431cae28e87db71453c414d35ddcc4d09109ef6 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 13:29:14 -0400 Subject: [PATCH 13/63] all tests passing --- package.json | 1 + src/database/stores.js | 38 +++++++++---------- src/http_tests/app.test.js | 2 + src/http_tests/stores.http | 4 +- src/http_tests/stores.test.js | 70 +++++++++++++++++++++++++++++++++++ src/routes/storesRoutes.js | 17 ++++----- 6 files changed, 100 insertions(+), 32 deletions(-) create mode 100644 src/http_tests/stores.test.js diff --git a/package.json b/package.json index 1973be5..bb19846 100755 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "body-parser": "^1.18.3", "compression": "^1.8.0", "cors": "^2.8.5", + "dotenv": "^16.5.0", "env-cmd": "^10.1.0", "express": "^4.16.4", "express-rate-limit": "^7.5.0", diff --git a/src/database/stores.js b/src/database/stores.js index 8f63412..b152f0f 100755 --- a/src/database/stores.js +++ b/src/database/stores.js @@ -1,59 +1,57 @@ const { getDatabase } = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ const { ObjectId } = require('mongodb'); - const getUserName = require('../utils/git-user-name'); -// a "collection" in mongo is a lot like a list which is a lot like an Array const collectionName = 'stores'; async function createStore(store) { const database = await getDatabase(); - store.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ + store.addedBy = getUserName(); + const { insertedId } = await database.collection(collectionName).insertOne(store); - return insertedId; + + return await database.collection(collectionName).findOne({ + _id: insertedId, + }); } async function getStores() { const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find return await database.collection(collectionName).find({}).toArray(); } async function deleteStore(_id) { const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - const restuls = await database.collection(collectionName).deleteOne({ - _id: ObjectId.createFromHexString(_id), + + const result = await database.collection(collectionName).deleteOne({ + _id: ObjectId.createFromHexString(_id), // Simplified ObjectId creation }); - if (restuls.deletedCount === 0) { - return "No store found with that id"; + if (result.deletedCount === 0) { + return { message: "No store found with that id" }; // Return an object with a message } - return "Store deleted"; + return { message: "Store deleted" }; // Return an object with a message } async function updateStore(id, store) { const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete delete store._id; - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: ObjectId.createFromHexString(id) }, + await database.collection(collectionName).updateOne( + { _id: ObjectId.createFromHexString(id) }, // Simplified ObjectId creation { $set: { ...store, }, }, ); + + // Return the updated store + const updatedStore = await database.collection(collectionName).findOne({ _id: ObjectId.createFromHexString(id) }); + return updatedStore; } -// export the functions that can be used by the main app code module.exports = { createStore, getStores, diff --git a/src/http_tests/app.test.js b/src/http_tests/app.test.js index dc77872..0a761c5 100644 --- a/src/http_tests/app.test.js +++ b/src/http_tests/app.test.js @@ -1,3 +1,5 @@ +require('dotenv').config(); + const request = require('supertest'); const app = require('../app'); diff --git a/src/http_tests/stores.http b/src/http_tests/stores.http index c4193ce..bbceeef 100755 --- a/src/http_tests/stores.http +++ b/src/http_tests/stores.http @@ -9,8 +9,8 @@ POST http://localhost:3001/stores Content-Type: application/json { - "Store Profile": "Nevada Golf Emprium", - "Shipping Info": "99 Nowhere Drive, Nevada" + "store_profile": "Nevada Golf Emprium", + "shipping_address": "99 Nowhere Drive, Nevada" } ### Test the PUT which should change a store diff --git a/src/http_tests/stores.test.js b/src/http_tests/stores.test.js new file mode 100644 index 0000000..f0c11cc --- /dev/null +++ b/src/http_tests/stores.test.js @@ -0,0 +1,70 @@ +require('dotenv').config(); + +const request = require('supertest'); +const app = require('../app'); +const { stopDatabase } = require('../database/mongo-common'); + +describe('Store "Collections" Endpoint', () => { + + afterAll(async () => { + await stopDatabase(); // Ensure database connection is closed + }); + + // Test POST /stores to create a new store + it('should create a new store', async () => { + const storeData = { + store_profile: 'Nevada Golf Emprium', + shipping_address: '99 Nowhere Drive, Nevada', + }; + + const res = await request(app) + .post('/stores') + .send(storeData) + .set('Content-Type', 'application/json'); + + expect(res.statusCode).toEqual(201); // Expecting 201 Created + expect(res.body.store_profile).toBe(storeData.store_profile); + expect(res.body.shipping_address).toBe(storeData.shipping_address); + }); + + // Test GET /stores to fetch all stores + it('should return a list of stores', async () => { + const res = await request(app).get('/stores'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); // Expecting an array of stores + // should contain at least one store + expect(res.body.length).toBeGreaterThan(0); + }); + + + + // Test PUT /stores/:id to update an existing store + it('should update an existing store', async () => { + const stores = await request(app).get('/stores'); + const storeId = stores.body[0]._id; + const updatedData = { + metadata: '68203238d1857e2fae0b6093', + }; + + const res = await request(app) + .put(`/stores/${storeId}`) + .send(updatedData) + .set('Content-Type', 'application/json'); + + expect(res.statusCode).toEqual(200); + expect(res.body.metadata).toBe(updatedData.metadata); + }); + + // Test DELETE /stores/:id to delete a store + it('should delete a store', async () => { + const stores = await request(app).get('/stores'); + const storeId = stores.body[0]._id; // Get the ID of the first store + expect(stores.body.length).toBeGreaterThan(0); + + const res = await request(app).delete(`/stores/${storeId}`); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toBe('Store deleted'); // Ensure that the response contains the message + }); + +}); diff --git a/src/routes/storesRoutes.js b/src/routes/storesRoutes.js index 33a24cc..1aa5153 100755 --- a/src/routes/storesRoutes.js +++ b/src/routes/storesRoutes.js @@ -1,6 +1,6 @@ const router = require('express').Router(); -const {deleteStore, updateStore, createStore, getStores} = require('../database/stores'); +const { deleteStore, updateStore, createStore, getStores } = require('../database/stores'); router.get('/', async (req, res) => { res.send(await getStores()); @@ -8,23 +8,20 @@ router.get('/', async (req, res) => { router.post('/', async (apiRequest, apiResponse) => { const newStore = apiRequest.body; - await createStore(newStore); - apiResponse.send({ - message: 'New Store created.', - allStores: await getStores(), - }); + + apiResponse.status(201).send(await createStore(newStore)); }); router.delete('/:_id', async (apiRequest, apiResponse) => { - apiResponse.send({ message: await deleteStore(apiRequest.params._id) }); + apiResponse.send(await deleteStore(apiRequest.params._id)); }); // endpoint to update a Store router.put('/:_id', async (apiRequest, apiResponse) => { const updatedStore = apiRequest.body; - console.log({ updateStore }) - await updateStore(apiRequest.params._id, updatedStore); - apiResponse.send({ message: 'Store updated.' }); + + apiResponse.send( + await updateStore(apiRequest.params._id, updatedStore)); }); module.exports = router; \ No newline at end of file From 08ec0305ecc8df80aa335147a24772879d3cde0e Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 13:39:59 -0400 Subject: [PATCH 14/63] feat: set up for testing --- package.json | 1 + src/database/mongo-common.js | 31 +++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index bb19846..64a622d 100755 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "devDependencies": { "jest": "^29.7.0", + "mongodb-memory-server": "^10.1.4", "supertest": "^7.1.0" } } diff --git a/src/database/mongo-common.js b/src/database/mongo-common.js index 4efd027..72870c4 100755 --- a/src/database/mongo-common.js +++ b/src/database/mongo-common.js @@ -4,23 +4,41 @@ */ const { MongoClient } = require('mongodb'); +const { MongoMemoryServer } = require('mongodb-memory-server'); const mongoDBURL = process.env.MONGO_URL; -if (!mongoDBURL) { +if (!mongoDBURL && process.env.NODE_ENV !== 'test') { throw new Error('MONGO_URL environment variable is not set'); } let client; let database; +let mongoServer; // store reference to in-memory server for shutdown -async function startDatabase() { +const getRightMongoDBURL = async () => { + const env = process.env.NODE_ENV; + + if (env === 'test') { + mongoServer = await MongoMemoryServer.create(); + return mongoServer.getUri(); + } + + if (['development' ,'production'].includes(env)) { + return mongoDBURL; + } + + throw new Error(`Unsupported NODE_ENV: ${env}`); +}; + +async function startDatabase(uri = null) { if (client && client.topology?.isConnected?.()) { - return database; // already connected + return database; } - client = new MongoClient(mongoDBURL); + const dbURI = uri || await getRightMongoDBURL(); + client = new MongoClient(dbURI); await client.connect(); database = client.db(); return database; @@ -36,6 +54,11 @@ async function stopDatabase() { client = null; database = null; } + + if (mongoServer) { + await mongoServer.stop(); + mongoServer = null; + } } module.exports = { From c573c3a7bd237f0a540e43699b42bfaccd9c8e87 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 13:44:00 -0400 Subject: [PATCH 15/63] enhance documentation --- .env-sample | 1 - .env.sample | 19 +++++++++++++++++++ package.json | 15 +++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) delete mode 100644 .env-sample create mode 100644 .env.sample diff --git a/.env-sample b/.env-sample deleted file mode 100644 index 6339c3a..0000000 --- a/.env-sample +++ /dev/null @@ -1 +0,0 @@ -MONGO_URL='mongodb://mlab2020:abc123def!@ds031617.mlab.com:31617/learningmongo' \ No newline at end of file diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..f433206 --- /dev/null +++ b/.env.sample @@ -0,0 +1,19 @@ +# 📦 .env.sample +# Copy this file to .env and fill in the actual values +# Command: cp .env.sample .env + +# === MongoDB Configuration === + +# MongoDB connection URI for development/production environments. +# Format: mongodb://:@:/ +# Example: mongodb://user:pass@localhost:27017/mydatabase +MONGO_URL='mongodb://your_username:your_password@host:port/database_name' + +# Set the environment +# NODE_ENV can be 'development', 'production', or 'test' +NODE_ENV=development + +# === Notes === +# Do NOT use real credentials in this file. +# In production, make sure this file is excluded from version control. +# For test, the in-memory MongoDB server will be used automatically if NODE_ENV=test diff --git a/package.json b/package.json index 64a622d..f8c878c 100755 --- a/package.json +++ b/package.json @@ -1,20 +1,23 @@ { - "name": "week-10", - "version": "1.0.0", + "name": "backend-api", + "version": "1.0.1", "description": "", "main": "./src/index.js", "scripts": { "start": "node ./src/index.js", "dev": "env-cmd nodemon ./src/index.js", - "test": "jest", - "zip-for-aws": "node make-zip-for-aws.js" + "test": "jest" }, - "keywords": [], + "keywords": [ + "mongodb", + "express", + "api", + "rest" + ], "author": "", "license": "ISC", "dependencies": { "archiver": "^5.0.2", - "body-parser": "^1.18.3", "compression": "^1.8.0", "cors": "^2.8.5", "dotenv": "^16.5.0", From cdf05fc7c3f4d815b3a88eae489e098828fdb990 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 14:01:52 -0400 Subject: [PATCH 16/63] enhance documentation, refactor code --- .env.sample | 8 ++++++++ README.md | 11 +++++++---- src/app.js | 23 ++++++++++++++--------- src/midleware/cors.js | 21 +++++++++++++++++++++ src/midleware/helmet.js | 21 +++++++++++++++++++++ src/midleware/json.js | 15 +++++++++++++++ src/midleware/morgan.js | 5 +++++ 7 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 src/midleware/cors.js create mode 100644 src/midleware/helmet.js create mode 100644 src/midleware/json.js create mode 100644 src/midleware/morgan.js diff --git a/.env.sample b/.env.sample index f433206..a685359 100644 --- a/.env.sample +++ b/.env.sample @@ -17,3 +17,11 @@ NODE_ENV=development # Do NOT use real credentials in this file. # In production, make sure this file is excluded from version control. # For test, the in-memory MongoDB server will be used automatically if NODE_ENV=test + +# === CORS Configuration === +# CORS (Cross-Origin Resource Sharing) settings +# By default, CORS is disabled. +# Default methods: GET, POST, PUT, DELETE +ALLOWED_ORIGINS= +ALLOWED_METHODS= +ALLOWED_HEADERS= \ No newline at end of file diff --git a/README.md b/README.md index 6f2eec8..48fbada 100755 --- a/README.md +++ b/README.md @@ -8,13 +8,16 @@ Environmental Variables: 2. PORT (optional) ## Getting Started +1. Copy this file to .env and fill in the actual values +```bash +cp .env.sample .env +``` -Scripts: +2. Run a script: ```json "scripts": { "start": "node ./src/index.js", "dev": "env-cmd nodemon ./src/index.js", - "test": "echo \"Error: no test specified\" && exit 1", - "zip-for-aws": "node make-zip-for-aws.js" - }, + "test": "echo \"Error: no test specified\" && exit 1" + } ``` \ No newline at end of file diff --git a/src/app.js b/src/app.js index 7c3cffd..82ed849 100644 --- a/src/app.js +++ b/src/app.js @@ -1,26 +1,31 @@ const express = require('express'); -const cors = require('cors'); -const helmet = require('helmet'); -const morgan = require('morgan'); +const app = express(); +// Middleware const errorHandler = require('./midleware/errorHandler'); const rateLimiter = require('./midleware/rateLimiter'); const compression = require('./midleware/compression'); +const helmet = require('./midleware/helmet') +const json = require('./midleware/json'); +const cors = require('./midleware/cors') +const morgan = require('./midleware/morgan'); + +// Routers const notFoundRouter = require('./routes/notFoundRoute') const healthRouter = require('./routes/healthRoute') const storesRouter = require('./routes/storesRoutes'); const rootRouter = require('./routes/rootRoute'); -const app = express(); - -app.use(helmet()); -app.use(express.json()); -app.use(cors()); -app.use(morgan('combined')); +// Apply Middleware +app.use(helmet); +app.use(json); +app.use(cors); +app.use(morgan); app.use(errorHandler); app.use(rateLimiter); app.use(compression); +// Set Routes app.use('/', rootRouter); app.use('/health', healthRouter); app.use('/stores', storesRouter); diff --git a/src/midleware/cors.js b/src/midleware/cors.js new file mode 100644 index 0000000..0e383f1 --- /dev/null +++ b/src/midleware/cors.js @@ -0,0 +1,21 @@ +const cors = require('cors'); + +const ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || ''; +const ALLOWED_METHODS = process.env.CORS_ALLOWED_METHODS || 'GET,POST,PUT,DELETE'; +const ALLOWED_HEADERS = process.env.CORS_ALLOWED_HEADERS || 'Content-Type,Authorization'; + +const corsOptions = { + origin: (origin, callback) => { + const allowedOrigins = ALLOWED_ORIGINS.split(',').map(o => o.trim()).filter(Boolean); + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + methods: ALLOWED_METHODS.split(',').map(m => m.trim()).filter(Boolean), + allowedHeaders: ALLOWED_HEADERS.split(',').map(h => h.trim()).filter(Boolean), + credentials: true, +}; + +module.exports = cors(corsOptions); \ No newline at end of file diff --git a/src/midleware/helmet.js b/src/midleware/helmet.js new file mode 100644 index 0000000..fe7be42 --- /dev/null +++ b/src/midleware/helmet.js @@ -0,0 +1,21 @@ +const helmet = require('helmet'); + +const configureHelmet = () => + helmet({ + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false, + crossOriginResourcePolicy: false, + referrerPolicy: { policy: 'no-referrer' }, + expectCt: false, + frameguard: false, + hidePoweredBy: true, + hsts: { + maxAge: 63072000, // 2 years + includeSubDomains: true, + preload: true, + }, + noSniff: true, + // xssFilter: true, // Removed in modern Helmet versions + }); + +module.exports = configureHelmet(); diff --git a/src/midleware/json.js b/src/midleware/json.js new file mode 100644 index 0000000..55685cf --- /dev/null +++ b/src/midleware/json.js @@ -0,0 +1,15 @@ +const { json } = require('express'); + +const configuredJson = json({ + limit: '1mb', + strict: false, + type: ['application/json', 'application/vnd.api+json'], + reviver: (key, value) => { + if (key === 'date' && typeof value === 'string') { + return new Date(value); + } + return value; + }, +}); + +module.exports = configuredJson; \ No newline at end of file diff --git a/src/midleware/morgan.js b/src/midleware/morgan.js new file mode 100644 index 0000000..2e37ea4 --- /dev/null +++ b/src/midleware/morgan.js @@ -0,0 +1,5 @@ +const morgan = require('morgan'); + +const configureMorgan = morgan('dev'); + +module.exports = configureMorgan; \ No newline at end of file From b21bd4fd56de0edfe8ddd32766f460b115d7716c Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 14:03:56 -0400 Subject: [PATCH 17/63] Create tests.yml --- .github/workflows/tests.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2f1574b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm test + env: + NODE_ENV: test From efb59eeebd1ac56ad224b33d65f51b5b56a1d712 Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 14:05:18 -0400 Subject: [PATCH 18/63] Update tests.yml --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2f1574b..c5e3053 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,6 +25,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' + - run: npm install - run: npm ci - run: npm test env: From d9653890bb8e12d11d1545c1a92ec68c226ffe73 Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 14:07:47 -0400 Subject: [PATCH 19/63] Update tests.yml --- .github/workflows/tests.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c5e3053..5df0c28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,11 +1,6 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - name: Node.js CI on: - push: - branches: [ "master" ] pull_request: branches: [ "master" ] @@ -26,7 +21,6 @@ jobs: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm install - - run: npm ci - run: npm test env: NODE_ENV: test From 3bab518614ffe9df051ec54f4ae086b2d7dbaaa8 Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 14:10:24 -0400 Subject: [PATCH 20/63] Update tests.yml --- .github/workflows/tests.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5df0c28..ee4f11a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,16 +11,24 @@ jobs: strategy: matrix: node-version: [18.x, 20.x, 22.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm install - - run: npm test + + # Install dependencies (without using lock file) + - name: Install dependencies + run: npm install + + # Ensure consistent installs with npm ci + - name: Run npm ci (ensure clean node_modules) + run: npm ci + + - name: Run tests + run: npm test env: NODE_ENV: test From 7b13a12d8d79e5fcaf0292baaf5716d3b4e0f850 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 14:12:55 -0400 Subject: [PATCH 21/63] update documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48fbada..b9465d7 100755 --- a/README.md +++ b/README.md @@ -18,6 +18,6 @@ cp .env.sample .env "scripts": { "start": "node ./src/index.js", "dev": "env-cmd nodemon ./src/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest" } ``` \ No newline at end of file From 2174ccf1547059f75bcabba6f4db727efa556ba9 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 14:14:41 -0400 Subject: [PATCH 22/63] add test badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b9465d7..e09749c 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Node.js and Express Backend +[![Node.js CI](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml) ## Requirements Identify your MongoDB URL. Visit MongoDB to sign up and get started. From eb8ede4ecd35735da1771937befce64f7bfa0e2d Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 14:20:23 -0400 Subject: [PATCH 23/63] update badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e09749c..25eb6bb 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Node.js and Express Backend -[![Node.js CI](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml) +[![Backend API - CI Tests](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml) ## Requirements Identify your MongoDB URL. Visit MongoDB to sign up and get started. From c35aea08bc229e447a60e440e1952158b3e8f843 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 14:24:38 -0400 Subject: [PATCH 24/63] include CORS variables --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 25eb6bb..0b2839e 100755 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Identify your MongoDB URL. Visit MongoDB to sign up and get started. Environmental Variables: 1. MONGO_URL 2. PORT (optional) +3. ALLOWED_ORIGINS (optional) +4. ALLOWED_METHODS (optional) +5. ALLOWED_HEADERS (optional) ## Getting Started 1. Copy this file to .env and fill in the actual values @@ -14,7 +17,7 @@ Environmental Variables: cp .env.sample .env ``` -2. Run a script: +1. Run a script: ```json "scripts": { "start": "node ./src/index.js", From c1ef70f23171b8a15f0868a2089c34bdc729c0b3 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 17:35:09 -0400 Subject: [PATCH 25/63] uninstall archiver due to no use and vulnerability --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index f8c878c..48da781 100755 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "author": "", "license": "ISC", "dependencies": { - "archiver": "^5.0.2", "compression": "^1.8.0", "cors": "^2.8.5", "dotenv": "^16.5.0", From 6078ad2aaaf39387b90fff6539e685e2fe232e99 Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 17:41:15 -0400 Subject: [PATCH 26/63] Create snyk.yml --- .github/workflows/snyk.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/snyk.yml diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 0000000..1fab28d --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,30 @@ +name: Snyk Security Scan + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +jobs: + snyk: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 # Adjust as needed + + - name: Install dependencies + run: npm install + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@v1 + with: + args: test + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} From 1b7e5572d924d67f146a2ddb43b37d6f930fd82b Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 17:44:15 -0400 Subject: [PATCH 27/63] remove unused npm package --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 48da781..aa58ba3 100755 --- a/package.json +++ b/package.json @@ -26,8 +26,7 @@ "helmet": "^3.15.1", "mongodb": "^6.16.0", "morgan": "^1.9.1", - "nodemon": "^3.1.10", - "zip-a-folder": "0.0.12" + "nodemon": "^3.1.10" }, "devDependencies": { "jest": "^29.7.0", From aff2c58ef3253f928de70742ede55523dd022d4b Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 17:45:41 -0400 Subject: [PATCH 28/63] Update snyk.yml --- .github/workflows/snyk.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 1fab28d..2549b18 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -17,13 +17,13 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 # Adjust as needed + node-version: 20 - name: Install dependencies run: npm install - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/node@v1 + uses: snyk/actions/node@0.4.0 with: args: test env: From 1cbb80e0b5f0df08bbd8afd6202bbb4989a8b471 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 17:53:21 -0400 Subject: [PATCH 29/63] remove duplicate setup --- .github/workflows/snyk.yml | 30 ------------------------------ .github/workflows/tests.yml | 6 ++++-- 2 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 .github/workflows/snyk.yml diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml deleted file mode 100644 index 2549b18..0000000 --- a/.github/workflows/snyk.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Snyk Security Scan - -on: - push: - branches: ["master"] - pull_request: - branches: ["master"] - -jobs: - snyk: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install dependencies - run: npm install - - - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/node@0.4.0 - with: - args: test - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee4f11a..f617707 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,8 +1,10 @@ -name: Node.js CI +name: Backend Unit Tests on: + push: + branches: ["master"] pull_request: - branches: [ "master" ] + branches: ["master"] jobs: build: From ffc14a0328a259e3d4ad22e3ed536202d70a56fb Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 18:01:30 -0400 Subject: [PATCH 30/63] chore(security): disable x-powered-by header to reduce information exposure --- src/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.js b/src/app.js index 82ed849..aff46fe 100644 --- a/src/app.js +++ b/src/app.js @@ -16,6 +16,10 @@ const healthRouter = require('./routes/healthRoute') const storesRouter = require('./routes/storesRoutes'); const rootRouter = require('./routes/rootRoute'); +// Disable Express identifying header +// SNYK CODE: CWE-200 +app.disable('x-powered-by'); + // Apply Middleware app.use(helmet); app.use(json); From e57fa14d7fb051a909020d49c606cf87b77903dc Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 18:04:00 -0400 Subject: [PATCH 31/63] send back json to prevent xss --- src/routes/storesRoutes.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/storesRoutes.js b/src/routes/storesRoutes.js index 1aa5153..4fbe53f 100755 --- a/src/routes/storesRoutes.js +++ b/src/routes/storesRoutes.js @@ -3,24 +3,24 @@ const router = require('express').Router(); const { deleteStore, updateStore, createStore, getStores } = require('../database/stores'); router.get('/', async (req, res) => { - res.send(await getStores()); + res.json(await getStores()); }); router.post('/', async (apiRequest, apiResponse) => { const newStore = apiRequest.body; - apiResponse.status(201).send(await createStore(newStore)); + apiResponse.status(201).json(await createStore(newStore)); }); router.delete('/:_id', async (apiRequest, apiResponse) => { - apiResponse.send(await deleteStore(apiRequest.params._id)); + apiResponse.json(await deleteStore(apiRequest.params._id)); }); // endpoint to update a Store router.put('/:_id', async (apiRequest, apiResponse) => { const updatedStore = apiRequest.body; - apiResponse.send( + apiResponse.json( await updateStore(apiRequest.params._id, updatedStore)); }); From 9d15a1ce8f06c5fad6fe0ca91b61dd5a8fcf5c21 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 18:07:56 -0400 Subject: [PATCH 32/63] add snyk badge; --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0b2839e..4e292e1 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Node.js and Express Backend [![Backend API - CI Tests](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml) +[![Known Vulnerabilities](https://snyk.io/test/github/pakeku/backend-api/badge.svg)](https://snyk.io/test/github/pakeku/backend-api) ## Requirements Identify your MongoDB URL. Visit MongoDB to sign up and get started. From b52c32772a4752f13b25a8135aa29f7fe44628d4 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 18:32:05 -0400 Subject: [PATCH 33/63] add authentication with jwt --- package.json | 2 + src/app.js | 2 + src/http_tests/authentication.test.js | 35 +++++++++++++++ src/routes/authRoute.js | 63 +++++++++++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 src/http_tests/authentication.test.js create mode 100644 src/routes/authRoute.js diff --git a/package.json b/package.json index aa58ba3..977052a 100755 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "author": "", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "compression": "^1.8.0", "cors": "^2.8.5", "dotenv": "^16.5.0", @@ -24,6 +25,7 @@ "express": "^4.16.4", "express-rate-limit": "^7.5.0", "helmet": "^3.15.1", + "jsonwebtoken": "^9.0.2", "mongodb": "^6.16.0", "morgan": "^1.9.1", "nodemon": "^3.1.10" diff --git a/src/app.js b/src/app.js index aff46fe..5d72331 100644 --- a/src/app.js +++ b/src/app.js @@ -15,6 +15,7 @@ const notFoundRouter = require('./routes/notFoundRoute') const healthRouter = require('./routes/healthRoute') const storesRouter = require('./routes/storesRoutes'); const rootRouter = require('./routes/rootRoute'); +const authRouter = require('./routes/authRoute') // Disable Express identifying header // SNYK CODE: CWE-200 @@ -33,6 +34,7 @@ app.use(compression); app.use('/', rootRouter); app.use('/health', healthRouter); app.use('/stores', storesRouter); +app.use('/auth', authRouter); app.use('*', notFoundRouter); module.exports = app; \ No newline at end of file diff --git a/src/http_tests/authentication.test.js b/src/http_tests/authentication.test.js new file mode 100644 index 0000000..9aea42a --- /dev/null +++ b/src/http_tests/authentication.test.js @@ -0,0 +1,35 @@ +require('dotenv').config(); + +const request = require('supertest'); +const app = require('../app'); +const { stopDatabase } = require('../database/mongo-common'); + +describe('Authentication JWT', () => { + afterAll(async () => { + await stopDatabase(); // Ensure database connection is closed + }); + + const testUser = { + email: 'testuser@example.com', + password: 'SecurePass123!', + }; + + it('should register a new user', async () => { + const res = await request(app) + .post('/auth/register') + .send(testUser) + .expect(201); + + expect(res.body).toHaveProperty('message'); + }); + + it('should login with valid credentials', async () => { + const res = await request(app) + .post('/auth/login') + .send(testUser) + .expect(200); + + expect(res.body).toHaveProperty('token'); + expect(typeof res.body.token).toBe('string'); + }); +}); diff --git a/src/routes/authRoute.js b/src/routes/authRoute.js new file mode 100644 index 0000000..2d14091 --- /dev/null +++ b/src/routes/authRoute.js @@ -0,0 +1,63 @@ +const express = require('express'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const { getDatabase } = require('../database/mongo-common'); + +const router = express.Router(); +const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret'; + +router.post('/register', async (req, res) => { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ message: 'Username and password are required' }); + } + + try { + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const existingUser = await usersCollection.findOne({ email }); + if (existingUser) { + return res.status(409).json({ message: 'Username already taken' }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + await usersCollection.insertOne({ email, password: hashedPassword }); + + res.status(201).json({ message: 'User registered successfully' }); + } catch (error) { + console.error('Error registering user:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}); + +router.post('/login', async (req, res) => { + const { email, password } = req.body; + + try { + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const user = await usersCollection.findOne({ email }); + if (!user) { + return res.status(401).json({ message: 'Invalid credentials' }); + } + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + return res.status(401).json({ message: 'Invalid credentials' }); + } + + const token = jwt.sign({ userId: user._id, email: user.email }, JWT_SECRET, { + expiresIn: '1h', + }); + + res.status(200).json({ message: 'User logged in successfully', token }); + } catch (error) { + console.error('Error logging in:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}); + +module.exports = router; From 726790718ce3bb81427c633b262d6682cfb6ceca Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 19:47:42 -0400 Subject: [PATCH 34/63] Update src/routes/authRoute.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/routes/authRoute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/authRoute.js b/src/routes/authRoute.js index 2d14091..1d8127c 100644 --- a/src/routes/authRoute.js +++ b/src/routes/authRoute.js @@ -19,7 +19,7 @@ router.post('/register', async (req, res) => { const existingUser = await usersCollection.findOne({ email }); if (existingUser) { - return res.status(409).json({ message: 'Username already taken' }); + return res.status(409).json({ message: 'Email already taken' }); } const hashedPassword = await bcrypt.hash(password, 10); From 84896cc0d967199f4c1f62084700259016816380 Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sun, 11 May 2025 19:47:56 -0400 Subject: [PATCH 35/63] Update src/routes/authRoute.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/routes/authRoute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/authRoute.js b/src/routes/authRoute.js index 1d8127c..f461edf 100644 --- a/src/routes/authRoute.js +++ b/src/routes/authRoute.js @@ -10,7 +10,7 @@ router.post('/register', async (req, res) => { const { email, password } = req.body; if (!email || !password) { - return res.status(400).json({ message: 'Username and password are required' }); + return res.status(400).json({ message: 'Email and password are required' }); } try { From 58539f8de2dbae694e38421d01e1a1292e413f06 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 11 May 2025 21:08:21 -0400 Subject: [PATCH 36/63] feat: migrate to ts --- .vscode/settings.json | 6 ++ jest.config.js | 7 ++ package.json | 23 +++++- src/app.js | 40 ---------- src/app.ts | 38 ++++++++++ .../{mongo-common.js => mongo-common.ts} | 33 ++++----- src/database/stores.js | 60 --------------- src/database/stores.ts | 74 +++++++++++++++++++ src/http_tests/app.test.js | 27 ------- src/http_tests/app.test.ts | 25 +++++++ ...ication.test.js => authentication.test.ts} | 9 +-- .../{stores.test.js => stores.test.ts} | 33 +++++---- src/{index.js => index.ts} | 17 +++-- src/midleware/compression.js | 3 - src/midleware/compression.ts | 5 ++ src/midleware/{cors.js => cors.ts} | 12 +-- src/midleware/errorHandler.js | 17 ----- src/midleware/errorHandler.ts | 14 ++++ src/midleware/helmet.js | 21 ------ src/midleware/helmet.ts | 18 +++++ src/midleware/{json.js => json.ts} | 7 +- src/midleware/morgan.js | 5 -- src/midleware/morgan.ts | 6 ++ .../{rateLimiter.js => rateLimiter.ts} | 4 +- src/routes/authRoute.js | 63 ---------------- src/routes/authRoute.ts | 72 ++++++++++++++++++ src/routes/healthRoute.js | 11 --- src/routes/healthRoute.ts | 13 ++++ src/routes/notFoundRoute.js | 12 --- src/routes/notFoundRoute.ts | 14 ++++ src/routes/rootRoute.js | 7 -- src/routes/rootRoute.ts | 9 +++ src/routes/storesRoute.ts | 30 ++++++++ src/routes/storesRoutes.js | 27 ------- .../{git-user-name.js => git-user-name.ts} | 6 +- tsconfig.json | 24 ++++++ 36 files changed, 435 insertions(+), 357 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 jest.config.js delete mode 100644 src/app.js create mode 100644 src/app.ts rename src/database/{mongo-common.js => mongo-common.ts} (57%) mode change 100755 => 100644 delete mode 100755 src/database/stores.js create mode 100644 src/database/stores.ts delete mode 100644 src/http_tests/app.test.js create mode 100644 src/http_tests/app.test.ts rename src/http_tests/{authentication.test.js => authentication.test.ts} (82%) rename src/http_tests/{stores.test.js => stores.test.ts} (71%) rename src/{index.js => index.ts} (71%) mode change 100755 => 100644 delete mode 100644 src/midleware/compression.js create mode 100644 src/midleware/compression.ts rename src/midleware/{cors.js => cors.ts} (64%) delete mode 100644 src/midleware/errorHandler.js create mode 100644 src/midleware/errorHandler.ts delete mode 100644 src/midleware/helmet.js create mode 100644 src/midleware/helmet.ts rename src/midleware/{json.js => json.ts} (61%) delete mode 100644 src/midleware/morgan.js create mode 100644 src/midleware/morgan.ts rename src/midleware/{rateLimiter.js => rateLimiter.ts} (64%) delete mode 100644 src/routes/authRoute.js create mode 100644 src/routes/authRoute.ts delete mode 100644 src/routes/healthRoute.js create mode 100644 src/routes/healthRoute.ts delete mode 100644 src/routes/notFoundRoute.js create mode 100644 src/routes/notFoundRoute.ts delete mode 100644 src/routes/rootRoute.js create mode 100644 src/routes/rootRoute.ts create mode 100644 src/routes/storesRoute.ts delete mode 100755 src/routes/storesRoutes.js rename src/utils/{git-user-name.js => git-user-name.ts} (63%) create mode 100644 tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f4e35a9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "accessibility.signals.chatRequestSent": { + "sound": "off", + "announcement": "off" + } +} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..9fc603e --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +export default { + testEnvironment: "node", + transform: { + "^.+\.tsx?$": ["ts-jest",{}], + }, +}; \ No newline at end of file diff --git a/package.json b/package.json index 977052a..a816f2b 100755 --- a/package.json +++ b/package.json @@ -2,10 +2,12 @@ "name": "backend-api", "version": "1.0.1", "description": "", - "main": "./src/index.js", + "main": "./src/index.ts", + "type": "module", "scripts": { - "start": "node ./src/index.js", - "dev": "env-cmd nodemon ./src/index.js", + "build": "tsc", + "start": "node dist/index.js", + "dev": "env-cmd ts-node src/index.ts", "test": "jest" }, "keywords": [ @@ -31,8 +33,21 @@ "nodemon": "^3.1.10" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.1", + "@types/helmet": "^0.0.48", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.9", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.17", + "@types/supertest": "^6.0.3", "jest": "^29.7.0", "mongodb-memory-server": "^10.1.4", - "supertest": "^7.1.0" + "supertest": "^7.1.0", + "ts-jest": "^29.3.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" } } diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 5d72331..0000000 --- a/src/app.js +++ /dev/null @@ -1,40 +0,0 @@ -const express = require('express'); -const app = express(); - -// Middleware -const errorHandler = require('./midleware/errorHandler'); -const rateLimiter = require('./midleware/rateLimiter'); -const compression = require('./midleware/compression'); -const helmet = require('./midleware/helmet') -const json = require('./midleware/json'); -const cors = require('./midleware/cors') -const morgan = require('./midleware/morgan'); - -// Routers -const notFoundRouter = require('./routes/notFoundRoute') -const healthRouter = require('./routes/healthRoute') -const storesRouter = require('./routes/storesRoutes'); -const rootRouter = require('./routes/rootRoute'); -const authRouter = require('./routes/authRoute') - -// Disable Express identifying header -// SNYK CODE: CWE-200 -app.disable('x-powered-by'); - -// Apply Middleware -app.use(helmet); -app.use(json); -app.use(cors); -app.use(morgan); -app.use(errorHandler); -app.use(rateLimiter); -app.use(compression); - -// Set Routes -app.use('/', rootRouter); -app.use('/health', healthRouter); -app.use('/stores', storesRouter); -app.use('/auth', authRouter); -app.use('*', notFoundRouter); - -module.exports = app; \ No newline at end of file diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..5eb7e6a --- /dev/null +++ b/src/app.ts @@ -0,0 +1,38 @@ +import express, { Application } from 'express'; + +import errorHandler from './midleware/errorHandler'; +import rateLimiter from './midleware/rateLimiter'; +import compression from './midleware/compression'; +import helmet from './midleware/helmet'; +import json from './midleware/json'; +import cors from './midleware/cors'; +import morgan from './midleware/morgan'; + +import notFoundRouter from './routes/notFoundRoute'; +import healthRouter from './routes/healthRoute'; +import storesRouter from './routes/storesRoute'; +import rootRouter from './routes/rootRoute'; +import authRouter from './routes/authRoute'; + +const app: Application = express(); + +// Disable Express identifying header +app.disable('x-powered-by'); + +// Apply Middleware +app.use(helmet); +app.use(json); +app.use(cors); +app.use(morgan); +app.use(errorHandler); +app.use(rateLimiter); +app.use(compression); + +// Set Routes +app.use('/', rootRouter); +app.use('/health', healthRouter); +app.use('/stores', storesRouter); +app.use('/auth', authRouter); +app.use('*', notFoundRouter); + +export default app; \ No newline at end of file diff --git a/src/database/mongo-common.js b/src/database/mongo-common.ts old mode 100755 new mode 100644 similarity index 57% rename from src/database/mongo-common.js rename to src/database/mongo-common.ts index 72870c4..ba1092b --- a/src/database/mongo-common.js +++ b/src/database/mongo-common.ts @@ -3,8 +3,8 @@ * Documentation: https://mongodb.github.io/node-mongodb-native/6.16/classes/MongoClient.html */ -const { MongoClient } = require('mongodb'); -const { MongoMemoryServer } = require('mongodb-memory-server'); +import { MongoClient, Db } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; const mongoDBURL = process.env.MONGO_URL; @@ -12,11 +12,11 @@ if (!mongoDBURL && process.env.NODE_ENV !== 'test') { throw new Error('MONGO_URL environment variable is not set'); } -let client; -let database; -let mongoServer; // store reference to in-memory server for shutdown +let client: MongoClient | null = null; +let database: Db | null = null; +let mongoServer: MongoMemoryServer | null = null; // store reference to in-memory server for shutdown -const getRightMongoDBURL = async () => { +const getRightMongoDBURL = async (): Promise => { const env = process.env.NODE_ENV; if (env === 'test') { @@ -24,15 +24,15 @@ const getRightMongoDBURL = async () => { return mongoServer.getUri(); } - if (['development' ,'production'].includes(env)) { - return mongoDBURL; + if (['development', 'production'].includes(env || '')) { + return mongoDBURL as string; } throw new Error(`Unsupported NODE_ENV: ${env}`); }; -async function startDatabase(uri = null) { - if (client && client.topology?.isConnected?.()) { +export async function startDatabase(uri: string | null = null): Promise { + if (client && database) { return database; } @@ -44,11 +44,12 @@ async function startDatabase(uri = null) { return database; } -async function getDatabase() { - return database || startDatabase(); + +export async function getDatabase(): Promise { + return database || await startDatabase(); } -async function stopDatabase() { +export async function stopDatabase(): Promise { if (client) { await client.close(); client = null; @@ -60,9 +61,3 @@ async function stopDatabase() { mongoServer = null; } } - -module.exports = { - getDatabase, - startDatabase, - stopDatabase, -}; \ No newline at end of file diff --git a/src/database/stores.js b/src/database/stores.js deleted file mode 100755 index b152f0f..0000000 --- a/src/database/stores.js +++ /dev/null @@ -1,60 +0,0 @@ -const { getDatabase } = require('./mongo-common'); -const { ObjectId } = require('mongodb'); -const getUserName = require('../utils/git-user-name'); - -const collectionName = 'stores'; - -async function createStore(store) { - const database = await getDatabase(); - store.addedBy = getUserName(); - - const { insertedId } = await database.collection(collectionName).insertOne(store); - - return await database.collection(collectionName).findOne({ - _id: insertedId, - }); -} - -async function getStores() { - const database = await getDatabase(); - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteStore(_id) { - const database = await getDatabase(); - - const result = await database.collection(collectionName).deleteOne({ - _id: ObjectId.createFromHexString(_id), // Simplified ObjectId creation - }); - - if (result.deletedCount === 0) { - return { message: "No store found with that id" }; // Return an object with a message - } - - return { message: "Store deleted" }; // Return an object with a message -} - -async function updateStore(id, store) { - const database = await getDatabase(); - delete store._id; - - await database.collection(collectionName).updateOne( - { _id: ObjectId.createFromHexString(id) }, // Simplified ObjectId creation - { - $set: { - ...store, - }, - }, - ); - - // Return the updated store - const updatedStore = await database.collection(collectionName).findOne({ _id: ObjectId.createFromHexString(id) }); - return updatedStore; -} - -module.exports = { - createStore, - getStores, - deleteStore, - updateStore, -}; diff --git a/src/database/stores.ts b/src/database/stores.ts new file mode 100644 index 0000000..67a93bd --- /dev/null +++ b/src/database/stores.ts @@ -0,0 +1,74 @@ +import { getDatabase } from './mongo-common'; +import { ObjectId } from 'mongodb'; +import getUserName from '../utils/git-user-name'; + +// Define the Store interface +interface Store { + _id?: string; + name: string; + addedBy?: string; + // add other fields relevant to your store here +} + +const collectionName = 'stores'; + +// Create a Store +async function createStore(store: Store): Promise { + const database = await getDatabase(); + store.addedBy = getUserName(); + + const storeToInsert = { ...store, _id: store._id ? new ObjectId(store._id) : undefined }; + const { insertedId } = await database.collection(collectionName).insertOne(storeToInsert); + + // Return the store document with the inserted _id + return await database.collection(collectionName).findOne({ _id: insertedId }) as Store | null; +} + +// Get all stores +async function getStores(): Promise { + const database = await getDatabase(); + const stores = await database.collection(collectionName).find({}).toArray(); + return stores.map(store => ({ + _id: store._id?.toString(), + name: store.name, + addedBy: store.addedBy, + })) as Store[]; +} + +// Delete a store by id +async function deleteStore(_id: string): Promise<{ message: string }> { + const database = await getDatabase(); + + const result = await database.collection(collectionName).deleteOne({ + _id: new ObjectId(_id), + }); + + if (result.deletedCount === 0) { + return { message: "No store found with that id" }; + } + + return { message: "Store deleted" }; +} + +// Update a store +async function updateStore(id: string, store: Partial): Promise { + const database = await getDatabase(); + delete store._id; + + await database.collection(collectionName).updateOne( + { _id: new ObjectId(id) }, + { + $set: store, + }, + ); + + // Return the updated store + const result = await database.collection(collectionName).findOne({ _id: new ObjectId(id) }); + return result ? { + _id: result._id.toString(), + name: result.name, + addedBy: result.addedBy + } as Store : null; +} + +export { createStore, getStores, deleteStore, updateStore }; \ No newline at end of file diff --git a/src/http_tests/app.test.js b/src/http_tests/app.test.js deleted file mode 100644 index 0a761c5..0000000 --- a/src/http_tests/app.test.js +++ /dev/null @@ -1,27 +0,0 @@ -require('dotenv').config(); - -const request = require('supertest'); -const app = require('../app'); - -describe('Health Check Endpoint', () => { - - it('should return 302 and redirect to /health', async () => { - const res = await request(app).get('/'); - expect(res.statusCode).toEqual(302); - expect(res.headers.location).toBe('/health'); - }) - - it('should return 200 and status OK', async () => { - const res = await request(app).get('/health'); - expect(res.statusCode).toEqual(200); - expect(res.body.status).toBe('OK'); - }); - - it('should return 404 for non-existent endpoint', async () => { - const res = await request(app).get('/non-existent'); - expect(res.statusCode).toEqual(404); - expect(res.body.message).toBe('Route not found'); - }); - - -}); diff --git a/src/http_tests/app.test.ts b/src/http_tests/app.test.ts new file mode 100644 index 0000000..35d7ab5 --- /dev/null +++ b/src/http_tests/app.test.ts @@ -0,0 +1,25 @@ +import 'dotenv/config'; +import request from 'supertest'; +import app from '../app'; + +describe('Health Check Endpoint', () => { + + it('should return 302 and redirect to /health', async () => { + const res = await request(app).get('/'); + expect(res.statusCode).toEqual(302); + expect(res.headers.location).toBe('/health'); + }); + + it('should return 200 and status OK', async () => { + const res = await request(app).get('/health'); + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('OK'); + }); + + it('should return 404 for non-existent endpoint', async () => { + const res = await request(app).get('/non-existent'); + expect(res.statusCode).toEqual(404); + expect(res.body.message).toBe('Route not found'); + }); + +}); \ No newline at end of file diff --git a/src/http_tests/authentication.test.js b/src/http_tests/authentication.test.ts similarity index 82% rename from src/http_tests/authentication.test.js rename to src/http_tests/authentication.test.ts index 9aea42a..27332e9 100644 --- a/src/http_tests/authentication.test.js +++ b/src/http_tests/authentication.test.ts @@ -1,8 +1,7 @@ -require('dotenv').config(); - -const request = require('supertest'); -const app = require('../app'); -const { stopDatabase } = require('../database/mongo-common'); +import 'dotenv/config'; +import request from 'supertest'; +import app from '../app'; // Adjust the path as necessary +import { stopDatabase } from '../database/mongo-common'; describe('Authentication JWT', () => { afterAll(async () => { diff --git a/src/http_tests/stores.test.js b/src/http_tests/stores.test.ts similarity index 71% rename from src/http_tests/stores.test.js rename to src/http_tests/stores.test.ts index f0c11cc..5edffa9 100644 --- a/src/http_tests/stores.test.js +++ b/src/http_tests/stores.test.ts @@ -1,8 +1,14 @@ -require('dotenv').config(); - -const request = require('supertest'); -const app = require('../app'); -const { stopDatabase } = require('../database/mongo-common'); +import 'dotenv/config'; +import request, { Response } from 'supertest'; +import app from '../app'; // Adjust the path as necessary +import { stopDatabase } from '../database/mongo-common'; + +interface Store { + store_profile: string; + shipping_address: string; + _id?: string; // Optionally include the ID in responses + metadata?: string; +} describe('Store "Collections" Endpoint', () => { @@ -12,12 +18,12 @@ describe('Store "Collections" Endpoint', () => { // Test POST /stores to create a new store it('should create a new store', async () => { - const storeData = { + const storeData: Store = { store_profile: 'Nevada Golf Emprium', shipping_address: '99 Nowhere Drive, Nevada', }; - const res = await request(app) + const res: Response = await request(app) .post('/stores') .send(storeData) .set('Content-Type', 'application/json'); @@ -29,24 +35,21 @@ describe('Store "Collections" Endpoint', () => { // Test GET /stores to fetch all stores it('should return a list of stores', async () => { - const res = await request(app).get('/stores'); + const res: Response = await request(app).get('/stores'); expect(res.statusCode).toEqual(200); expect(Array.isArray(res.body)).toBe(true); // Expecting an array of stores - // should contain at least one store - expect(res.body.length).toBeGreaterThan(0); + expect(res.body.length).toBeGreaterThan(0); // Should contain at least one store }); - - // Test PUT /stores/:id to update an existing store it('should update an existing store', async () => { const stores = await request(app).get('/stores'); const storeId = stores.body[0]._id; - const updatedData = { + const updatedData: Partial = { metadata: '68203238d1857e2fae0b6093', }; - const res = await request(app) + const res: Response = await request(app) .put(`/stores/${storeId}`) .send(updatedData) .set('Content-Type', 'application/json'); @@ -61,7 +64,7 @@ describe('Store "Collections" Endpoint', () => { const storeId = stores.body[0]._id; // Get the ID of the first store expect(stores.body.length).toBeGreaterThan(0); - const res = await request(app).delete(`/stores/${storeId}`); + const res: Response = await request(app).delete(`/stores/${storeId}`); expect(res.statusCode).toEqual(200); expect(res.body.message).toBe('Store deleted'); // Ensure that the response contains the message diff --git a/src/index.js b/src/index.ts old mode 100755 new mode 100644 similarity index 71% rename from src/index.js rename to src/index.ts index f22ed91..e488b5b --- a/src/index.js +++ b/src/index.ts @@ -1,12 +1,14 @@ -const app = require('./app.js'); -const { startDatabase, stopDatabase } = require('./database/mongo-common.js'); +// src/server.ts +import app from './app'; +import { startDatabase, stopDatabase } from './database/mongo-common'; +import { Server } from 'http'; -const PORT = process.env.PORT || 3001; -const MONGO_URL = process.env.MONGO_URL; +const PORT: number = parseInt(process.env.PORT || '3001', 10); +const MONGO_URL: string | undefined = process.env.MONGO_URL; -let server; +let server: Server | undefined; -async function startServer() { +async function startServer(): Promise { if (!MONGO_URL) { // Gracefully handle missing DB config app.all('*', (req, res) => { @@ -34,7 +36,7 @@ async function startServer() { } } -function gracefulShutdown(signal) { +function gracefulShutdown(signal: string): void { console.log(`\nReceived ${signal}, shutting down...`); if (server) { server.close(async () => { @@ -48,7 +50,6 @@ function gracefulShutdown(signal) { } } -// Listen for shutdown signals ['SIGINT', 'SIGTERM'].forEach(signal => { process.on(signal, () => gracefulShutdown(signal)); }); diff --git a/src/midleware/compression.js b/src/midleware/compression.js deleted file mode 100644 index 2903485..0000000 --- a/src/midleware/compression.js +++ /dev/null @@ -1,3 +0,0 @@ -const compression = require('compression'); - -module.exports = compression(); diff --git a/src/midleware/compression.ts b/src/midleware/compression.ts new file mode 100644 index 0000000..8ef7cfe --- /dev/null +++ b/src/midleware/compression.ts @@ -0,0 +1,5 @@ +import compression from 'compression'; + +const compressionMiddleware = compression(); + +export default compressionMiddleware; \ No newline at end of file diff --git a/src/midleware/cors.js b/src/midleware/cors.ts similarity index 64% rename from src/midleware/cors.js rename to src/midleware/cors.ts index 0e383f1..9bc0aca 100644 --- a/src/midleware/cors.js +++ b/src/midleware/cors.ts @@ -1,16 +1,18 @@ -const cors = require('cors'); +import cors, { CorsOptions } from 'cors'; +// Load environment variables with fallback values const ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || ''; const ALLOWED_METHODS = process.env.CORS_ALLOWED_METHODS || 'GET,POST,PUT,DELETE'; const ALLOWED_HEADERS = process.env.CORS_ALLOWED_HEADERS || 'Content-Type,Authorization'; -const corsOptions = { - origin: (origin, callback) => { +// Type-safe CORS options +const corsOptions: CorsOptions = { + origin: (origin: string | undefined, callback: (error: Error | null, allow: boolean) => void) => { const allowedOrigins = ALLOWED_ORIGINS.split(',').map(o => o.trim()).filter(Boolean); if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { - callback(new Error('Not allowed by CORS')); + callback(new Error('Not allowed by CORS'), false); } }, methods: ALLOWED_METHODS.split(',').map(m => m.trim()).filter(Boolean), @@ -18,4 +20,4 @@ const corsOptions = { credentials: true, }; -module.exports = cors(corsOptions); \ No newline at end of file +export default cors(corsOptions); \ No newline at end of file diff --git a/src/midleware/errorHandler.js b/src/midleware/errorHandler.js deleted file mode 100644 index 1bed0f9..0000000 --- a/src/midleware/errorHandler.js +++ /dev/null @@ -1,17 +0,0 @@ -// Centralized error-handling middleware for Express -// Logs the stack trace and sends a structured response to the client. - -const errorHandler = (err, req, res, next) => { - // Log detailed error for debugging - console.error(`[ERROR] ${err.stack}`); - - // Customize error response based on environment - const response = { - message: 'Internal Server Error', - ...(process.env.NODE_ENV !== 'production' && { error: err.message }), - }; - - res.status(500).json(response); -}; - -module.exports = errorHandler; \ No newline at end of file diff --git a/src/midleware/errorHandler.ts b/src/midleware/errorHandler.ts new file mode 100644 index 0000000..e010727 --- /dev/null +++ b/src/midleware/errorHandler.ts @@ -0,0 +1,14 @@ +import { Request, Response, NextFunction } from 'express'; + +const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction): void => { + console.error(`[ERROR] ${err.stack}`); + + const response = { + message: 'Internal Server Error', + ...(process.env.NODE_ENV !== 'production' && { error: err.message }), + }; + + res.status(500).json(response); +}; + +export default errorHandler; \ No newline at end of file diff --git a/src/midleware/helmet.js b/src/midleware/helmet.js deleted file mode 100644 index fe7be42..0000000 --- a/src/midleware/helmet.js +++ /dev/null @@ -1,21 +0,0 @@ -const helmet = require('helmet'); - -const configureHelmet = () => - helmet({ - contentSecurityPolicy: false, - crossOriginEmbedderPolicy: false, - crossOriginResourcePolicy: false, - referrerPolicy: { policy: 'no-referrer' }, - expectCt: false, - frameguard: false, - hidePoweredBy: true, - hsts: { - maxAge: 63072000, // 2 years - includeSubDomains: true, - preload: true, - }, - noSniff: true, - // xssFilter: true, // Removed in modern Helmet versions - }); - -module.exports = configureHelmet(); diff --git a/src/midleware/helmet.ts b/src/midleware/helmet.ts new file mode 100644 index 0000000..04112f0 --- /dev/null +++ b/src/midleware/helmet.ts @@ -0,0 +1,18 @@ +import helmet from 'helmet'; +import { RequestHandler } from 'express'; + +const configureHelmet = (): RequestHandler => // Explicitly type as RequestHandler + helmet({ + contentSecurityPolicy: false, + referrerPolicy: { policy: 'no-referrer' }, + expectCt: false, + hidePoweredBy: true, + hsts: { + maxAge: 63072000, // 2 years + includeSubDomains: true, + preload: true, + }, + noSniff: true, + }); + +export default configureHelmet(); \ No newline at end of file diff --git a/src/midleware/json.js b/src/midleware/json.ts similarity index 61% rename from src/midleware/json.js rename to src/midleware/json.ts index 55685cf..ac4eef4 100644 --- a/src/midleware/json.js +++ b/src/midleware/json.ts @@ -1,10 +1,11 @@ -const { json } = require('express'); +import { json } from 'express'; +// Custom JSON middleware configuration const configuredJson = json({ limit: '1mb', strict: false, type: ['application/json', 'application/vnd.api+json'], - reviver: (key, value) => { + reviver: (key: string, value: any): any => { if (key === 'date' && typeof value === 'string') { return new Date(value); } @@ -12,4 +13,4 @@ const configuredJson = json({ }, }); -module.exports = configuredJson; \ No newline at end of file +export default configuredJson; \ No newline at end of file diff --git a/src/midleware/morgan.js b/src/midleware/morgan.js deleted file mode 100644 index 2e37ea4..0000000 --- a/src/midleware/morgan.js +++ /dev/null @@ -1,5 +0,0 @@ -const morgan = require('morgan'); - -const configureMorgan = morgan('dev'); - -module.exports = configureMorgan; \ No newline at end of file diff --git a/src/midleware/morgan.ts b/src/midleware/morgan.ts new file mode 100644 index 0000000..c8ae24b --- /dev/null +++ b/src/midleware/morgan.ts @@ -0,0 +1,6 @@ +import morgan, { StreamOptions } from 'morgan'; + +// Morgan configuration +const configureMorgan = morgan('dev'); + +export default configureMorgan; \ No newline at end of file diff --git a/src/midleware/rateLimiter.js b/src/midleware/rateLimiter.ts similarity index 64% rename from src/midleware/rateLimiter.js rename to src/midleware/rateLimiter.ts index 50eac64..064df85 100644 --- a/src/midleware/rateLimiter.js +++ b/src/midleware/rateLimiter.ts @@ -1,8 +1,8 @@ -const rateLimit = require('express-rate-limit'); +import rateLimit from 'express-rate-limit'; const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per window }); -module.exports = limiter; \ No newline at end of file +export default limiter; \ No newline at end of file diff --git a/src/routes/authRoute.js b/src/routes/authRoute.js deleted file mode 100644 index f461edf..0000000 --- a/src/routes/authRoute.js +++ /dev/null @@ -1,63 +0,0 @@ -const express = require('express'); -const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); -const { getDatabase } = require('../database/mongo-common'); - -const router = express.Router(); -const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret'; - -router.post('/register', async (req, res) => { - const { email, password } = req.body; - - if (!email || !password) { - return res.status(400).json({ message: 'Email and password are required' }); - } - - try { - const db = await getDatabase(); - const usersCollection = db.collection('users'); - - const existingUser = await usersCollection.findOne({ email }); - if (existingUser) { - return res.status(409).json({ message: 'Email already taken' }); - } - - const hashedPassword = await bcrypt.hash(password, 10); - await usersCollection.insertOne({ email, password: hashedPassword }); - - res.status(201).json({ message: 'User registered successfully' }); - } catch (error) { - console.error('Error registering user:', error); - res.status(500).json({ message: 'Internal server error' }); - } -}); - -router.post('/login', async (req, res) => { - const { email, password } = req.body; - - try { - const db = await getDatabase(); - const usersCollection = db.collection('users'); - - const user = await usersCollection.findOne({ email }); - if (!user) { - return res.status(401).json({ message: 'Invalid credentials' }); - } - - const isMatch = await bcrypt.compare(password, user.password); - if (!isMatch) { - return res.status(401).json({ message: 'Invalid credentials' }); - } - - const token = jwt.sign({ userId: user._id, email: user.email }, JWT_SECRET, { - expiresIn: '1h', - }); - - res.status(200).json({ message: 'User logged in successfully', token }); - } catch (error) { - console.error('Error logging in:', error); - res.status(500).json({ message: 'Internal server error' }); - } -}); - -module.exports = router; diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts new file mode 100644 index 0000000..7e23a0d --- /dev/null +++ b/src/routes/authRoute.ts @@ -0,0 +1,72 @@ +import { Request, Response, Router } from 'express'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { getDatabase } from '../database/mongo-common'; + +const router: Router = Router(); +const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret'; + +interface User { + _id?: string; // Make _id optional + email: string; + password: string; +} + +// Register endpoint +router.post('/register', async (req: Request, res: Response): Promise => { + const { email, password } = req.body; + + if (!email || !password) { + res.status(400).json({ message: 'Email and password are required' }); + } + + try { + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const existingUser = await usersCollection.findOne({ email }); + if (existingUser) { + res.status(409).json({ message: 'Email already taken' }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + await usersCollection.insertOne({ email, password: hashedPassword }); + + res.status(201).json({ message: 'User registered successfully' }); + } catch (error) { + console.error('Error registering user:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}); + +// Login endpoint +router.post('/login', async (req: Request, res: Response): Promise => { + const { email, password } = req.body; + + try { + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const user = await usersCollection.findOne({ email }); + if (!user) { + res.status(401).json({ message: 'Invalid credentials' }); + return; + } + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + res.status(401).json({ message: 'Invalid credentials' }); + } + + const token = jwt.sign({ userId: user._id, email: user.email }, JWT_SECRET, { + expiresIn: '1h', + }); + + res.status(200).json({ message: 'User logged in successfully', token }); + } catch (error) { + console.error('Error logging in:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}); + +export default router; diff --git a/src/routes/healthRoute.js b/src/routes/healthRoute.js deleted file mode 100644 index 1ca936d..0000000 --- a/src/routes/healthRoute.js +++ /dev/null @@ -1,11 +0,0 @@ -const router = require('express').Router(); - -router.get('/', (req, res) => { - res.status(200).json({ - status: 'OK', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/healthRoute.ts b/src/routes/healthRoute.ts new file mode 100644 index 0000000..7b5a07b --- /dev/null +++ b/src/routes/healthRoute.ts @@ -0,0 +1,13 @@ +import { Request, Response, Router } from 'express'; + +const router: Router = Router(); + +router.get('/', (req: Request, res: Response): void => { + res.status(200).json({ + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + }); +}); + +export default router; \ No newline at end of file diff --git a/src/routes/notFoundRoute.js b/src/routes/notFoundRoute.js deleted file mode 100644 index 7c3deb2..0000000 --- a/src/routes/notFoundRoute.js +++ /dev/null @@ -1,12 +0,0 @@ -const router = require('express').Router(); - -router.all('*', (req, res) => { - res.status(404).send({ - message: 'Route not found', - method: req.method, - endpoint: req.originalUrl, - timestamp: new Date().toISOString(), - }); -}); - -module.exports = router; diff --git a/src/routes/notFoundRoute.ts b/src/routes/notFoundRoute.ts new file mode 100644 index 0000000..99897dd --- /dev/null +++ b/src/routes/notFoundRoute.ts @@ -0,0 +1,14 @@ +import { Request, Response, Router } from 'express'; + +const router: Router = Router(); + +router.all('*', (req: Request, res: Response) => { + res.status(404).json({ + message: 'Route not found', + method: req.method, + endpoint: req.originalUrl, + timestamp: new Date().toISOString(), + }); +}); + +export default router; \ No newline at end of file diff --git a/src/routes/rootRoute.js b/src/routes/rootRoute.js deleted file mode 100644 index c0b60da..0000000 --- a/src/routes/rootRoute.js +++ /dev/null @@ -1,7 +0,0 @@ -const router = require('express').Router(); - -router.get('/', (_, res) => { - res.redirect('/health'); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/rootRoute.ts b/src/routes/rootRoute.ts new file mode 100644 index 0000000..833840c --- /dev/null +++ b/src/routes/rootRoute.ts @@ -0,0 +1,9 @@ +import { Request, Response, Router } from 'express'; + +const router: Router = Router(); + +router.get('/', (_: Request, res: Response): void => { + res.redirect('/health'); +}); + +export default router; \ No newline at end of file diff --git a/src/routes/storesRoute.ts b/src/routes/storesRoute.ts new file mode 100644 index 0000000..505bae4 --- /dev/null +++ b/src/routes/storesRoute.ts @@ -0,0 +1,30 @@ +import { Router, Request, Response } from 'express'; +import { deleteStore, updateStore, createStore, getStores } from '../database/stores'; + +const router: Router = Router(); + +router.get('/', async (req: Request, res: Response): Promise => { + const stores = await getStores(); + res.json(stores); +}); + +router.post('/', async (req: Request, res: Response): Promise => { + const newStore = req.body; + const createdStore = await createStore(newStore); + res.status(201).json(createdStore); +}); + +router.delete('/:_id', async (req: Request, res: Response): Promise => { + const { _id } = req.params; + const result = await deleteStore(_id); + res.json(result); +}); + +// Endpoint to update a Store +router.put('/:_id', async (req: Request, res: Response): Promise => { + const updatedStore = req.body; + const result = await updateStore(req.params._id, updatedStore); + res.json(result); +}); + +export default router; \ No newline at end of file diff --git a/src/routes/storesRoutes.js b/src/routes/storesRoutes.js deleted file mode 100755 index 4fbe53f..0000000 --- a/src/routes/storesRoutes.js +++ /dev/null @@ -1,27 +0,0 @@ - -const router = require('express').Router(); -const { deleteStore, updateStore, createStore, getStores } = require('../database/stores'); - -router.get('/', async (req, res) => { - res.json(await getStores()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newStore = apiRequest.body; - - apiResponse.status(201).json(await createStore(newStore)); -}); - -router.delete('/:_id', async (apiRequest, apiResponse) => { - apiResponse.json(await deleteStore(apiRequest.params._id)); -}); - -// endpoint to update a Store -router.put('/:_id', async (apiRequest, apiResponse) => { - const updatedStore = apiRequest.body; - - apiResponse.json( - await updateStore(apiRequest.params._id, updatedStore)); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/utils/git-user-name.js b/src/utils/git-user-name.ts similarity index 63% rename from src/utils/git-user-name.js rename to src/utils/git-user-name.ts index 272da89..6c6f224 100644 --- a/src/utils/git-user-name.js +++ b/src/utils/git-user-name.ts @@ -1,6 +1,6 @@ -const { execSync } = require('child_process'); +import { execSync } from 'child_process'; -function getGitUserName() { +function getGitUserName(): string { try { const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); return name || 'unknown'; @@ -9,4 +9,4 @@ function getGitUserName() { } } -module.exports = getGitUserName; \ No newline at end of file +export default getGitUserName; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0b62d54 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES6", // Use ES6 as the output target for better module handling + "module": "CommonJS", // Use CommonJS modules since Node.js uses them + "moduleResolution": "node", // Use Node module resolution for imports + "esModuleInterop": true, // Allow default imports from non-ES modules + "skipLibCheck": true, // Skip type checking of declaration files for faster builds + "strict": true, // Enable strict type-checking options + "forceConsistentCasingInFileNames": true, // Enforce consistent casing in file names + "outDir": "./dist", // Specify where compiled JavaScript files go + "baseUrl": ".", // Base URL to resolve non-relative modules + "types": ["node", "jest"], // Include types for Node.js and Jest for testing + "allowJs": true, // Allow JavaScript files to be included in the compilation + "resolveJsonModule": true, // Allow importing of JSON files + }, + "include": [ + "src/**/*.ts", // Include all TS files in the `src` folder + "tests/**/*.ts" // Include test files in a separate `tests` folder + ], + "exclude": [ + "node_modules", // Exclude node_modules + "dist" // Exclude the `dist` folder where compiled JS files are stored + ] +} From ebd57890df7fe1e1366375257207f80df0fb7875 Mon Sep 17 00:00:00 2001 From: pakeku Date: Fri, 16 May 2025 21:50:05 -0400 Subject: [PATCH 37/63] fix: update return field for update --- package.json | 2 +- src/database/stores.ts | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index a816f2b..34686b5 100755 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "start": "node dist/index.js", - "dev": "env-cmd ts-node src/index.ts", + "dev": "env-cmd ts-node --esm src/index.ts", "test": "jest" }, "keywords": [ diff --git a/src/database/stores.ts b/src/database/stores.ts index 67a93bd..942e98f 100644 --- a/src/database/stores.ts +++ b/src/database/stores.ts @@ -7,7 +7,7 @@ interface Store { _id?: string; name: string; addedBy?: string; - // add other fields relevant to your store here + metadata?: string; } const collectionName = 'stores'; @@ -57,18 +57,18 @@ async function updateStore(id: string, store: Partial): Promise Date: Fri, 16 May 2025 21:59:43 -0400 Subject: [PATCH 38/63] update NODE_ENV to "test" for out of the box ready experience --- .env.sample | 3 ++- README.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index a685359..a5e05e8 100644 --- a/.env.sample +++ b/.env.sample @@ -11,7 +11,8 @@ MONGO_URL='mongodb://your_username:your_password@host:port/database_name' # Set the environment # NODE_ENV can be 'development', 'production', or 'test' -NODE_ENV=development +# When set to `"test"`, a test database is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. +NODE_ENV=test # === Notes === # Do NOT use real credentials in this file. diff --git a/README.md b/README.md index 4e292e1..76fe8b5 100755 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Environmental Variables: 3. ALLOWED_ORIGINS (optional) 4. ALLOWED_METHODS (optional) 5. ALLOWED_HEADERS (optional) +6. NODE_ENV=test --- When set to ***"test"***, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. ## Getting Started 1. Copy this file to .env and fill in the actual values From 0964c3af16f5675369fda6d9092b45cb97aabd24 Mon Sep 17 00:00:00 2001 From: pakeku Date: Fri, 16 May 2025 23:41:28 -0400 Subject: [PATCH 39/63] feat: get profile info --- src/http_tests/authentication.test.ts | 16 ++++++++++++ src/routes/authRoute.ts | 35 ++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/http_tests/authentication.test.ts b/src/http_tests/authentication.test.ts index 27332e9..ae9d8f1 100644 --- a/src/http_tests/authentication.test.ts +++ b/src/http_tests/authentication.test.ts @@ -31,4 +31,20 @@ describe('Authentication JWT', () => { expect(res.body).toHaveProperty('token'); expect(typeof res.body.token).toBe('string'); }); + + it('should return user profile with valid token', async () => { + const loginRes = await request(app) + .post('/auth/login') + .send(testUser) + .expect(200); + + const token = loginRes.body.token; + + const res = await request(app) + .get('/auth/me') + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(res.body).toHaveProperty('email', testUser.email); + }) }); diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts index 7e23a0d..45b42b3 100644 --- a/src/routes/authRoute.ts +++ b/src/routes/authRoute.ts @@ -1,13 +1,14 @@ import { Request, Response, Router } from 'express'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; +import { ObjectId } from 'mongodb'; import { getDatabase } from '../database/mongo-common'; const router: Router = Router(); const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret'; interface User { - _id?: string; // Make _id optional + _id?: ObjectId; email: string; password: string; } @@ -18,6 +19,7 @@ router.post('/register', async (req: Request, res: Response): Promise => { if (!email || !password) { res.status(400).json({ message: 'Email and password are required' }); + return; } try { @@ -27,15 +29,18 @@ router.post('/register', async (req: Request, res: Response): Promise => { const existingUser = await usersCollection.findOne({ email }); if (existingUser) { res.status(409).json({ message: 'Email already taken' }); + return; } const hashedPassword = await bcrypt.hash(password, 10); await usersCollection.insertOne({ email, password: hashedPassword }); res.status(201).json({ message: 'User registered successfully' }); + return; } catch (error) { console.error('Error registering user:', error); res.status(500).json({ message: 'Internal server error' }); + return; } }); @@ -69,4 +74,32 @@ router.post('/login', async (req: Request, res: Response): Promise => { } }); +// responds with user data +router.get('/me', async (req: Request, res: Response): Promise => { + const token = req.headers.authorization?.split(' ')[1]; + + if (!token) { + res.status(401).json({ message: 'No token provided' }); + return; + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string; email: string }; + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const user = await usersCollection.findOne({ _id: new ObjectId(decoded.userId) }); + + if (!user) { + res.status(201).json({ message: 'User not found' }); + return; + } + + res.status(200).json({ email: user.email }); + } catch (error) { + console.error('Error fetching user data:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}) + export default router; From 7f4e25e8b5aa32f460752f15b2931f0b759b3b9e Mon Sep 17 00:00:00 2001 From: pakeku Date: Fri, 16 May 2025 23:50:56 -0400 Subject: [PATCH 40/63] fix: add documentation about JWT_SECRET --- .env.sample | 10 +++++++++- README.md | 3 +++ src/routes/authRoute.ts | 6 +++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index a5e05e8..4c1bf72 100644 --- a/.env.sample +++ b/.env.sample @@ -25,4 +25,12 @@ NODE_ENV=test # Default methods: GET, POST, PUT, DELETE ALLOWED_ORIGINS= ALLOWED_METHODS= -ALLOWED_HEADERS= \ No newline at end of file +ALLOWED_HEADERS= + +# === JWT Configuration === +# JWT (JSON Web Token) secret key for signing tokens +# A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs). +# This is required for authentication to work correctly. +# 🔐 Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: +# $ node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" +JWT_SECRET= \ No newline at end of file diff --git a/README.md b/README.md index 76fe8b5..c8d7c20 100755 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Environmental Variables: 4. ALLOWED_METHODS (optional) 5. ALLOWED_HEADERS (optional) 6. NODE_ENV=test --- When set to ***"test"***, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. +7. JWT_SECRET --- A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs). This is required for authentication to work correctly. + Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: ```bash node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"``` +8. ## Getting Started 1. Copy this file to .env and fill in the actual values diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts index 45b42b3..b5ee04b 100644 --- a/src/routes/authRoute.ts +++ b/src/routes/authRoute.ts @@ -5,7 +5,11 @@ import { ObjectId } from 'mongodb'; import { getDatabase } from '../database/mongo-common'; const router: Router = Router(); -const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret'; +const JWT_SECRET = process.env.JWT_SECRET; + +if (!JWT_SECRET) { + throw new Error('JWT_SECRET is not defined'); +} interface User { _id?: ObjectId; From 453822ded25224c2f06360b75972d1d475f20af2 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 00:07:03 -0400 Subject: [PATCH 41/63] fix build output and start command, update documentation --- README.md | 7 +++++-- package.json | 9 +++++---- src/index.ts | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c8d7c20..25ca409 100755 --- a/README.md +++ b/README.md @@ -25,8 +25,11 @@ cp .env.sample .env 1. Run a script: ```json "scripts": { + "prebuild":"rm -rf dist", + "build":"tsc", "start": "node ./src/index.js", - "dev": "env-cmd nodemon ./src/index.js", - "test": "jest" + "dev": "env-cmd nodemon ./src/index.ts", + "test": "jest", + "test:watch": "jest --watch" } ``` \ No newline at end of file diff --git a/package.json b/package.json index 34686b5..c1e0ba6 100755 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "version": "1.0.1", "description": "", "main": "./src/index.ts", - "type": "module", "scripts": { + "prebuild": "rm -rf dist", "build": "tsc", "start": "node dist/index.js", - "dev": "env-cmd ts-node --esm src/index.ts", - "test": "jest" + "dev": "env-cmd ts-node src/index.ts", + "test": "jest", + "test:watch": "jest --watch" }, "keywords": [ "mongodb", @@ -50,4 +51,4 @@ "ts-node": "^10.9.2", "typescript": "^5.8.3" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index e488b5b..0c5dd6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ // src/server.ts +import 'dotenv/config'; import app from './app'; import { startDatabase, stopDatabase } from './database/mongo-common'; import { Server } from 'http'; From b535c5c9b10862530374aeced0f4d7743c388afd Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 00:40:31 -0400 Subject: [PATCH 42/63] use err message returned --- src/utils/git-user-name.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index 6c6f224..ee4f35b 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -1,12 +1,13 @@ import { execSync } from 'child_process'; function getGitUserName(): string { - try { - const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); - return name || 'unknown'; - } catch (err) { - return 'unknown'; - } + try { + const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); + return name || 'unknown'; + } catch (err) { + console.info('Git user name not found, returning "unknown"', err); + return 'unknown'; + } } -export default getGitUserName; \ No newline at end of file +export default getGitUserName; From 533f3f3609a38fd7b6ae119997c499cc4d45f51e Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 00:41:28 -0400 Subject: [PATCH 43/63] remove unused variable --- src/midleware/morgan.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/midleware/morgan.ts b/src/midleware/morgan.ts index c8ae24b..85f2240 100644 --- a/src/midleware/morgan.ts +++ b/src/midleware/morgan.ts @@ -1,6 +1,6 @@ -import morgan, { StreamOptions } from 'morgan'; +import morgan from 'morgan'; // Morgan configuration const configureMorgan = morgan('dev'); -export default configureMorgan; \ No newline at end of file +export default configureMorgan; From 98dc26975ee19ee93290d972421238d46464cbf7 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 01:42:47 -0400 Subject: [PATCH 44/63] enhancement: add prettier and eslint for consistent code --- .prettierignore | 3 + .prettierrc | 7 ++ .vscode/settings.json | 10 +-- README.md | 24 ++++-- eslint.config.js | 23 ++++++ jest.config.js | 6 +- package.json | 24 +++++- src/app.ts | 15 ++-- src/database/mongo-common.ts | 22 ++--- src/database/stores.ts | 63 +++++++------- src/http_tests/app.test.ts | 26 +++--- src/http_tests/authentication.test.ts | 88 ++++++++++---------- src/http_tests/stores.test.ts | 115 ++++++++++++++------------ src/index.ts | 51 +++++++----- src/midleware/compression.ts | 2 +- src/midleware/cors.ts | 22 +++-- src/midleware/errorHandler.ts | 10 ++- src/midleware/helmet.ts | 10 +-- src/midleware/json.ts | 8 +- src/midleware/rateLimiter.ts | 4 +- src/routes/authRoute.ts | 13 +-- src/routes/healthRoute.ts | 12 +-- src/routes/notFoundRoute.ts | 14 ++-- src/routes/rootRoute.ts | 2 +- src/routes/storesRoute.ts | 11 +-- src/utils/git-user-name.ts | 2 +- tsconfig.json | 30 +++---- 27 files changed, 353 insertions(+), 264 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5c5bb8f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules +.github \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..204110e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "avoid" +} diff --git a/.vscode/settings.json b/.vscode/settings.json index f4e35a9..5294c53 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { - "accessibility.signals.chatRequestSent": { - "sound": "off", - "announcement": "off" - } -} \ No newline at end of file + "accessibility.signals.chatRequestSent": { + "sound": "off", + "announcement": "off" + } +} diff --git a/README.md b/README.md index 25ca409..acb2acb 100755 --- a/README.md +++ b/README.md @@ -1,35 +1,45 @@ # Node.js and Express Backend + [![Backend API - CI Tests](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml) [![Known Vulnerabilities](https://snyk.io/test/github/pakeku/backend-api/badge.svg)](https://snyk.io/test/github/pakeku/backend-api) ## Requirements + Identify your MongoDB URL. Visit MongoDB to sign up and get started. Environmental Variables: + 1. MONGO_URL 2. PORT (optional) 3. ALLOWED_ORIGINS (optional) 4. ALLOWED_METHODS (optional) 5. ALLOWED_HEADERS (optional) -6. NODE_ENV=test --- When set to ***"test"***, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. +6. NODE\*ENV=test --- When set to \*\*\*"test"\_\*\*, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. 7. JWT_SECRET --- A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs). This is required for authentication to work correctly. - Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: ```bash node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"``` -8. + Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: `bash node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` +8. ## Getting Started + 1. Copy this file to .env and fill in the actual values -```bash + +```bash cp .env.sample .env ``` 1. Run a script: -```json + +```json "scripts": { "prebuild":"rm -rf dist", "build":"tsc", "start": "node ./src/index.js", "dev": "env-cmd nodemon ./src/index.ts", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts --no-ignore", + "format": "prettier --write ." } -``` \ No newline at end of file +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..56a2c19 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import perfectionist from 'eslint-plugin-perfectionist'; + +export default tseslint.config( + { + ignores: ['**/*.js'], + }, + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + perfectionist.configs['recommended-natural'] +); diff --git a/jest.config.js b/jest.config.js index 9fc603e..7c534e1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ /** @type {import('ts-jest').JestConfigWithTsJest} **/ export default { - testEnvironment: "node", + testEnvironment: 'node', transform: { - "^.+\.tsx?$": ["ts-jest",{}], + '^.+\.tsx?$': ['ts-jest', {}], }, -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index c1e0ba6..d325ae9 100755 --- a/package.json +++ b/package.json @@ -3,13 +3,22 @@ "version": "1.0.1", "description": "", "main": "./src/index.ts", + "type": "module", + "private": true, "scripts": { "prebuild": "rm -rf dist", "build": "tsc", "start": "node dist/index.js", "dev": "env-cmd ts-node src/index.ts", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts --no-ignore", + "format": "prettier --write ." + }, + "imports": { + "#*": "./src/*" }, "keywords": [ "mongodb", @@ -34,6 +43,7 @@ "nodemon": "^3.1.10" }, "devDependencies": { + "@eslint/js": "^9.27.0", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", "@types/cors": "^2.8.18", @@ -44,11 +54,19 @@ "@types/morgan": "^1.9.9", "@types/node": "^22.15.17", "@types/supertest": "^6.0.3", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "eslint": "^9.27.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-perfectionist": "^4.13.0", + "eslint-plugin-prettier": "^5.4.0", "jest": "^29.7.0", "mongodb-memory-server": "^10.1.4", + "prettier": "^3.5.3", "supertest": "^7.1.0", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1" } -} \ No newline at end of file +} diff --git a/src/app.ts b/src/app.ts index 5eb7e6a..3ac052c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,18 +1,17 @@ import express, { Application } from 'express'; -import errorHandler from './midleware/errorHandler'; -import rateLimiter from './midleware/rateLimiter'; import compression from './midleware/compression'; +import cors from './midleware/cors'; +import errorHandler from './midleware/errorHandler'; import helmet from './midleware/helmet'; import json from './midleware/json'; -import cors from './midleware/cors'; import morgan from './midleware/morgan'; - -import notFoundRouter from './routes/notFoundRoute'; +import rateLimiter from './midleware/rateLimiter'; +import authRouter from './routes/authRoute'; import healthRouter from './routes/healthRoute'; -import storesRouter from './routes/storesRoute'; +import notFoundRouter from './routes/notFoundRoute'; import rootRouter from './routes/rootRoute'; -import authRouter from './routes/authRoute'; +import storesRouter from './routes/storesRoute'; const app: Application = express(); @@ -35,4 +34,4 @@ app.use('/stores', storesRouter); app.use('/auth', authRouter); app.use('*', notFoundRouter); -export default app; \ No newline at end of file +export default app; diff --git a/src/database/mongo-common.ts b/src/database/mongo-common.ts index ba1092b..f01cac5 100644 --- a/src/database/mongo-common.ts +++ b/src/database/mongo-common.ts @@ -3,7 +3,7 @@ * Documentation: https://mongodb.github.io/node-mongodb-native/6.16/classes/MongoClient.html */ -import { MongoClient, Db } from 'mongodb'; +import { Db, MongoClient } from 'mongodb'; import { MongoMemoryServer } from 'mongodb-memory-server'; const mongoDBURL = process.env.MONGO_URL; @@ -17,26 +17,31 @@ let database: Db | null = null; let mongoServer: MongoMemoryServer | null = null; // store reference to in-memory server for shutdown const getRightMongoDBURL = async (): Promise => { - const env = process.env.NODE_ENV; + const env = process.env.NODE_ENV ?? 'development'; if (env === 'test') { mongoServer = await MongoMemoryServer.create(); return mongoServer.getUri(); } - if (['development', 'production'].includes(env || '')) { - return mongoDBURL as string; + if (['development', 'production'].includes(env)) { + if (!mongoDBURL) throw new Error('MONGO_URL is not defined'); + return mongoDBURL; } throw new Error(`Unsupported NODE_ENV: ${env}`); }; -export async function startDatabase(uri: string | null = null): Promise { +export async function getDatabase(): Promise { + return database ?? (await startDatabase()); +} + +export async function startDatabase(uri: null | string = null): Promise { if (client && database) { return database; } - const dbURI = uri || await getRightMongoDBURL(); + const dbURI = uri ?? (await getRightMongoDBURL()); client = new MongoClient(dbURI); await client.connect(); @@ -44,11 +49,6 @@ export async function startDatabase(uri: string | null = null): Promise { return database; } - -export async function getDatabase(): Promise { - return database || await startDatabase(); -} - export async function stopDatabase(): Promise { if (client) { await client.close(); diff --git a/src/database/stores.ts b/src/database/stores.ts index 942e98f..6579687 100644 --- a/src/database/stores.ts +++ b/src/database/stores.ts @@ -1,74 +1,73 @@ -import { getDatabase } from './mongo-common'; import { ObjectId } from 'mongodb'; + import getUserName from '../utils/git-user-name'; +import { getDatabase } from './mongo-common'; // Define the Store interface -interface Store { - _id?: string; - name: string; +export interface Store { + _id?: ObjectId; addedBy?: string; metadata?: string; + name: string; } const collectionName = 'stores'; // Create a Store -async function createStore(store: Store): Promise { +async function createStore(store: Store): Promise { const database = await getDatabase(); store.addedBy = getUserName(); - + const storeToInsert = { ...store, _id: store._id ? new ObjectId(store._id) : undefined }; const { insertedId } = await database.collection(collectionName).insertOne(storeToInsert); // Return the store document with the inserted _id - return await database.collection(collectionName).findOne({ _id: insertedId }) as Store | null; -} - -// Get all stores -async function getStores(): Promise { - const database = await getDatabase(); - const stores = await database.collection(collectionName).find({}).toArray(); - return stores.map(store => ({ - _id: store._id?.toString(), - name: store.name, - addedBy: store.addedBy, - })) as Store[]; + return (await database.collection(collectionName).findOne({ _id: insertedId })) as null | Store; } // Delete a store by id async function deleteStore(_id: string): Promise<{ message: string }> { const database = await getDatabase(); - + const result = await database.collection(collectionName).deleteOne({ _id: new ObjectId(_id), }); if (result.deletedCount === 0) { - return { message: "No store found with that id" }; + return { message: 'No store found with that id' }; } - return { message: "Store deleted" }; + return { message: 'Store deleted' }; +} + +// Get all stores +async function getStores(): Promise { + const database = await getDatabase(); + const stores = await database.collection(collectionName).find({}).toArray(); + return stores.map(store => ({ + _id: new ObjectId(store._id), + addedBy: store.addedBy, + name: store.name, + })); } // Update a store -async function updateStore(id: string, store: Partial): Promise { +async function updateStore(id: string, store: Partial): Promise { const database = await getDatabase(); delete store._id; - await database.collection(collectionName).updateOne( - { _id: new ObjectId(id) }, - { $set: store } - ); + await database.collection(collectionName).updateOne({ _id: new ObjectId(id) }, { $set: store }); - const updated = await database.collection(collectionName).findOne({ _id: new ObjectId(id) }); + const updated = await database + .collection(collectionName) + .findOne({ _id: new ObjectId(id) }); if (!updated) return null; return { - _id: updated._id?.toString(), - name: updated.name, + _id: new ObjectId(updated._id), addedBy: updated.addedBy, metadata: updated.metadata, - } as Store; + name: updated.name, + }; } - -export { createStore, getStores, deleteStore, updateStore }; \ No newline at end of file +export { createStore, deleteStore, getStores, updateStore }; diff --git a/src/http_tests/app.test.ts b/src/http_tests/app.test.ts index 35d7ab5..f148cfc 100644 --- a/src/http_tests/app.test.ts +++ b/src/http_tests/app.test.ts @@ -1,25 +1,31 @@ import 'dotenv/config'; -import request from 'supertest'; +import request, { Response } from 'supertest'; + import app from '../app'; -describe('Health Check Endpoint', () => { +interface ResponseBody { + message?: string; + status: string; +} +describe('Health Check Endpoint', () => { it('should return 302 and redirect to /health', async () => { - const res = await request(app).get('/'); - expect(res.statusCode).toEqual(302); + const res: Response = await request(app).get('/'); + expect(res.status).toBe(302); expect(res.headers.location).toBe('/health'); }); it('should return 200 and status OK', async () => { - const res = await request(app).get('/health'); + const res: Response = await request(app).get('/health'); + const body = res.body as ResponseBody; + expect(body.status).toBe('OK'); expect(res.statusCode).toEqual(200); - expect(res.body.status).toBe('OK'); }); it('should return 404 for non-existent endpoint', async () => { - const res = await request(app).get('/non-existent'); + const res: Response = await request(app).get('/non-existent'); + const body = res.body as ResponseBody; expect(res.statusCode).toEqual(404); - expect(res.body.message).toBe('Route not found'); + expect(body.message).toBe('Route not found'); }); - -}); \ No newline at end of file +}); diff --git a/src/http_tests/authentication.test.ts b/src/http_tests/authentication.test.ts index ae9d8f1..3f65a2e 100644 --- a/src/http_tests/authentication.test.ts +++ b/src/http_tests/authentication.test.ts @@ -1,50 +1,48 @@ import 'dotenv/config'; -import request from 'supertest'; -import app from '../app'; // Adjust the path as necessary +import request, { Response } from 'supertest'; + +import app from '../app'; import { stopDatabase } from '../database/mongo-common'; +// Define expected response shapes +interface AuthResponse { + message?: string; + token: string; +} + describe('Authentication JWT', () => { - afterAll(async () => { - await stopDatabase(); // Ensure database connection is closed - }); - - const testUser = { - email: 'testuser@example.com', - password: 'SecurePass123!', - }; - - it('should register a new user', async () => { - const res = await request(app) - .post('/auth/register') - .send(testUser) - .expect(201); - - expect(res.body).toHaveProperty('message'); - }); - - it('should login with valid credentials', async () => { - const res = await request(app) - .post('/auth/login') - .send(testUser) - .expect(200); - - expect(res.body).toHaveProperty('token'); - expect(typeof res.body.token).toBe('string'); - }); - - it('should return user profile with valid token', async () => { - const loginRes = await request(app) - .post('/auth/login') - .send(testUser) - .expect(200); - - const token = loginRes.body.token; - - const res = await request(app) - .get('/auth/me') - .set('Authorization', `Bearer ${token}`) - .expect(200); - - expect(res.body).toHaveProperty('email', testUser.email); - }) + afterAll(async () => { + await stopDatabase(); // Ensure database connection is closed + }); + + const testUser = { + email: 'testuser@example.com', + password: 'SecurePass123!', + }; + + it('should register a new user', async () => { + const res: Response = await request(app).post('/auth/register').send(testUser).expect(201); + const body = res.body as AuthResponse; + expect(body).toHaveProperty('message'); + }); + + it('should login with valid credentials', async () => { + const res: Response = await request(app).post('/auth/login').send(testUser).expect(200); + const body = res.body as AuthResponse; + expect(body).toHaveProperty('token'); + expect(typeof body.token).toBe('string'); + }); + + it('should return user profile with valid token', async () => { + const loginRes: Response = await request(app).post('/auth/login').send(testUser).expect(200); + const body = loginRes.body as AuthResponse; + + const res = await request(app) + .get('/auth/me') + .set('Authorization', `Bearer ${body.token}`) + .expect(200); + + expect(res.statusCode).toEqual(200); + expect(body).toHaveProperty('email', testUser.email); + }); }); diff --git a/src/http_tests/stores.test.ts b/src/http_tests/stores.test.ts index 5edffa9..9de28b4 100644 --- a/src/http_tests/stores.test.ts +++ b/src/http_tests/stores.test.ts @@ -1,73 +1,80 @@ import 'dotenv/config'; import request, { Response } from 'supertest'; + import app from '../app'; // Adjust the path as necessary import { stopDatabase } from '../database/mongo-common'; interface Store { - store_profile: string; - shipping_address: string; - _id?: string; // Optionally include the ID in responses - metadata?: string; + _id?: string; // Optionally include the ID in responses + metadata?: string; + shipping_address: string; + store_profile: string; } describe('Store "Collections" Endpoint', () => { + afterAll(async () => { + await stopDatabase(); // Ensure database connection is closed + }); - afterAll(async () => { - await stopDatabase(); // Ensure database connection is closed - }); - - // Test POST /stores to create a new store - it('should create a new store', async () => { - const storeData: Store = { - store_profile: 'Nevada Golf Emprium', - shipping_address: '99 Nowhere Drive, Nevada', - }; - - const res: Response = await request(app) - .post('/stores') - .send(storeData) - .set('Content-Type', 'application/json'); - - expect(res.statusCode).toEqual(201); // Expecting 201 Created - expect(res.body.store_profile).toBe(storeData.store_profile); - expect(res.body.shipping_address).toBe(storeData.shipping_address); - }); + // Test POST /stores to create a new store + it('should create a new store', async () => { + const storeData: Store = { + shipping_address: '99 Nowhere Drive, Nevada', + store_profile: 'Nevada Golf Emprium', + }; - // Test GET /stores to fetch all stores - it('should return a list of stores', async () => { - const res: Response = await request(app).get('/stores'); - expect(res.statusCode).toEqual(200); - expect(Array.isArray(res.body)).toBe(true); // Expecting an array of stores - expect(res.body.length).toBeGreaterThan(0); // Should contain at least one store - }); + const res: Response = await request(app) + .post('/stores') + .send(storeData) + .set('Content-Type', 'application/json'); - // Test PUT /stores/:id to update an existing store - it('should update an existing store', async () => { - const stores = await request(app).get('/stores'); - const storeId = stores.body[0]._id; - const updatedData: Partial = { - metadata: '68203238d1857e2fae0b6093', - }; + const body = res.body as Store; + expect(res.statusCode).toEqual(201); // Expecting 201 Created + expect(body.store_profile).toBe(storeData.store_profile); + expect(body.shipping_address).toBe(storeData.shipping_address); + }); - const res: Response = await request(app) - .put(`/stores/${storeId}`) - .send(updatedData) - .set('Content-Type', 'application/json'); + // Test GET /stores to fetch all stores + it('should return a list of stores', async () => { + const res: Response = await request(app).get('/stores'); + const body = res.body as Store[]; + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); // Expecting an array of stores + expect(body.length).toBeGreaterThan(0); // Should contain at least one store + }); - expect(res.statusCode).toEqual(200); - expect(res.body.metadata).toBe(updatedData.metadata); - }); + // Test PUT /stores/:id to update an existing store + it('should update an existing store', async () => { + const stores: Response = await request(app).get('/stores'); + const storesBody = stores.body as Store[]; + const storeId = storesBody[0]._id; + const updatedData: Partial = { + metadata: '68203238d1857e2fae0b6093', + }; - // Test DELETE /stores/:id to delete a store - it('should delete a store', async () => { - const stores = await request(app).get('/stores'); - const storeId = stores.body[0]._id; // Get the ID of the first store - expect(stores.body.length).toBeGreaterThan(0); + if (storeId) { + const res: Response = await request(app) + .put(`/stores/${storeId}`) + .send(updatedData) + .set('Content-Type', 'application/json'); - const res: Response = await request(app).delete(`/stores/${storeId}`); + const body = res.body as Store; + expect(res.statusCode).toEqual(200); + expect(body.metadata).toBe(updatedData.metadata); + } + }); - expect(res.statusCode).toEqual(200); - expect(res.body.message).toBe('Store deleted'); // Ensure that the response contains the message - }); + // Test DELETE /stores/:id to delete a store + it('should delete a store', async () => { + const stores: Response = await request(app).get('/stores'); + const storesBody = stores.body as Store[]; + const storeId = storesBody[0]._id; // Get the ID of the first store + if (storeId) { + const res: Response = await request(app).delete(`/stores/${storeId}`); + const body = res.body as { message: string }; + expect(res.statusCode).toEqual(200); + expect(body.message).toBe('Store deleted'); // Ensure that the response contains the message + } + }); }); diff --git a/src/index.ts b/src/index.ts index 0c5dd6d..4f915e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,35 @@ // src/server.ts import 'dotenv/config'; +import { Server } from 'http'; + import app from './app'; import { startDatabase, stopDatabase } from './database/mongo-common'; -import { Server } from 'http'; -const PORT: number = parseInt(process.env.PORT || '3001', 10); +const PORT: number = parseInt(process.env.PORT ?? '3001', 10); const MONGO_URL: string | undefined = process.env.MONGO_URL; let server: Server | undefined; +function gracefulShutdown(signal: string): void { + console.log(`\nReceived ${signal}, shutting down...`); + if (server) { + server.close(() => { + console.log('HTTP server closed'); + stopDatabase() + .then(() => { + console.log('Database connection closed'); + process.exit(0); + }) + .catch((err: unknown) => { + console.error('Error during shutdown:', err); + process.exit(1); + }); + }); + } else { + process.exit(0); + } +} + async function startServer(): Promise { if (!MONGO_URL) { // Gracefully handle missing DB config @@ -19,7 +40,7 @@ async function startServer(): Promise { }); server = app.listen(PORT, () => { - console.log(`Server running without DB on port ${PORT}`); + console.log(`Server running without DB on port ${PORT.toString()}`); }); return; @@ -29,30 +50,18 @@ async function startServer(): Promise { await startDatabase(); server = app.listen(PORT, () => { - console.log(`Server started on port ${PORT}`); + console.log(`Server started on port ${PORT.toString()}`); }); - } catch (err) { + } catch (err: unknown) { console.error('Failed to start database:', err); process.exit(1); } } -function gracefulShutdown(signal: string): void { - console.log(`\nReceived ${signal}, shutting down...`); - if (server) { - server.close(async () => { - console.log('HTTP server closed'); - await stopDatabase(); - console.log('Database connection closed'); - process.exit(0); - }); - } else { - process.exit(0); - } -} - ['SIGINT', 'SIGTERM'].forEach(signal => { - process.on(signal, () => gracefulShutdown(signal)); + process.on(signal, () => { + gracefulShutdown(signal); + }); }); -startServer(); \ No newline at end of file +void startServer() \ No newline at end of file diff --git a/src/midleware/compression.ts b/src/midleware/compression.ts index 8ef7cfe..968c3ae 100644 --- a/src/midleware/compression.ts +++ b/src/midleware/compression.ts @@ -2,4 +2,4 @@ import compression from 'compression'; const compressionMiddleware = compression(); -export default compressionMiddleware; \ No newline at end of file +export default compressionMiddleware; diff --git a/src/midleware/cors.ts b/src/midleware/cors.ts index 9bc0aca..04f2ffb 100644 --- a/src/midleware/cors.ts +++ b/src/midleware/cors.ts @@ -1,23 +1,29 @@ import cors, { CorsOptions } from 'cors'; // Load environment variables with fallback values -const ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || ''; -const ALLOWED_METHODS = process.env.CORS_ALLOWED_METHODS || 'GET,POST,PUT,DELETE'; -const ALLOWED_HEADERS = process.env.CORS_ALLOWED_HEADERS || 'Content-Type,Authorization'; +const ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS ?? ''; +const ALLOWED_METHODS = process.env.CORS_ALLOWED_METHODS ?? 'GET,POST,PUT,DELETE'; +const ALLOWED_HEADERS = process.env.CORS_ALLOWED_HEADERS ?? 'Content-Type,Authorization'; // Type-safe CORS options const corsOptions: CorsOptions = { + allowedHeaders: ALLOWED_HEADERS.split(',') + .map(h => h.trim()) + .filter(Boolean), + credentials: true, + methods: ALLOWED_METHODS.split(',') + .map(m => m.trim()) + .filter(Boolean), origin: (origin: string | undefined, callback: (error: Error | null, allow: boolean) => void) => { - const allowedOrigins = ALLOWED_ORIGINS.split(',').map(o => o.trim()).filter(Boolean); + const allowedOrigins = ALLOWED_ORIGINS.split(',') + .map(o => o.trim()) + .filter(Boolean); if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS'), false); } }, - methods: ALLOWED_METHODS.split(',').map(m => m.trim()).filter(Boolean), - allowedHeaders: ALLOWED_HEADERS.split(',').map(h => h.trim()).filter(Boolean), - credentials: true, }; -export default cors(corsOptions); \ No newline at end of file +export default cors(corsOptions); diff --git a/src/midleware/errorHandler.ts b/src/midleware/errorHandler.ts index e010727..c2f4b60 100644 --- a/src/midleware/errorHandler.ts +++ b/src/midleware/errorHandler.ts @@ -1,7 +1,9 @@ -import { Request, Response, NextFunction } from 'express'; +import { NextFunction, Request, Response } from 'express'; -const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction): void => { - console.error(`[ERROR] ${err.stack}`); +// using _ to indicate that the parameter is not used +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction): void => { + console.error(`[ERROR] ${err.stack ?? 'No stack trace available'}`); const response = { message: 'Internal Server Error', @@ -11,4 +13,4 @@ const errorHandler = (err: Error, req: Request, res: Response, next: NextFunctio res.status(500).json(response); }; -export default errorHandler; \ No newline at end of file +export default errorHandler; diff --git a/src/midleware/helmet.ts b/src/midleware/helmet.ts index 04112f0..fc6aab0 100644 --- a/src/midleware/helmet.ts +++ b/src/midleware/helmet.ts @@ -1,18 +1,18 @@ +import { RequestHandler } from 'express'; import helmet from 'helmet'; -import { RequestHandler } from 'express'; const configureHelmet = (): RequestHandler => // Explicitly type as RequestHandler helmet({ contentSecurityPolicy: false, - referrerPolicy: { policy: 'no-referrer' }, expectCt: false, hidePoweredBy: true, hsts: { - maxAge: 63072000, // 2 years includeSubDomains: true, - preload: true, + maxAge: 63072000, // 2 years + preload: true, }, noSniff: true, + referrerPolicy: { policy: 'no-referrer' }, }); -export default configureHelmet(); \ No newline at end of file +export default configureHelmet(); diff --git a/src/midleware/json.ts b/src/midleware/json.ts index ac4eef4..4662242 100644 --- a/src/midleware/json.ts +++ b/src/midleware/json.ts @@ -3,14 +3,14 @@ import { json } from 'express'; // Custom JSON middleware configuration const configuredJson = json({ limit: '1mb', - strict: false, - type: ['application/json', 'application/vnd.api+json'], - reviver: (key: string, value: any): any => { + reviver: (key: string, value: unknown): unknown => { if (key === 'date' && typeof value === 'string') { return new Date(value); } return value; }, + strict: false, + type: ['application/json', 'application/vnd.api+json'], }); -export default configuredJson; \ No newline at end of file +export default configuredJson; diff --git a/src/midleware/rateLimiter.ts b/src/midleware/rateLimiter.ts index 064df85..6a3351a 100644 --- a/src/midleware/rateLimiter.ts +++ b/src/midleware/rateLimiter.ts @@ -1,8 +1,8 @@ import rateLimit from 'express-rate-limit'; const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per window + windowMs: 15 * 60 * 1000, // 15 minutes }); -export default limiter; \ No newline at end of file +export default limiter; diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts index b5ee04b..917e946 100644 --- a/src/routes/authRoute.ts +++ b/src/routes/authRoute.ts @@ -1,7 +1,8 @@ -import { Request, Response, Router } from 'express'; import bcrypt from 'bcrypt'; +import { Request, Response, Router } from 'express'; import jwt from 'jsonwebtoken'; import { ObjectId } from 'mongodb'; + import { getDatabase } from '../database/mongo-common'; const router: Router = Router(); @@ -19,7 +20,7 @@ interface User { // Register endpoint router.post('/register', async (req: Request, res: Response): Promise => { - const { email, password } = req.body; + const { email, password } = req.body as User; if (!email || !password) { res.status(400).json({ message: 'Email and password are required' }); @@ -50,7 +51,7 @@ router.post('/register', async (req: Request, res: Response): Promise => { // Login endpoint router.post('/login', async (req: Request, res: Response): Promise => { - const { email, password } = req.body; + const { email, password } = req.body as User; try { const db = await getDatabase(); @@ -67,7 +68,7 @@ router.post('/login', async (req: Request, res: Response): Promise => { res.status(401).json({ message: 'Invalid credentials' }); } - const token = jwt.sign({ userId: user._id, email: user.email }, JWT_SECRET, { + const token = jwt.sign({ email: user.email, userId: user._id }, JWT_SECRET, { expiresIn: '1h', }); @@ -88,7 +89,7 @@ router.get('/me', async (req: Request, res: Response): Promise => { } try { - const decoded = jwt.verify(token, JWT_SECRET) as { userId: string; email: string }; + const decoded = jwt.verify(token, JWT_SECRET) as { email: string; userId: string }; const db = await getDatabase(); const usersCollection = db.collection('users'); @@ -104,6 +105,6 @@ router.get('/me', async (req: Request, res: Response): Promise => { console.error('Error fetching user data:', error); res.status(500).json({ message: 'Internal server error' }); } -}) +}); export default router; diff --git a/src/routes/healthRoute.ts b/src/routes/healthRoute.ts index 7b5a07b..ea2b486 100644 --- a/src/routes/healthRoute.ts +++ b/src/routes/healthRoute.ts @@ -3,11 +3,11 @@ import { Request, Response, Router } from 'express'; const router: Router = Router(); router.get('/', (req: Request, res: Response): void => { - res.status(200).json({ - status: 'OK', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - }); + res.status(200).json({ + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + }); }); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/notFoundRoute.ts b/src/routes/notFoundRoute.ts index 99897dd..258aef3 100644 --- a/src/routes/notFoundRoute.ts +++ b/src/routes/notFoundRoute.ts @@ -3,12 +3,12 @@ import { Request, Response, Router } from 'express'; const router: Router = Router(); router.all('*', (req: Request, res: Response) => { - res.status(404).json({ - message: 'Route not found', - method: req.method, - endpoint: req.originalUrl, - timestamp: new Date().toISOString(), - }); + res.status(404).json({ + endpoint: req.originalUrl, + message: 'Route not found', + method: req.method, + timestamp: new Date().toISOString(), + }); }); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/rootRoute.ts b/src/routes/rootRoute.ts index 833840c..e140900 100644 --- a/src/routes/rootRoute.ts +++ b/src/routes/rootRoute.ts @@ -6,4 +6,4 @@ router.get('/', (_: Request, res: Response): void => { res.redirect('/health'); }); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/storesRoute.ts b/src/routes/storesRoute.ts index 505bae4..dc0270c 100644 --- a/src/routes/storesRoute.ts +++ b/src/routes/storesRoute.ts @@ -1,5 +1,6 @@ -import { Router, Request, Response } from 'express'; -import { deleteStore, updateStore, createStore, getStores } from '../database/stores'; +import { Request, Response, Router } from 'express'; + +import { createStore, deleteStore, getStores, Store, updateStore } from '../database/stores'; const router: Router = Router(); @@ -9,7 +10,7 @@ router.get('/', async (req: Request, res: Response): Promise => { }); router.post('/', async (req: Request, res: Response): Promise => { - const newStore = req.body; + const newStore = req.body as Store; const createdStore = await createStore(newStore); res.status(201).json(createdStore); }); @@ -22,9 +23,9 @@ router.delete('/:_id', async (req: Request, res: Response): Promise => { // Endpoint to update a Store router.put('/:_id', async (req: Request, res: Response): Promise => { - const updatedStore = req.body; + const updatedStore = req.body as Store; const result = await updateStore(req.params._id, updatedStore); res.json(result); }); -export default router; \ No newline at end of file +export default router; diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index ee4f35b..7c9d519 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -10,4 +10,4 @@ function getGitUserName(): string { } } -export default getGitUserName; +export default getGitUserName \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0b62d54..4cf4576 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,24 @@ { "compilerOptions": { - "target": "ES6", // Use ES6 as the output target for better module handling - "module": "CommonJS", // Use CommonJS modules since Node.js uses them - "moduleResolution": "node", // Use Node module resolution for imports - "esModuleInterop": true, // Allow default imports from non-ES modules - "skipLibCheck": true, // Skip type checking of declaration files for faster builds - "strict": true, // Enable strict type-checking options + "target": "ES6", // Use ES6 as the output target for better module handling + "module": "CommonJS", // Use CommonJS modules since Node.js uses them + "moduleResolution": "node", // Use Node module resolution for imports + "esModuleInterop": true, // Allow default imports from non-ES modules + "skipLibCheck": true, // Skip type checking of declaration files for faster builds + "strict": true, // Enable strict type-checking options "forceConsistentCasingInFileNames": true, // Enforce consistent casing in file names - "outDir": "./dist", // Specify where compiled JavaScript files go - "baseUrl": ".", // Base URL to resolve non-relative modules - "types": ["node", "jest"], // Include types for Node.js and Jest for testing - "allowJs": true, // Allow JavaScript files to be included in the compilation - "resolveJsonModule": true, // Allow importing of JSON files + "outDir": "./dist", // Specify where compiled JavaScript files go + "baseUrl": ".", // Base URL to resolve non-relative modules + "types": ["node", "jest"], // Include types for Node.js and Jest for testing + "allowJs": true, // Allow JavaScript files to be included in the compilation + "resolveJsonModule": true // Allow importing of JSON files }, "include": [ - "src/**/*.ts", // Include all TS files in the `src` folder - "tests/**/*.ts" // Include test files in a separate `tests` folder + "src/**/*.ts", // Include all TS files in the `src` folder + "tests/**/*.ts" // Include test files in a separate `tests` folder ], "exclude": [ - "node_modules", // Exclude node_modules - "dist" // Exclude the `dist` folder where compiled JS files are stored + "node_modules", // Exclude node_modules + "dist" // Exclude the `dist` folder where compiled JS files are stored ] } From ba1f6aa7548723cc0f2d939ac1201feff4895782 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 01:46:39 -0400 Subject: [PATCH 45/63] fix wrong response body check on test --- src/http_tests/authentication.test.ts | 5 +++-- src/index.ts | 2 +- src/utils/git-user-name.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/http_tests/authentication.test.ts b/src/http_tests/authentication.test.ts index 3f65a2e..986a5fc 100644 --- a/src/http_tests/authentication.test.ts +++ b/src/http_tests/authentication.test.ts @@ -37,12 +37,13 @@ describe('Authentication JWT', () => { const loginRes: Response = await request(app).post('/auth/login').send(testUser).expect(200); const body = loginRes.body as AuthResponse; - const res = await request(app) + const res: Response = await request(app) .get('/auth/me') .set('Authorization', `Bearer ${body.token}`) .expect(200); + const userBody = res.body as { email: string }; expect(res.statusCode).toEqual(200); - expect(body).toHaveProperty('email', testUser.email); + expect(userBody).toHaveProperty('email', testUser.email); }); }); diff --git a/src/index.ts b/src/index.ts index 4f915e5..42dbbc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,4 +64,4 @@ async function startServer(): Promise { }); }); -void startServer() \ No newline at end of file +void startServer(); diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index 7c9d519..ee4f35b 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -10,4 +10,4 @@ function getGitUserName(): string { } } -export default getGitUserName \ No newline at end of file +export default getGitUserName; From 0728c34183e3edf3f9e4aba5ac841e3da701a997 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 01:50:39 -0400 Subject: [PATCH 46/63] fix bug re: failed test --- src/database/stores.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/stores.ts b/src/database/stores.ts index 6579687..a942f68 100644 --- a/src/database/stores.ts +++ b/src/database/stores.ts @@ -5,7 +5,7 @@ import { getDatabase } from './mongo-common'; // Define the Store interface export interface Store { - _id?: ObjectId; + _id: ObjectId; addedBy?: string; metadata?: string; name: string; @@ -45,7 +45,7 @@ async function getStores(): Promise { const database = await getDatabase(); const stores = await database.collection(collectionName).find({}).toArray(); return stores.map(store => ({ - _id: new ObjectId(store._id), + _id: store._id, addedBy: store.addedBy, name: store.name, })); From 030edb99c874d2984b8583d9fade3ff7676c215d Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sat, 17 May 2025 01:54:14 -0400 Subject: [PATCH 47/63] Update tests.yml The >> .env appends the line to a .env file at the repo root. node -e executes an inline script that prints a secure 64-byte random hex string. --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f617707..1a7edd2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,10 @@ jobs: # Ensure consistent installs with npm ci - name: Run npm ci (ensure clean node_modules) run: npm ci + + # Generate JWT secret key for testing + - name: Generate JWT_SECRET and save to .env + run: echo "JWT_SECRET=$(node -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\")" >> .env - name: Run tests run: npm test From f22ef8df15cca9cc148d10a0cc976f471b5f4e1e Mon Sep 17 00:00:00 2001 From: Erick Pacheco Date: Sat, 17 May 2025 02:02:04 -0400 Subject: [PATCH 48/63] Update tests.yml Add JWT_SECRET: ${{ secrets.JWT_SECRET }} --- .github/workflows/tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1a7edd2..92f3f43 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,12 +29,9 @@ jobs: # Ensure consistent installs with npm ci - name: Run npm ci (ensure clean node_modules) run: npm ci - - # Generate JWT secret key for testing - - name: Generate JWT_SECRET and save to .env - run: echo "JWT_SECRET=$(node -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\")" >> .env - name: Run tests run: npm test env: NODE_ENV: test + JWT_SECRET: ${{ secrets.JWT_SECRET }} From ca9dffa12344208beb58f1c50695c8c54ce31023 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 02:11:26 -0400 Subject: [PATCH 49/63] fix store responses --- README.md | 2 +- src/database/stores.ts | 2 +- src/index.ts | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index acb2acb..e6e3159 100755 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Environmental Variables: 3. ALLOWED_ORIGINS (optional) 4. ALLOWED_METHODS (optional) 5. ALLOWED_HEADERS (optional) -6. NODE\*ENV=test --- When set to \*\*\*"test"\_\*\*, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. +6. NODE_ENV=test --- When set to "test", a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. 7. JWT_SECRET --- A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs). This is required for authentication to work correctly. Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: `bash node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` 8. diff --git a/src/database/stores.ts b/src/database/stores.ts index a942f68..d7f7043 100644 --- a/src/database/stores.ts +++ b/src/database/stores.ts @@ -18,7 +18,7 @@ async function createStore(store: Store): Promise { const database = await getDatabase(); store.addedBy = getUserName(); - const storeToInsert = { ...store, _id: store._id ? new ObjectId(store._id) : undefined }; + const storeToInsert = { ...store, _id: store._id }; const { insertedId } = await database.collection(collectionName).insertOne(storeToInsert); // Return the store document with the inserted _id diff --git a/src/index.ts b/src/index.ts index 42dbbc8..c457226 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,15 +32,14 @@ function gracefulShutdown(signal: string): void { async function startServer(): Promise { if (!MONGO_URL) { - // Gracefully handle missing DB config - app.all('*', (req, res) => { + app.all('*', (_, res) => { res.status(500).send({ message: 'MONGO_URL not configured. See documentation.', }); }); server = app.listen(PORT, () => { - console.log(`Server running without DB on port ${PORT.toString()}`); + console.log(`Server running without DB on port ${String(PORT)}`); }); return; @@ -50,7 +49,7 @@ async function startServer(): Promise { await startDatabase(); server = app.listen(PORT, () => { - console.log(`Server started on port ${PORT.toString()}`); + console.log(`Server started on port ${String(PORT)}`); }); } catch (err: unknown) { console.error('Failed to start database:', err); From 11cf818233f0c1e75a8b8a37b1dc886b4af2e2f7 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 02:15:42 -0400 Subject: [PATCH 50/63] This commit enhances the function to detect the execution environment. When running within a GitHub Actions workflow, it now returns the . --- src/utils/git-user-name.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index ee4f35b..85e7a7a 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -1,12 +1,17 @@ import { execSync } from 'child_process'; function getGitUserName(): string { - try { - const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); - return name || 'unknown'; - } catch (err) { - console.info('Git user name not found, returning "unknown"', err); - return 'unknown'; + if (process.env.GITHUB_ACTIONS === 'true') { + const githubActor = process.env.GITHUB_ACTOR; + return githubActor || 'github-actions'; + } else { + try { + const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); + return name || 'unknown'; + } catch (err) { + console.info('Git user name not found, returning "unknown"', err); + return 'unknown'; + } } } From c1d9928270977233b7e4cee78e9d0ab4a1922f2d Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 02:41:49 -0400 Subject: [PATCH 51/63] add auth middleware, correct type in git-username --- src/http_tests/authentication.test.ts | 13 +++++++++++ src/midleware/authMiddleware.ts | 31 +++++++++++++++++++++++++++ src/routes/authRoute.ts | 3 ++- src/utils/git-user-name.ts | 2 +- 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/midleware/authMiddleware.ts diff --git a/src/http_tests/authentication.test.ts b/src/http_tests/authentication.test.ts index 986a5fc..a0bbacd 100644 --- a/src/http_tests/authentication.test.ts +++ b/src/http_tests/authentication.test.ts @@ -46,4 +46,17 @@ describe('Authentication JWT', () => { expect(res.statusCode).toEqual(200); expect(userBody).toHaveProperty('email', testUser.email); }); + + it('should reject request with invalid token', async () => { + const invalidToken = 'this.is.an.invalid.token'; + + const res: Response = await request(app) + .get('/auth/me') + .set('Authorization', `Bearer ${invalidToken}`) + .expect(401); + + const body = res.body as AuthResponse; + expect(body).toHaveProperty('message'); + expect(typeof body.message).toBe('string'); + }); }); diff --git a/src/midleware/authMiddleware.ts b/src/midleware/authMiddleware.ts new file mode 100644 index 0000000..87a1733 --- /dev/null +++ b/src/midleware/authMiddleware.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET; + +if (!JWT_SECRET) { + throw new Error('JWT_SECRET is not defined'); +} + +// Extend the Request interface to include the user payload from the token + +const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { + const authHeader = req.headers.authorization; + const token = authHeader?.split(' ')[1]; + + if (!token) { + res.status(401).json({ message: 'No token provided' }); + return; + } + + jwt.verify(token, JWT_SECRET, (err, payload) => { + if (err && !payload) { + res.status(401).json({ message: 'Invalid or expired token' }); + return; + } + + next(); + }); +}; + +export default authMiddleware; diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts index 917e946..f006429 100644 --- a/src/routes/authRoute.ts +++ b/src/routes/authRoute.ts @@ -4,6 +4,7 @@ import jwt from 'jsonwebtoken'; import { ObjectId } from 'mongodb'; import { getDatabase } from '../database/mongo-common'; +import authMiddleware from '../midleware/authMiddleware'; const router: Router = Router(); const JWT_SECRET = process.env.JWT_SECRET; @@ -80,7 +81,7 @@ router.post('/login', async (req: Request, res: Response): Promise => { }); // responds with user data -router.get('/me', async (req: Request, res: Response): Promise => { +router.get('/me', authMiddleware, async (req: Request, res: Response): Promise => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index 85e7a7a..a9b9247 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -3,7 +3,7 @@ import { execSync } from 'child_process'; function getGitUserName(): string { if (process.env.GITHUB_ACTIONS === 'true') { const githubActor = process.env.GITHUB_ACTOR; - return githubActor || 'github-actions'; + return githubActor ?? 'github-actions'; } else { try { const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); From 3be402fee9fe1dd97ad67a77fc0b47e1b47ff8ac Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 02:44:20 -0400 Subject: [PATCH 52/63] make the intent of the condition clearer and reduce potential confusion --- src/midleware/authMiddleware.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/midleware/authMiddleware.ts b/src/midleware/authMiddleware.ts index 87a1733..99386b4 100644 --- a/src/midleware/authMiddleware.ts +++ b/src/midleware/authMiddleware.ts @@ -18,8 +18,8 @@ const authMiddleware = (req: Request, res: Response, next: NextFunction): void = return; } - jwt.verify(token, JWT_SECRET, (err, payload) => { - if (err && !payload) { + jwt.verify(token, JWT_SECRET, err => { + if (err) { res.status(401).json({ message: 'Invalid or expired token' }); return; } From ba03b045e7c177a716afe5c77f1cde2ee8f3469d Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 02:52:39 -0400 Subject: [PATCH 53/63] update documentation --- README.md | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e6e3159..9adec7f 100755 --- a/README.md +++ b/README.md @@ -2,32 +2,45 @@ [![Backend API - CI Tests](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml) [![Known Vulnerabilities](https://snyk.io/test/github/pakeku/backend-api/badge.svg)](https://snyk.io/test/github/pakeku/backend-api) - -## Requirements - -Identify your MongoDB URL. Visit MongoDB to sign up and get started. - -Environmental Variables: - -1. MONGO_URL -2. PORT (optional) -3. ALLOWED_ORIGINS (optional) -4. ALLOWED_METHODS (optional) -5. ALLOWED_HEADERS (optional) -6. NODE_ENV=test --- When set to "test", a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. -7. JWT_SECRET --- A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs). This is required for authentication to work correctly. - Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: `bash node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` -8. +[![code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat&logo=prettier)](https://prettier.io/) +[![ESLint](https://img.shields.io/badge/linting-eslint-blue.svg?style=flat&logo=eslint)](https://eslint.org/) +[![TypeScript](https://img.shields.io/badge/language-typescript-blue.svg?style=flat&logo=typescript)](https://www.typescriptlang.org/) + +## Configuration + +This backend API is configured using environment variables. You can set these variables in a `.env` file in the root of the project (copy `.env.sample` to `.env` and modify the values). **Important: Ensure your `.env` file is not committed to your version control system.** + +The following environment variables are used: + +- `MONGO_URL`: **Required.** The connection string for your MongoDB database. You can obtain this from your MongoDB provider (e.g., MongoDB Atlas). Example: `mongodb+srv://:@/?retryWrites=true&w=majority` +- `PORT`: **Optional.** The port number that the Express server will listen on. If not specified, it defaults to `3000`. Example: `8080` +- `ALLOWED_ORIGINS`: **Optional.** A comma-separated list of allowed origins for Cross-Origin Resource Sharing (CORS). This controls which websites can make requests to your API. Example: `http://localhost:3000,https://your-frontend.com` +- `ALLOWED_METHODS`: **Optional.** A comma-separated list of allowed HTTP methods for CORS. Example: `GET,POST,PUT,DELETE` +- `ALLOWED_HEADERS`: **Optional.** A comma-separated list of allowed request headers for CORS. Example: `Content-Type,Authorization` +- `NODE_ENV`: Specifies the application environment. + - `test`: When set to `test`, the application will use an in-memory MongoDB server provided by `mongodb-memory-server` for testing purposes. In this mode, the `MONGO_URL` variable is not required. + - `development`: Typically used during development. You might have specific development configurations. + - `production`: Used in the production environment. +- `JWT_SECRET`: **Required and Critically Important.** A secret key used to sign and verify JSON Web Tokens (JWTs) for authentication. **This should be a long, random, and secure string. Do not share or expose this secret.** + ```bash + node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" + ``` ## Getting Started -1. Copy this file to .env and fill in the actual values +Copy this file to .env and fill in the actual values ```bash cp .env.sample .env ``` -1. Run a script: +Install Dependencies + +```bash +npm run install +``` + +Run a script: ```json "scripts": { From e26c4419a13881ebe6a6d02c3c909541bc1114c7 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 04:25:27 -0400 Subject: [PATCH 54/63] feat: add swagger documentation --- README.md | 1 + package.json | 13 ++++++++----- src/app.ts | 3 +++ src/documentation/swaggerOptions.ts | 18 ++++++++++++++++++ src/midleware/authMiddleware.ts | 2 -- src/routes/healthRoute.ts | 29 ++++++++++++++++++++++++++++- 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 src/documentation/swaggerOptions.ts diff --git a/README.md b/README.md index 9adec7f..c434b21 100755 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat&logo=prettier)](https://prettier.io/) [![ESLint](https://img.shields.io/badge/linting-eslint-blue.svg?style=flat&logo=eslint)](https://eslint.org/) [![TypeScript](https://img.shields.io/badge/language-typescript-blue.svg?style=flat&logo=typescript)](https://www.typescriptlang.org/) +[![Swagger UI](https://img.shields.io/badge/docs-Swagger_UI-blue?logo=swagger)](http://localhost:3000/api-docs) ## Configuration diff --git a/package.json b/package.json index d325ae9..a8806b5 100755 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "1.0.1", "description": "", "main": "./src/index.ts", - "type": "module", "private": true, "scripts": { "prebuild": "rm -rf dist", @@ -29,31 +28,35 @@ "author": "", "license": "ISC", "dependencies": { + "@types/swagger-jsdoc": "^6.0.4", "bcrypt": "^6.0.0", "compression": "^1.8.0", "cors": "^2.8.5", "dotenv": "^16.5.0", "env-cmd": "^10.1.0", - "express": "^4.16.4", + "express": "^4.21.2", "express-rate-limit": "^7.5.0", "helmet": "^3.15.1", "jsonwebtoken": "^9.0.2", "mongodb": "^6.16.0", "morgan": "^1.9.1", - "nodemon": "^3.1.10" + "nodemon": "^3.1.10", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@eslint/js": "^9.27.0", "@types/bcrypt": "^5.0.2", "@types/compression": "^1.7.5", "@types/cors": "^2.8.18", - "@types/express": "^5.0.1", + "@types/express": "^5.0.2", "@types/helmet": "^0.0.48", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.9", "@types/morgan": "^1.9.9", - "@types/node": "^22.15.17", + "@types/node": "^22.15.18", "@types/supertest": "^6.0.3", + "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", "eslint": "^9.27.0", diff --git a/src/app.ts b/src/app.ts index 3ac052c..7925e2c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,6 +12,8 @@ import healthRouter from './routes/healthRoute'; import notFoundRouter from './routes/notFoundRoute'; import rootRouter from './routes/rootRoute'; import storesRouter from './routes/storesRoute'; +import swaggerSpec from './documentation/swaggerOptions'; +import swaggerUi from 'swagger-ui-express'; const app: Application = express(); @@ -32,6 +34,7 @@ app.use('/', rootRouter); app.use('/health', healthRouter); app.use('/stores', storesRouter); app.use('/auth', authRouter); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); app.use('*', notFoundRouter); export default app; diff --git a/src/documentation/swaggerOptions.ts b/src/documentation/swaggerOptions.ts new file mode 100644 index 0000000..583a26a --- /dev/null +++ b/src/documentation/swaggerOptions.ts @@ -0,0 +1,18 @@ +import swaggerJsdoc, { Options } from 'swagger-jsdoc'; +import { name } from '../../package.json'; + +const options: Options = { + definition: { + openapi: '3.0.0', + info: { + title: name, + version: '1.0.0', + description: 'A sample API documentation', + }, + }, + apis: ['./src/**/*.ts'], // recursive, includes subfolders like ./src/routes +}; + +const swaggerSpec = swaggerJsdoc(options); + +export default swaggerSpec; diff --git a/src/midleware/authMiddleware.ts b/src/midleware/authMiddleware.ts index 99386b4..4bd2016 100644 --- a/src/midleware/authMiddleware.ts +++ b/src/midleware/authMiddleware.ts @@ -7,8 +7,6 @@ if (!JWT_SECRET) { throw new Error('JWT_SECRET is not defined'); } -// Extend the Request interface to include the user payload from the token - const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { const authHeader = req.headers.authorization; const token = authHeader?.split(' ')[1]; diff --git a/src/routes/healthRoute.ts b/src/routes/healthRoute.ts index ea2b486..b2bee86 100644 --- a/src/routes/healthRoute.ts +++ b/src/routes/healthRoute.ts @@ -1,7 +1,34 @@ import { Request, Response, Router } from 'express'; const router: Router = Router(); - +/** + * @swagger + * /health: + * get: + * tags: + * - Health + * summary: Health check + * description: Returns service status, current timestamp, and uptime. + * responses: + * 200: + * description: Service is running + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: OK + * timestamp: + * type: string + * format: date-time + * example: "2025-05-17T12:34:56.789Z" + * uptime: + * type: number + * description: Time in seconds since the server started + * example: 123.456 + */ router.get('/', (req: Request, res: Response): void => { res.status(200).json({ status: 'OK', From ab14e0ca755db7e509b9df74a0e94aa640c1df6b Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 04:30:36 -0400 Subject: [PATCH 55/63] update documentation, orgnize --- README.md | 44 +++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index c434b21..3ec250e 100755 --- a/README.md +++ b/README.md @@ -8,41 +8,31 @@ [![Swagger UI](https://img.shields.io/badge/docs-Swagger_UI-blue?logo=swagger)](http://localhost:3000/api-docs) ## Configuration - -This backend API is configured using environment variables. You can set these variables in a `.env` file in the root of the project (copy `.env.sample` to `.env` and modify the values). **Important: Ensure your `.env` file is not committed to your version control system.** - -The following environment variables are used: - -- `MONGO_URL`: **Required.** The connection string for your MongoDB database. You can obtain this from your MongoDB provider (e.g., MongoDB Atlas). Example: `mongodb+srv://:@/?retryWrites=true&w=majority` -- `PORT`: **Optional.** The port number that the Express server will listen on. If not specified, it defaults to `3000`. Example: `8080` -- `ALLOWED_ORIGINS`: **Optional.** A comma-separated list of allowed origins for Cross-Origin Resource Sharing (CORS). This controls which websites can make requests to your API. Example: `http://localhost:3000,https://your-frontend.com` -- `ALLOWED_METHODS`: **Optional.** A comma-separated list of allowed HTTP methods for CORS. Example: `GET,POST,PUT,DELETE` -- `ALLOWED_HEADERS`: **Optional.** A comma-separated list of allowed request headers for CORS. Example: `Content-Type,Authorization` -- `NODE_ENV`: Specifies the application environment. - - `test`: When set to `test`, the application will use an in-memory MongoDB server provided by `mongodb-memory-server` for testing purposes. In this mode, the `MONGO_URL` variable is not required. - - `development`: Typically used during development. You might have specific development configurations. - - `production`: Used in the production environment. -- `JWT_SECRET`: **Required and Critically Important.** A secret key used to sign and verify JSON Web Tokens (JWTs) for authentication. **This should be a long, random, and secure string. Do not share or expose this secret.** - ```bash - node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" - ``` +You can define your environmental variables in a `.env` file at the root of the project. (Start by copying `.env.sample` → `.env`).\ +**⚠️ Important:** Never commit your `.env` file to version control. + +| Variable | Required | Description | Example | +| --- | --- | --- | --- | +| `MONGO_URL` | ✅ Yes | Connection string for MongoDB. Obtain this from your MongoDB provider. | `mongodb+srv://:@/?retryWrites=true&w=majority` | +| `PORT` | ❌ No | Port for the Express server to listen on. Defaults to `3000`. | `8080` | +| `ALLOWED_ORIGINS` | ❌ No | Comma-separated list of allowed origins for CORS. | `http://localhost:3000,https://your-frontend.com` | +| `ALLOWED_METHODS` | ❌ No | Comma-separated list of allowed HTTP methods for CORS. | `GET,POST,PUT,DELETE` | +| `ALLOWED_HEADERS` | ❌ No | Comma-separated list of allowed request headers for CORS. | `Content-Type,Authorization` | +| `NODE_ENV` | ⚠️ Depends | Application environment: `development`, `production`, or `test`. `MONGO_URL` not required in `test`. | `development` | +| `JWT_SECRET` | ✅ Yes | Secret key used for signing/verifying JWTs. Must be secure and private. | `Use: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` | ## Getting Started - -Copy this file to .env and fill in the actual values - +1. Install Dependencies ```bash -cp .env.sample .env +npm run install ``` -Install Dependencies - +2. Create .env file and gather your variable values. ```bash -npm run install +cp .env.sample .env ``` -Run a script: - +3. Run script: ```json "scripts": { "prebuild":"rm -rf dist", From 31c88493e04282fd53676526ba488cbacc5f48c9 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 04:39:07 -0400 Subject: [PATCH 56/63] format --- README.md | 25 +++++++++++++++---------- src/app.ts | 4 ++-- src/documentation/swaggerOptions.ts | 7 ++++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3ec250e..7ee8bcb 100755 --- a/README.md +++ b/README.md @@ -8,31 +8,36 @@ [![Swagger UI](https://img.shields.io/badge/docs-Swagger_UI-blue?logo=swagger)](http://localhost:3000/api-docs) ## Configuration + You can define your environmental variables in a `.env` file at the root of the project. (Start by copying `.env.sample` → `.env`).\ **⚠️ Important:** Never commit your `.env` file to version control. -| Variable | Required | Description | Example | -| --- | --- | --- | --- | -| `MONGO_URL` | ✅ Yes | Connection string for MongoDB. Obtain this from your MongoDB provider. | `mongodb+srv://:@/?retryWrites=true&w=majority` | -| `PORT` | ❌ No | Port for the Express server to listen on. Defaults to `3000`. | `8080` | -| `ALLOWED_ORIGINS` | ❌ No | Comma-separated list of allowed origins for CORS. | `http://localhost:3000,https://your-frontend.com` | -| `ALLOWED_METHODS` | ❌ No | Comma-separated list of allowed HTTP methods for CORS. | `GET,POST,PUT,DELETE` | -| `ALLOWED_HEADERS` | ❌ No | Comma-separated list of allowed request headers for CORS. | `Content-Type,Authorization` | -| `NODE_ENV` | ⚠️ Depends | Application environment: `development`, `production`, or `test`. `MONGO_URL` not required in `test`. | `development` | -| `JWT_SECRET` | ✅ Yes | Secret key used for signing/verifying JWTs. Must be secure and private. | `Use: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` | +| Variable | Required | Description | Example | +| ----------------- | ---------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `MONGO_URL` | ✅ Yes | Connection string for MongoDB. Obtain this from your MongoDB provider. | `mongodb+srv://:@/?retryWrites=true&w=majority` | +| `PORT` | ❌ No | Port for the Express server to listen on. Defaults to `3000`. | `8080` | +| `ALLOWED_ORIGINS` | ❌ No | Comma-separated list of allowed origins for CORS. | `http://localhost:3000,https://your-frontend.com` | +| `ALLOWED_METHODS` | ❌ No | Comma-separated list of allowed HTTP methods for CORS. | `GET,POST,PUT,DELETE` | +| `ALLOWED_HEADERS` | ❌ No | Comma-separated list of allowed request headers for CORS. | `Content-Type,Authorization` | +| `NODE_ENV` | ⚠️ Depends | Application environment: `development`, `production`, or `test`. `MONGO_URL` not required in `test`. | `development` | +| `JWT_SECRET` | ✅ Yes | Secret key used for signing/verifying JWTs. Must be secure and private. | `Use: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` | ## Getting Started -1. Install Dependencies + +1. Install Dependencies + ```bash npm run install ``` 2. Create .env file and gather your variable values. + ```bash cp .env.sample .env ``` 3. Run script: + ```json "scripts": { "prebuild":"rm -rf dist", diff --git a/src/app.ts b/src/app.ts index 7925e2c..cd55ad3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,7 @@ import express, { Application } from 'express'; +import swaggerUi from 'swagger-ui-express'; +import swaggerSpec from './documentation/swaggerOptions'; import compression from './midleware/compression'; import cors from './midleware/cors'; import errorHandler from './midleware/errorHandler'; @@ -12,8 +14,6 @@ import healthRouter from './routes/healthRoute'; import notFoundRouter from './routes/notFoundRoute'; import rootRouter from './routes/rootRoute'; import storesRouter from './routes/storesRoute'; -import swaggerSpec from './documentation/swaggerOptions'; -import swaggerUi from 'swagger-ui-express'; const app: Application = express(); diff --git a/src/documentation/swaggerOptions.ts b/src/documentation/swaggerOptions.ts index 583a26a..5c62a57 100644 --- a/src/documentation/swaggerOptions.ts +++ b/src/documentation/swaggerOptions.ts @@ -1,16 +1,17 @@ import swaggerJsdoc, { Options } from 'swagger-jsdoc'; + import { name } from '../../package.json'; const options: Options = { + apis: ['./src/**/*.ts'], // recursive, includes subfolders like ./src/routes definition: { - openapi: '3.0.0', info: { + description: 'A sample API documentation', title: name, version: '1.0.0', - description: 'A sample API documentation', }, + openapi: '3.0.0', }, - apis: ['./src/**/*.ts'], // recursive, includes subfolders like ./src/routes }; const swaggerSpec = swaggerJsdoc(options); From 905023f84b0a3219192a77df2fd287520e1ee154 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 11:34:46 -0400 Subject: [PATCH 57/63] add type:module --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a8806b5..80f4610 100755 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "", "main": "./src/index.ts", "private": true, + "type": "module", "scripts": { "prebuild": "rm -rf dist", "build": "tsc", From 48fedea15195a72f483f708d2dcc4d64a4e93bcb Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 11:51:29 -0400 Subject: [PATCH 58/63] remove type:module --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 80f4610..a8806b5 100755 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./src/index.ts", "private": true, - "type": "module", "scripts": { "prebuild": "rm -rf dist", "build": "tsc", From 012a894d009b3ca7926ce0b9b4a070fdc4cc127c Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 11:56:50 -0400 Subject: [PATCH 59/63] change config to use module.exports instead of export default --- jest.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jest.config.js b/jest.config.js index 7c534e1..d5aeae5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} **/ -export default { +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { testEnvironment: 'node', transform: { - '^.+\.tsx?$': ['ts-jest', {}], + '^.+\\.tsx?$': ['ts-jest', {}], }, }; From 751b4706e7b0138acac4896a9985b20e5fddd4ea Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 12:21:06 -0400 Subject: [PATCH 60/63] lint --- .gitignore | 3 ++ jest.config.js | 12 +++++ package.json | 1 + src/{http_tests => __tests__/http}/app.http | 0 .../http}/app.test.ts | 2 +- .../http}/authentication.test.ts | 4 +- .../http}/stores.http | 0 .../http}/stores.test.ts | 4 +- src/__tests__/unit/errorHandler.test.ts | 50 +++++++++++++++++++ src/__tests__/unit/git-user-name.test.ts | 36 +++++++++++++ src/__tests__/unit/stores.test.ts | 37 ++++++++++++++ 11 files changed, 144 insertions(+), 5 deletions(-) rename src/{http_tests => __tests__/http}/app.http (100%) rename src/{http_tests => __tests__/http}/app.test.ts (96%) rename src/{http_tests => __tests__/http}/authentication.test.ts (95%) rename src/{http_tests => __tests__/http}/stores.http (100%) mode change 100755 => 100644 rename src/{http_tests => __tests__/http}/stores.test.ts (95%) create mode 100644 src/__tests__/unit/errorHandler.test.ts create mode 100644 src/__tests__/unit/git-user-name.test.ts create mode 100644 src/__tests__/unit/stores.test.ts diff --git a/.gitignore b/.gitignore index a86b657..dd4b88e 100755 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ typings/ .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml !.elasticbeanstalk/*.global.yml + +# Test Coverage +coverage \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index d5aeae5..4a524fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,4 +4,16 @@ module.exports = { transform: { '^.+\\.tsx?$': ['ts-jest', {}], }, + coverageDirectory: 'coverage', + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/*.test.{ts,tsx}', + '!src/**/index.ts', + '!src/**/types.ts', + ], + coverageReporters: ['text', 'lcov'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + modulePathIgnorePatterns: ['/dist/'], }; diff --git a/package.json b/package.json index a8806b5..33038c6 100755 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev": "env-cmd ts-node src/index.ts", "test": "jest", "test:watch": "jest --watch", + "test:coverage": "jest --coverage", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "lint:check": "eslint . --ext .ts --no-ignore", diff --git a/src/http_tests/app.http b/src/__tests__/http/app.http similarity index 100% rename from src/http_tests/app.http rename to src/__tests__/http/app.http diff --git a/src/http_tests/app.test.ts b/src/__tests__/http/app.test.ts similarity index 96% rename from src/http_tests/app.test.ts rename to src/__tests__/http/app.test.ts index f148cfc..a0ccd0a 100644 --- a/src/http_tests/app.test.ts +++ b/src/__tests__/http/app.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import request, { Response } from 'supertest'; -import app from '../app'; +import app from '../../app'; interface ResponseBody { message?: string; diff --git a/src/http_tests/authentication.test.ts b/src/__tests__/http/authentication.test.ts similarity index 95% rename from src/http_tests/authentication.test.ts rename to src/__tests__/http/authentication.test.ts index a0bbacd..ed5bd33 100644 --- a/src/http_tests/authentication.test.ts +++ b/src/__tests__/http/authentication.test.ts @@ -1,8 +1,8 @@ import 'dotenv/config'; import request, { Response } from 'supertest'; -import app from '../app'; -import { stopDatabase } from '../database/mongo-common'; +import app from '../../app'; +import { stopDatabase } from '../../database/mongo-common'; // Define expected response shapes interface AuthResponse { diff --git a/src/http_tests/stores.http b/src/__tests__/http/stores.http old mode 100755 new mode 100644 similarity index 100% rename from src/http_tests/stores.http rename to src/__tests__/http/stores.http diff --git a/src/http_tests/stores.test.ts b/src/__tests__/http/stores.test.ts similarity index 95% rename from src/http_tests/stores.test.ts rename to src/__tests__/http/stores.test.ts index 9de28b4..91133f2 100644 --- a/src/http_tests/stores.test.ts +++ b/src/__tests__/http/stores.test.ts @@ -1,8 +1,8 @@ import 'dotenv/config'; import request, { Response } from 'supertest'; -import app from '../app'; // Adjust the path as necessary -import { stopDatabase } from '../database/mongo-common'; +import app from '../../app'; // Adjust the path as necessary +import { stopDatabase } from '../../database/mongo-common'; interface Store { _id?: string; // Optionally include the ID in responses diff --git a/src/__tests__/unit/errorHandler.test.ts b/src/__tests__/unit/errorHandler.test.ts new file mode 100644 index 0000000..1b1d34b --- /dev/null +++ b/src/__tests__/unit/errorHandler.test.ts @@ -0,0 +1,50 @@ +import { Request, Response } from 'express'; + +import errorHandler from '../../midleware/errorHandler'; + +describe('errorHandler middleware', () => { + const mockReq = {} as Request; + + let mockStatus: jest.Mock; + let mockJson: jest.Mock; + let mockRes: Response; + + beforeEach(() => { + mockStatus = jest.fn().mockReturnThis(); + mockJson = jest.fn(); + + mockRes = { + json: mockJson, + status: mockStatus, + } as unknown as Response; + }); + + const mockNext = jest.fn(); + + it('should respond with 500 and error message in non-production', () => { + process.env.NODE_ENV = 'development'; // or 'test' + const err = new Error('Something went wrong'); + + errorHandler(err, mockReq, mockRes, mockNext); + + expect(mockStatus).toHaveBeenCalledWith(500); + expect(mockJson).toHaveBeenCalledWith( + expect.objectContaining({ + error: 'Something went wrong', + message: 'Internal Server Error', + }) + ); + }); + + it('should not include error details in production', () => { + process.env.NODE_ENV = 'production'; + const err = new Error('Production error'); + + errorHandler(err, mockReq, mockRes, mockNext); + + expect(mockStatus).toHaveBeenCalledWith(500); + expect(mockJson).toHaveBeenCalledWith({ + message: 'Internal Server Error', + }); + }); +}); diff --git a/src/__tests__/unit/git-user-name.test.ts b/src/__tests__/unit/git-user-name.test.ts new file mode 100644 index 0000000..14f1b85 --- /dev/null +++ b/src/__tests__/unit/git-user-name.test.ts @@ -0,0 +1,36 @@ +import { execSync } from 'child_process'; + +import getGitUserName from '../../utils/git-user-name'; + +jest.mock('child_process'); + +describe('getGitUserName', () => { + afterEach(() => { + jest.resetAllMocks(); + delete process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_ACTOR; + }); + + it('returns GITHUB_ACTOR when running in GitHub Actions', () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_ACTOR = 'github-test-user'; + expect(getGitUserName()).toBe('github-test-user'); + }); + + it('returns "github-actions" if GITHUB_ACTOR is missing in GitHub Actions', () => { + process.env.GITHUB_ACTIONS = 'true'; + delete process.env.GITHUB_ACTOR; + expect(getGitUserName()).toBe('github-actions'); + }); + + it('returns git config user.name when not in GitHub Actions', () => { + (execSync as jest.Mock).mockReturnValue('Test User\n'); + expect(getGitUserName()).toBe('Test User'); + expect(execSync).toHaveBeenCalledWith('git config --get user.name', { encoding: 'utf8' }); + }); + + it('returns "unknown" if execSync throws', () => { + (execSync as jest.Mock).mockImplementation(() => { throw new Error('fail'); }); + expect(getGitUserName()).toBe('unknown'); + }); +}); diff --git a/src/__tests__/unit/stores.test.ts b/src/__tests__/unit/stores.test.ts new file mode 100644 index 0000000..e34ade2 --- /dev/null +++ b/src/__tests__/unit/stores.test.ts @@ -0,0 +1,37 @@ +// tests/database/stores.test.ts +import { ObjectId } from 'mongodb'; + +import { getDatabase } from '../../database/mongo-common'; +import { deleteStore, updateStore } from '../../database/stores'; + +jest.mock('../../database/mongo-common'); + +const mockCollection = { + deleteOne: jest.fn(), + findOne: jest.fn(), + updateOne: jest.fn(), +}; + +(getDatabase as jest.Mock).mockResolvedValue({ + collection: () => mockCollection, +}); + +describe('stores.ts unit tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return "No store found with that id" if delete count is 0', async () => { + mockCollection.deleteOne.mockResolvedValueOnce({ deletedCount: 0 }); + const response = await deleteStore(new ObjectId().toHexString()); + expect(response).toEqual({ message: 'No store found with that id' }); + }); + + it('should return null if store not found after update', async () => { + mockCollection.updateOne.mockResolvedValueOnce({}); + mockCollection.findOne.mockResolvedValueOnce(null); + + const result = await updateStore(new ObjectId().toHexString(), { name: 'Updated' }); + expect(result).toBeNull(); + }); +}); From fc3a162bb2e5b42f3a2c17588dd95b36ddd4361c Mon Sep 17 00:00:00 2001 From: pakeku Date: Sat, 17 May 2025 14:50:02 -0400 Subject: [PATCH 61/63] increase code coverage --- src/__tests__/unit/git-user-name.test.ts | 4 +++- src/midleware/errorHandler.ts | 2 -- src/utils/git-user-name.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/__tests__/unit/git-user-name.test.ts b/src/__tests__/unit/git-user-name.test.ts index 14f1b85..e1c97bb 100644 --- a/src/__tests__/unit/git-user-name.test.ts +++ b/src/__tests__/unit/git-user-name.test.ts @@ -30,7 +30,9 @@ describe('getGitUserName', () => { }); it('returns "unknown" if execSync throws', () => { - (execSync as jest.Mock).mockImplementation(() => { throw new Error('fail'); }); + (execSync as jest.Mock).mockImplementation(() => { + throw new Error('fail'); + }); expect(getGitUserName()).toBe('unknown'); }); }); diff --git a/src/midleware/errorHandler.ts b/src/midleware/errorHandler.ts index c2f4b60..71c76cc 100644 --- a/src/midleware/errorHandler.ts +++ b/src/midleware/errorHandler.ts @@ -3,8 +3,6 @@ import { NextFunction, Request, Response } from 'express'; // using _ to indicate that the parameter is not used // eslint-disable-next-line @typescript-eslint/no-unused-vars const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction): void => { - console.error(`[ERROR] ${err.stack ?? 'No stack trace available'}`); - const response = { message: 'Internal Server Error', ...(process.env.NODE_ENV !== 'production' && { error: err.message }), diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index a9b9247..7dc31b0 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -9,7 +9,7 @@ function getGitUserName(): string { const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); return name || 'unknown'; } catch (err) { - console.info('Git user name not found, returning "unknown"', err); + console.error('Error getting git user name:', err); return 'unknown'; } } From c38267e3ee29505e2857eaeb4e3bf2ac56226cf8 Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 18 May 2025 09:07:48 -0400 Subject: [PATCH 62/63] remove console.error log --- src/utils/git-user-name.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index 7dc31b0..ba5f02e 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -9,7 +9,6 @@ function getGitUserName(): string { const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); return name || 'unknown'; } catch (err) { - console.error('Error getting git user name:', err); return 'unknown'; } } From 0ca10f41104fdbdc9803f8fd1217eaa2eff963ba Mon Sep 17 00:00:00 2001 From: pakeku Date: Sun, 18 May 2025 09:39:46 -0400 Subject: [PATCH 63/63] improve docs --- src/documentation/swaggerOptions.ts | 22 +++++-- src/routes/authRoute.ts | 98 ++++++++++++++++++++++++++++- src/utils/git-user-name.ts | 1 + 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/documentation/swaggerOptions.ts b/src/documentation/swaggerOptions.ts index 5c62a57..a2bb2d2 100644 --- a/src/documentation/swaggerOptions.ts +++ b/src/documentation/swaggerOptions.ts @@ -1,16 +1,30 @@ import swaggerJsdoc, { Options } from 'swagger-jsdoc'; -import { name } from '../../package.json'; +import { description, name, version } from '../../package.json'; const options: Options = { - apis: ['./src/**/*.ts'], // recursive, includes subfolders like ./src/routes + apis: ['./src/**/*.ts'], definition: { + components: { + securitySchemes: { + bearerAuth: { + bearerFormat: 'JWT', + scheme: 'bearer', + type: 'http', + }, + }, + }, info: { - description: 'A sample API documentation', + description, title: name, - version: '1.0.0', + version, }, openapi: '3.0.0', + security: [ + { + bearerAuth: [], + }, + ], }, }; diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts index f006429..08261e5 100644 --- a/src/routes/authRoute.ts +++ b/src/routes/authRoute.ts @@ -19,7 +19,39 @@ interface User { password: string; } -// Register endpoint +// register endpoint +/** + * @swagger + * /auth/register: + * post: + * summary: Register a new user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * format: password + * responses: + * 201: + * description: User registered successfully + * 400: + * description: Email and password are required + * 409: + * description: Email already taken + * 500: + * description: Internal server error + */ router.post('/register', async (req: Request, res: Response): Promise => { const { email, password } = req.body as User; @@ -51,6 +83,46 @@ router.post('/register', async (req: Request, res: Response): Promise => { }); // Login endpoint +/** + * @swagger + * /auth/login: + * post: + * summary: Log in an existing user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * format: password + * responses: + * 200: + * description: User logged in successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * token: + * type: string + * 401: + * description: Invalid credentials + * 500: + * description: Internal server error + */ + router.post('/login', async (req: Request, res: Response): Promise => { const { email, password } = req.body as User; @@ -81,6 +153,30 @@ router.post('/login', async (req: Request, res: Response): Promise => { }); // responds with user data +/** + * @swagger + * /auth/me: + * get: + * summary: Get current authenticated user info + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Returns user email + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * 401: + * description: No token provided or unauthorized + * 500: + * description: Internal server error + */ + router.get('/me', authMiddleware, async (req: Request, res: Response): Promise => { const token = req.headers.authorization?.split(' ')[1]; diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts index ba5f02e..7dc31b0 100644 --- a/src/utils/git-user-name.ts +++ b/src/utils/git-user-name.ts @@ -9,6 +9,7 @@ function getGitUserName(): string { const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); return name || 'unknown'; } catch (err) { + console.error('Error getting git user name:', err); return 'unknown'; } }