diff --git a/.gitignore b/.gitignore index 5c325f3..49a546a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ ormconfig.dev.json ormconfig.test.json .env .directory -.history \ No newline at end of file +.history +data_test +data_dev +data \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e150f8..14a02e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,8 @@ "eslint.workingDirectories": [ ".", "./frontend" - ] + ], + "search.exclude": { + "**/package-lock.json": true + } } \ No newline at end of file diff --git a/frontend/src/Photos/Overview.tsx b/frontend/src/Photos/Overview.tsx index e476aa3..142a264 100644 --- a/frontend/src/Photos/Overview.tsx +++ b/frontend/src/Photos/Overview.tsx @@ -8,7 +8,7 @@ export interface IOverviewComponentProps { fetching: boolean; spinner: boolean; - fetchDocs: () => void; + fetchPhotos: () => void; } export function OverviewComponent() { diff --git a/package-lock.json b/package-lock.json index ab10df4..b1fc1ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,33 @@ "vary": "^1.1.2" } }, + "@koa/router": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-9.4.0.tgz", + "integrity": "sha512-dOOXgzqaDoHu5qqMEPLKEgLz5CeIA7q8+1W62mCvFVCOqeC71UoTGJ4u1xUSOpIl2J1x2pqrNULkFteUeZW3/A==", + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.1.0" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -180,6 +207,12 @@ "@types/node": "*" } }, + "@types/deasync": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@types/deasync/-/deasync-0.1.1.tgz", + "integrity": "sha512-/AsDEUsHjyzMX0UjPgysggxFO8r7//c4aS9aeQwHzgs5POBsqaBFWW9+KYFGUyx/VYT4HrT/+JzAGTEEL2d4OQ==", + "dev": true + }, "@types/eslint": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.3.tgz", @@ -318,15 +351,6 @@ "@types/koa": "*" } }, - "@types/koa-router": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/koa-router/-/koa-router-7.4.1.tgz", - "integrity": "sha512-Hg78TXz78QYfEgdq3nTeRmQFEwJKZljsXb/DhtexmyrpRDRnl59oMglh9uPj3/WgKor0woANrYTnxA8gaWGK2A==", - "dev": true, - "requires": { - "@types/koa": "*" - } - }, "@types/koa-send": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@types/koa-send/-/koa-send-4.1.2.tgz", @@ -364,18 +388,42 @@ "@types/koa": "*" } }, + "@types/koa__router": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/koa__router/-/koa__router-8.0.3.tgz", + "integrity": "sha512-eS8K49z1x6OaW1ha61kRksVo42L5DWdQUA3kVpH1Kz6TuKBlG0ri42ELA4zSh+xg+6fAqjfuWA7bfNvwVMNXQA==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", "dev": true }, + "@types/mime-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz", + "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=", + "dev": true + }, "@types/mocha": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz", "integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==", "dev": true }, + "@types/mysql": { + "version": "2.15.15", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.15.tgz", + "integrity": "sha512-1GJnq7RwuFPRicMHdT53vza5v39nep9OKIbozxNUpFXP04CydcdWrqpZQ+MlVdlLFCisWnnt09xughajjWpFsw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "14.11.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.5.tgz", @@ -414,6 +462,15 @@ "@types/mime": "*" } }, + "@types/sharp": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.26.0.tgz", + "integrity": "sha512-oJrR8eiwpL7qykn2IeFRduXM4za7z+7yOUEbKVtuDQ/F6htDLHYO6IbzhaJQHV5n6O3adIh4tJvtgPyLyyydqg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -895,6 +952,36 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "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": { + "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" + } + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1299,6 +1386,15 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1312,6 +1408,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1536,6 +1641,22 @@ } } }, + "deasync": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.20.tgz", + "integrity": "sha512-E1GI7jMI57hL30OX6Ht/hfQU8DO4AuB9m72WFm4c38GNbUD4Q03//XZaOIHZiY+H1xUaomcot5yk2q/qIZQkGQ==", + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^1.7.1" + }, + "dependencies": { + "node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==" + } + } + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -1549,6 +1670,14 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -1674,6 +1803,14 @@ "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" + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -2024,6 +2161,11 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -2101,6 +2243,11 @@ "flat-cache": "^2.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2168,6 +2315,11 @@ "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-minipass": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", @@ -2260,6 +2412,11 @@ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==" }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -2357,6 +2514,15 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, + "hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2697,6 +2863,11 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==" }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, "is-string": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", @@ -2934,33 +3105,6 @@ } } }, - "koa-router": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-9.4.0.tgz", - "integrity": "sha512-RO/Y8XqSNM2J5vQeDaBI/7iRpL50C9QEudY4d3T4D1A2VMKLH0swmfjxDFPiIpVDLuNN6mVD9zBI1eFTHB6QaA==", - "requires": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.1.0" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "koa-send": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", @@ -3294,6 +3438,11 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -3332,6 +3481,11 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "mocha": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz", @@ -3487,6 +3641,11 @@ "thenify-all": "^1.0.0" } }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3527,6 +3686,21 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node-abi": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.1.tgz", + "integrity": "sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A==", + "requires": { + "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "node-addon-api": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.2.tgz", @@ -3556,6 +3730,11 @@ } } }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -3924,6 +4103,40 @@ "semver-compare": "^1.0.0" } }, + "prebuild-install": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.5.tgz", + "integrity": "sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + } + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4241,6 +4454,15 @@ "iterate-value": "^1.0.0" } }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -4532,6 +4754,22 @@ "safe-buffer": "^5.0.1" } }, + "sharp": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.26.1.tgz", + "integrity": "sha512-9MhwS4ys8pnwBH7MtnBdLzUv+cb24QC4xbzzQL6A+1MQ4Se2V6oPHEX8TIGIZUPRKi6S1kJPVNzt/Xqqp6/H3Q==", + "requires": { + "color": "^3.1.2", + "detect-libc": "^1.0.3", + "node-addon-api": "^3.0.2", + "npmlog": "^4.1.2", + "prebuild-install": "^5.3.5", + "semver": "^7.3.2", + "simple-get": "^4.0.0", + "tar-fs": "^2.1.0", + "tunnel-agent": "^0.6.0" + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4550,6 +4788,51 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz", + "integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + }, + "dependencies": { + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4796,6 +5079,41 @@ "yallist": "^3.0.3" } }, + "tar-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", + "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.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" + }, + "dependencies": { + "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" + } + } + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4949,6 +5267,14 @@ "tslib": "^1.8.1" } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 258461d..918485e 100644 --- a/package.json +++ b/package.json @@ -16,37 +16,43 @@ "lint-all": "npm run lint && npm run lint-frontend", "lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix", "prettier-check": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --check", - "prettify": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --write" + "prettify": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --write", + "typeorm-dev": "cross-env NODE_ENV=development ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js", + "typeorm": "ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js" }, "license": "MIT", "dependencies": { "@koa/cors": "^3.1.0", + "@koa/router": "^9.4.0", "@typescript-eslint/eslint-plugin": "^4.4.0", "@typescript-eslint/parser": "^4.4.0", "bcrypt": "^5.0.0", "chai": "^4.2.0", "concurrently": "^5.3.0", "cross-env": "^7.0.2", + "deasync": "^0.1.20", "eslint": "^7.10.0", "eslint-config-prettier": "^6.12.0", "eslint-import-resolver-typescript": "^2.3.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-mocha": "^8.0.0", "eslint-plugin-prettier": "^3.1.4", + "hasha": "^5.2.2", "husky": "^4.3.0", "jsonwebtoken": "^8.5.1", "koa": "^2.13.0", "koa-body": "^4.2.0", "koa-jwt": "^4.0.0", "koa-logger": "^3.2.1", - "koa-router": "^9.4.0", "koa-send": "^5.0.1", "koa-sslify": "^4.0.3", "koa-static": "^5.0.0", + "mime-types": "^2.1.27", "mocha": "^8.1.3", "mysql": "^2.18.1", "prettier": "^2.1.2", "prettier-eslint": "^11.0.0", + "sharp": "^0.26.1", "supertest": "^5.0.0", "ts-node": "^9.0.0", "ts-node-dev": "^1.0.0-pre.63", @@ -58,18 +64,22 @@ "@types/bcrypt": "^3.0.0", "@types/chai": "^4.2.13", "@types/concurrently": "^5.2.1", + "@types/deasync": "^0.1.1", "@types/eslint": "^7.2.3", "@types/eslint-plugin-prettier": "^3.1.0", "@types/jsonwebtoken": "^8.5.0", "@types/koa": "^2.11.4", "@types/koa-logger": "^3.1.1", - "@types/koa-router": "^7.4.1", "@types/koa-send": "^4.1.2", "@types/koa-sslify": "^4.0.1", "@types/koa-static": "^4.0.1", "@types/koa__cors": "^3.0.2", + "@types/koa__router": "^8.0.3", + "@types/mime-types": "^2.1.0", "@types/mocha": "^8.0.3", + "@types/mysql": "^2.15.15", "@types/prettier": "^2.1.1", + "@types/sharp": "^0.26.0", "@types/supertest": "^2.0.10" }, "husky": { @@ -77,4 +87,4 @@ "pre-commit": "npm run lint-all && npm run prettier-check" } } -} \ No newline at end of file +} diff --git a/src/app.ts b/src/app.ts index b21e670..2754a90 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,6 +8,8 @@ import * as logger from "koa-logger"; import * as send from "koa-send"; import sslify, { xForwardedProtoResolver } from "koa-sslify"; import * as serve from "koa-static"; +import * as path from "path"; +import * as fs from "fs"; import { config, EnvType } from "~config"; import { userRouter } from "~routes/users"; @@ -16,9 +18,20 @@ import { photosRouter } from "~routes/photos"; export const app = new Koa(); +const tmpPath = path.join(config.dataDir, "tmp"); + +// Create both data dir if it doesn't exist and temp dir +fs.mkdirSync(tmpPath, { recursive: true }); + app.use(cors()); app.use(logger()); -app.use(bodyParser()); +app.use( + bodyParser({ + multipart: true, + formidable: { uploadDir: tmpPath }, + }), +); + if (config.env === EnvType.production) { app.use(sslify({ resolver: xForwardedProtoResolver })); } @@ -29,6 +42,27 @@ app.use( }), ); +app.use(async (ctx, next) => { + try { + await next(); + } finally { + if (ctx.request.files) { + const filesVals = Object.values(ctx.request.files); + await Promise.all( + filesVals.map(async (f) => { + try { + await fs.promises.unlink(f.path); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } + }), + ); + } + } +}); + app.use(async (ctx, next) => { try { await next(); diff --git a/src/config/index.ts b/src/config/index.ts index 6c57756..8e2da58 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -11,12 +11,20 @@ export interface IConfig { env: EnvType; port: number; jwtSecret: string; + dataDir: string; dbConnectionOptions: ConnectionOptions | null; } function getJwtSecret(): string { switch (process.env.NODE_ENV) { + case "development": + return "DEVSECRET"; + break; + case "test": + return "TESTSECRET"; + break; case "production": + default: if (process.env.JWT_SECRET === undefined) { console.log("JWT_SECRET is not set"); process.exit(1); @@ -24,15 +32,26 @@ function getJwtSecret(): string { return process.env.JWT_SECRET; } break; + } +} + +function getDataDir(): string { + switch (process.env.NODE_ENV) { case "development": - return "DEVSECRET"; + return "./data_dev"; break; case "test": - return "TESTSECRET"; + return "./data_test"; break; + + case "production": default: - console.log("Unknown NODE_ENV"); - process.exit(1); + if (process.env.DATA_DIR === undefined) { + console.log("DATA_DIR is not set"); + process.exit(1); + } else { + return process.env.DATA_DIR; + } break; } } @@ -41,6 +60,7 @@ const production: IConfig = { env: EnvType.production, port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, jwtSecret: getJwtSecret(), + dataDir: getDataDir(), dbConnectionOptions: null, }; diff --git a/src/entity/Photo.ts b/src/entity/Photo.ts index 3016deb..0dc2937 100644 --- a/src/entity/Photo.ts +++ b/src/entity/Photo.ts @@ -1,10 +1,29 @@ +import * as path from "path"; +import * as fs from "fs/promises"; +import * as mime from "mime-types"; +import { constants as fsConstants } from "fs"; + import { + AfterRemove, BaseEntity, + BeforeRemove, Column, Entity, Index, + ManyToOne, PrimaryGeneratedColumn, } from "typeorm"; +import { User } from "./User"; + +export interface IPhotoJSON { + id: number; + user: number; + hash: string; + size: string; + format: string; + createdAt: number; + editedAt: number; +} @Entity() export class Photo extends BaseEntity { @@ -14,4 +33,73 @@ export class Photo extends BaseEntity { @Column({ length: 190 }) @Index() public hash: string; + + @Column({ length: 190 }) + @Index() + public size: string; + + @Column({ length: 190 }) + @Index() + public format: string; + + @Column({ type: "timestamp", default: null }) + public createdAt: Date; + + @Column({ type: "timestamp", default: null }) + public editedAt: Date; + + @ManyToOne(() => User, (user) => user.photos, { eager: true }) + public user: User; + + public getFileName(): string { + return `${this.user.id.toString()}-${this.hash}-${this.size}.${ + mime.extension(this.format) as string + }`; + } + + public getPath(): string { + return path.join(this.user.getDataPath(), this.getFileName()); + } + + @BeforeRemove() + async cleanupFiles(): Promise { + try { + await fs.unlink(this.getPath()); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } + } + + public async isUploaded(): Promise { + try { + await fs.access(this.getPath(), fsConstants.F_OK); + return true; + } catch (e) { + return false; + } + } + + constructor(user: User, hash: string, size: string, format: string) { + super(); + this.createdAt = new Date(); + this.editedAt = this.createdAt; + this.hash = hash; + this.format = format; + this.size = size; + this.user = user; + } + + public toJSON(): IPhotoJSON { + return { + id: this.id, + user: this.user.id, + hash: this.hash, + size: this.size, + format: this.format, + createdAt: this.createdAt.getTime(), + editedAt: this.editedAt.getTime(), + }; + } } diff --git a/src/entity/User.ts b/src/entity/User.ts index a603692..1b3b0a5 100644 --- a/src/entity/User.ts +++ b/src/entity/User.ts @@ -1,13 +1,21 @@ import * as bcrypt from "bcrypt"; import * as jwt from "jsonwebtoken"; +import * as path from "path"; +import * as fs from "fs/promises"; import { + AfterInsert, + AfterRemove, BaseEntity, + BeforeInsert, + BeforeRemove, Column, Entity, Index, + OneToMany, PrimaryGeneratedColumn, } from "typeorm"; import { config } from "../config"; +import { Photo } from "./Photo"; export type IUserJSON = Pick; @@ -36,6 +44,9 @@ export class User extends BaseEntity { @Column({ length: 190 }) public passwordHash: string; + @OneToMany(() => Photo, (photo) => photo.user) + photos: Promise; + constructor(username: string, email: string) { super(); this.username = username; @@ -50,6 +61,20 @@ export class User extends BaseEntity { this.passwordHash = await bcrypt.hash(password, 10); } + public getDataPath(): string { + return path.join(config.dataDir, this.id.toString()); + } + + @AfterInsert() + async createDataDir(): Promise { + await fs.mkdir(this.getDataPath()); + } + + @BeforeRemove() + async removeDataDir(): Promise { + await fs.rmdir(this.getDataPath(), { recursive: true }); + } + public toJSON(): IUserJSON { const { id, username } = this; return { id, username }; diff --git a/src/routes/dev.ts b/src/routes/dev.ts index 76a955a..69eaeb3 100644 --- a/src/routes/dev.ts +++ b/src/routes/dev.ts @@ -1,9 +1,11 @@ -import * as Router from "koa-router"; +import * as Router from "@koa/router"; +import { Photo } from "~entity/Photo"; import { User } from "~entity/User"; export const devRouter = new Router(); devRouter.post("/dev/clean", async (ctx) => { + await Photo.remove(await Photo.find()); await User.remove(await User.find()); ctx.body = { success: true }; }); diff --git a/src/routes/photos.ts b/src/routes/photos.ts index f38cbb3..1186e99 100644 --- a/src/routes/photos.ts +++ b/src/routes/photos.ts @@ -1,3 +1,234 @@ -import * as Router from "koa-router"; +import * as Router from "@koa/router"; +import { IPhotoJSON, Photo } from "~entity/Photo"; +import { User } from "~entity/User"; +import { IAPIResponse } from "~types"; +import * as fs from "fs/promises"; +import send = require("koa-send"); export const photosRouter = new Router(); + +export interface IPhotosNewPostBody { + hash: string | undefined; + size: string | undefined; + format: string | undefined; +} +export type IPhotosNewRespBody = IAPIResponse; +photosRouter.post("/photos/new", async (ctx) => { + if (!ctx.state.user) { + ctx.throw(401); + } + + const { user } = ctx.state; + const body = ctx.request.body as IPhotosNewPostBody; + const { hash, size, format } = body; + + if (!(hash && size && format)) { + ctx.throw(400); + return; + } + + const photo = new Photo(user.id, hash, size, format); + + try { + await photo.save(); + } catch (e) { + ctx.throw(400); + } + + ctx.body = { + error: false, + data: photo.toJSON(), + } as IPhotosNewRespBody; +}); + +export type IPhotosUploadRespBody = IAPIResponse; +photosRouter.post("/photos/upload/:id", async (ctx) => { + if (!ctx.state.user) { + ctx.throw(401); + } + + const { id } = ctx.params as { + id: number | undefined; + }; + + if (!id) { + ctx.throw(400); + return; + } + + const { user } = ctx.state; + const photo = await Photo.findOne({ id, user }); + if (!photo) { + ctx.throw(404); + return; + } + + if (!ctx.request.files || Object.keys(ctx.request.files).length === 0) { + ctx.throw(400, "No file"); + return; + } + + if (ctx.request.files) { + const files = ctx.request.files; + if (Object.keys(files).length > 1) { + ctx.throw(400, "Too many files"); + return; + } + const file = Object.values(files)[0]; + + try { + // TODO: actually move file if it's on different filesystems + await fs.rename(file.path, photo.getPath()); + } catch (e) { + ctx.throw(500); + } + } + ctx.body = { + error: false, + data: photo.toJSON(), + } as IPhotosUploadRespBody; +}); + +/** +export interface IPhotosByIDPatchBody { +} +export type IPhotosByIDPatchRespBody = IAPIResponse; +photosRouter.patch("/photos/byID/:id", async (ctx) => { + if (!ctx.state.user) { + ctx.throw(401); + return; + } + + const { user } = ctx.state; + const { id } = ctx.params as { + id: number | undefined; + }; + + if (!id) { + ctx.throw(400); + return; + } + + const photo = await Photo.findOne({ id, user }); + + if (!photo) { + ctx.throw(404); + return; + } + + // TODO: Some actual editing + + try { + photo.editedAt = new Date(); + await photo.save(); + } catch (e) { + ctx.throw(400); + } + + ctx.body = { + error: false, + data: photo.toJSON(), + }; +}); +*/ + +export type IPhotosListRespBody = IAPIResponse; +photosRouter.get("/photos/list", async (ctx) => { + if (!ctx.state.user) { + ctx.throw(401); + } + + const { user } = ctx.state; + + const photos = await Photo.find({ user }); + + ctx.body = { + error: false, + data: photos.map((photo) => photo.toJSON()), + } as IPhotosListRespBody; +}); + +export type IPhotosByIDGetRespBody = IAPIResponse; +photosRouter.get("/photos/byID/:id", async (ctx) => { + if (!ctx.state.user) { + ctx.throw(401); + } + + const { id } = ctx.params as { + id: number | undefined; + }; + + if (!id) { + ctx.throw(400); + } + + const { user } = ctx.state; + + const photo = await Photo.findOne({ id, user }); + + if (!photo) { + ctx.throw(404); + return; + } + + ctx.body = { + error: false, + data: photo.toJSON(), + } as IPhotosByIDGetRespBody; +}); + +photosRouter.get("/photos/showByID/:id", async (ctx) => { + if (!ctx.state.user) { + ctx.throw(401); + } + + const { id } = ctx.params as { + id: number | undefined; + }; + + if (!id) { + ctx.throw(400); + } + + const { user } = ctx.state; + + const photo = await Photo.findOne({ id, user }); + + if (!photo) { + ctx.throw(404); + return; + } + + await send(ctx, photo.getPath()); +}); + +export type IPhotosByIDDeleteRespBody = IAPIResponse; +photosRouter.delete("/photos/byID/:id", async (ctx) => { + if (!ctx.state.user) { + ctx.throw(401); + } + + const { id } = ctx.params as { + id: number | undefined; + }; + + if (!id) { + ctx.throw(400); + } + + const { user } = ctx.state; + + const photo = await Photo.findOne({ id, user }); + + if (!photo) { + ctx.throw(404); + return; + } + + await photo.remove(); + + ctx.body = { + error: false, + data: true, + } as IPhotosByIDDeleteRespBody; +}); diff --git a/src/routes/users.ts b/src/routes/users.ts index 415833c..bffb828 100644 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -1,8 +1,10 @@ -import * as Router from "koa-router"; -import { IUserJWT, User } from "~entity/User"; +import * as Router from "@koa/router"; +import { IUserAuthJSON, IUserJWT, User } from "~entity/User"; +import { IAPIResponse } from "~types"; export const userRouter = new Router(); +export type IUserGetRespBody = IAPIResponse; userRouter.get("/users/user", async (ctx) => { if (!ctx.state.user) { ctx.throw(401); @@ -17,19 +19,21 @@ userRouter.get("/users/user", async (ctx) => { return; } - ctx.body = { error: false, data: user.toAuthJSON() }; + ctx.body = { error: false, data: user.toAuthJSON() } as IUserGetRespBody; }); +export interface IUserLoginBody { + username: string | undefined; + password: string | undefined; +} +export type IUserLoginRespBody = IAPIResponse; userRouter.post("/users/login", async (ctx) => { const request = ctx.request; if (!request.body) { ctx.throw(400); } - const { username, password } = request.body as { - username: string | undefined; - password: string | undefined; - }; + const { username, password } = request.body as IUserLoginBody; if (!(username && password)) { ctx.throw(400); @@ -42,9 +46,15 @@ userRouter.post("/users/login", async (ctx) => { return; } - ctx.body = { error: false, data: user.toAuthJSON() }; + ctx.body = { error: false, data: user.toAuthJSON() } as IUserLoginRespBody; }); +export interface IUserSignupBody { + username: string | undefined; + password: string | undefined; + email: string | undefined; +} +export type IUserSignupRespBody = IAPIResponse; userRouter.post("/users/signup", async (ctx) => { const request = ctx.request; @@ -52,11 +62,7 @@ userRouter.post("/users/signup", async (ctx) => { ctx.throw(400); } - const { username, password, email } = request.body as { - username: string | undefined; - password: string | undefined; - email: string | undefined; - }; + const { username, password, email } = request.body as IUserSignupBody; if (!(username && password && email)) { ctx.throw(400); @@ -74,9 +80,13 @@ userRouter.post("/users/signup", async (ctx) => { } } - ctx.body = { error: false, data: user.toAuthJSON() }; + ctx.body = { error: false, data: user.toAuthJSON() } as IUserSignupRespBody; }); +export interface IUserEditBody { + password: string | undefined; +} +export type IUserEditRespBody = IAPIResponse; userRouter.post("/users/edit", async (ctx) => { if (!ctx.state.user) { ctx.throw(401); @@ -96,9 +106,7 @@ userRouter.post("/users/edit", async (ctx) => { return; } - const { password } = request.body as { - password: string | undefined; - }; + const { password } = request.body as IUserEditBody; if (!password) { ctx.throw(400); @@ -113,5 +121,5 @@ userRouter.post("/users/edit", async (ctx) => { ctx.throw(400); } - ctx.body = { error: false, data: user.toAuthJSON() }; + ctx.body = { error: false, data: user.toAuthJSON() } as IUserEditRespBody; }); diff --git a/src/tests/integration/photos.test.ts b/src/tests/integration/photos.test.ts new file mode 100644 index 0000000..9aacbb5 --- /dev/null +++ b/src/tests/integration/photos.test.ts @@ -0,0 +1,298 @@ +import { assert, expect } from "chai"; +import { connect } from "config/database"; +import * as request from "supertest"; +import { getConnection } from "typeorm"; +import { app } from "~app"; +import { Photo, IPhotoJSON } from "~entity/Photo"; +import { IPhotosNewPostBody } from "~routes/photos"; +import * as fs from "fs/promises"; +import { constants as fsConstants } from "fs"; + +import { + dogFileSize, + dogFormat, + dogHash, + dogPath, + dogSize, + ISeed, + prepareMetadata, + seedDB, +} from "./util"; +import { sleep } from "deasync"; + +const callback = app.callback(); + +let seed: ISeed; + +describe("photos", function () { + before(async function () { + await connect(); + await prepareMetadata(); + }); + + after(async function () { + await getConnection().close(); + }); + + beforeEach(async function () { + seed = await seedDB(); + }); + + it("should get a photo", async function () { + const response = await request(callback) + .get(`/photos/byID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + + expect(response.body.error).to.be.false; + + const photo = response.body.data as IPhotoJSON; + + const usedPhoto = seed.dogPhoto.toJSON(); + + expect(photo).to.deep.equal(usedPhoto); + }); + + it("should not get a photo without jwt", async function () { + const response = await request(callback) + .get(`/photos/byID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + }) + .expect(404); + + expect(response.body.error).to.be.equal("Not Found"); + }); + + it("should show a photo", async function () { + const response = await request(callback) + .get(`/photos/showByID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + expect(parseInt(response.header["content-length"])).to.equal( + dogFileSize, + ); + }); + + it("should not show a photo without jwt", async function () { + const response = await request(callback) + .get(`/photos/byID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + }) + .expect(404); + + expect(response.body.error).to.be.equal("Not Found"); + }); + + it("should create, upload and show a photo", async function () { + const response = await request(callback) + .post("/photos/new") + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + "Content-Type": "application/json", + }) + .send({ + hash: dogHash, + size: dogSize, + format: dogFormat, + } as IPhotosNewPostBody) + .expect(200); + + expect(response.body.error).to.be.false; + + const photo = response.body.data as IPhotoJSON; + + expect(photo.hash).to.be.equal(dogHash); + const dbPhoto = await Photo.findOneOrFail({ + id: photo.id, + user: seed.user1.id as any, + }); + expect(dbPhoto.hash).to.be.equal(dogHash); + + await request(callback) + .post(`/photos/upload/${photo.id}`) + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + "Content-Type": "application/json", + }) + .attach("photo", dogPath) + .expect(200); + + const showResp = await request(callback) + .get(`/photos/showByID/${photo.id}`) + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + }) + .expect(200); + + expect(parseInt(showResp.header["content-length"])).to.equal( + dogFileSize, + ); + }); + + it("should create a photo but not upload for other user", async function () { + const response = await request(callback) + .post("/photos/new") + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + "Content-Type": "application/json", + }) + .send({ + hash: dogHash, + size: dogSize, + format: dogFormat, + } as IPhotosNewPostBody) + .expect(200); + + expect(response.body.error).to.be.false; + + const photo = response.body.data as IPhotoJSON; + + expect(photo.hash).to.be.equal(dogHash); + const dbPhoto = await Photo.findOneOrFail({ + id: photo.id, + user: seed.user1.id as any, + }); + expect(dbPhoto.hash).to.be.equal(dogHash); + + await request(callback) + .post(`/photos/upload/${photo.id}`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + "Content-Type": "application/json", + }) + .attach("photo", dogPath) + .expect(404); + }); + + it("should create, upload but not show a photo to another user", async function () { + const response = await request(callback) + .post("/photos/new") + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + "Content-Type": "application/json", + }) + .send({ + hash: dogHash, + size: dogSize, + format: dogFormat, + } as IPhotosNewPostBody) + .expect(200); + + expect(response.body.error).to.be.false; + + const photo = response.body.data as IPhotoJSON; + + expect(photo.hash).to.be.equal(dogHash); + const dbPhoto = await Photo.findOneOrFail({ + id: photo.id, + user: seed.user1.id as any, + }); + expect(dbPhoto.hash).to.be.equal(dogHash); + + await request(callback) + .post(`/photos/upload/${photo.id}`) + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + "Content-Type": "application/json", + }) + .attach("photo", dogPath) + .expect(200); + + await request(callback) + .get(`/photos/showByID/${photo.id}`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(404); + }); + + /* + it("should update a photo", async function () { + const response = await request(callback) + .patch(`/photos/byID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user1.toJWT()}`, + "Content-Type": "application/json", + }) + .send({ name: "Test1", content: "Test1" }) + .expect(200); + + expect(response.body.error).to.be.false; + + const photo = response.body.data as IPhotoJSON; + + expect(photo.name).to.be.equal("Test1"); + + const dbPhoto = await Photo.findOne({ + id: seed.dogPhoto.id, + user: seed.user1.id as any, + }); + + expect(dbPhoto.name).to.be.equal("Test1"); + expect(dbPhoto.editedAt.getTime()).to.be.closeTo( + new Date().getTime(), + 2000, + ); + }); + */ + + it("should list photos", async function () { + const response = await request(callback) + .get("/photos/list") + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + + expect(response.body.error).to.be.false; + + const photos = response.body.data as IPhotoJSON[]; + + const userPhotos = [seed.dogPhoto.toJSON(), seed.catPhoto.toJSON()]; + + expect(photos).to.deep.equal(userPhotos); + }); + + /* + it("should get a shared photo", async function () { + const response = await request(callback) + .get(`/photos/shared/${seed.user1.username}/${seed.catPhoto.id}`) + .expect(200); + + expect(response.body.error).to.be.false; + + const photo = response.body.data as IPhotoJSON; + + const usedPhoto = seed.catPhoto.toJSON(); + + expect(photo).to.deep.equal(usedPhoto); + }); + */ + + it("should delete a photo", async function () { + const photoPath = seed.dogPhoto.getPath(); + const response = await request(callback) + .delete(`/photos/byID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + + expect(response.body.error).to.be.false; + const dbPhoto = await Photo.findOne(seed.dogPhoto.id); + expect(dbPhoto).to.be.undefined; + + try { + await fs.access(photoPath, fsConstants.F_OK); + assert(false); + } catch (e) { + assert(true); + } + }); +}); diff --git a/src/tests/integration/photos/cat.jpg b/src/tests/integration/photos/cat.jpg new file mode 100644 index 0000000..fe7cb57 Binary files /dev/null and b/src/tests/integration/photos/cat.jpg differ diff --git a/src/tests/integration/photos/dog.jpg b/src/tests/integration/photos/dog.jpg new file mode 100644 index 0000000..ac32d2f Binary files /dev/null and b/src/tests/integration/photos/dog.jpg differ diff --git a/src/tests/integration/users.test.ts b/src/tests/integration/users.test.ts index 45a0914..548c8f2 100644 --- a/src/tests/integration/users.test.ts +++ b/src/tests/integration/users.test.ts @@ -1,9 +1,18 @@ -import { expect } from "chai"; +import { assert, expect } from "chai"; import { connect } from "config/database"; import * as request from "supertest"; import { getConnection } from "typeorm"; import { app } from "~app"; import { IUserAuthJSON, User } from "~entity/User"; +import { + IUserEditBody, + IUserEditRespBody, + IUserGetRespBody, + IUserLoginBody, + IUserLoginRespBody, + IUserSignupBody, + IUserSignupRespBody, +} from "~routes/users"; import { ISeed, seedDB } from "./util"; @@ -34,9 +43,14 @@ describe("users", function () { .expect("Content-Type", /json/) .expect(200); - expect(response.body.error).to.be.false; + const body = response.body as IUserGetRespBody; - const { jwt: _, ...user } = response.body.data as IUserAuthJSON; + if (body.error !== false) { + assert(false); + return; + } + + const { jwt: _, ...user } = body.data; expect(user).to.deep.equal(seed.user1.toJSON()); }); @@ -45,14 +59,18 @@ describe("users", function () { const response = await request(callback) .post("/users/login") .set({ "Content-Type": "application/json" }) - .send({ username: "User1", password: "User1" }) + .send({ username: "User1", password: "User1" } as IUserLoginBody) .expect("Content-Type", /json/) .expect(200); - expect(response.body.error).to.be.false; + const body = response.body as IUserLoginRespBody; - const { jwt: _, ...user } = response.body.data as IUserAuthJSON; + if (body.error !== false) { + assert(false); + return; + } + const { jwt: _, ...user } = response.body.data; expect(user).to.deep.equal(seed.user1.toJSON()); }); @@ -60,11 +78,12 @@ describe("users", function () { const response = await request(callback) .post("/users/login") .set({ "Content-Type": "application/json" }) - .send({ username: "User1", password: "asdf" }) + .send({ username: "User1", password: "asdf" } as IUserLoginBody) .expect(404); - expect(response.body.error).to.be.equal("User not found"); - expect(response.body.data).to.be.false; + const body = response.body as IUserLoginRespBody; + expect(body.error).to.be.equal("User not found"); + expect(body.data).to.be.false; }); it("should signup user", async function () { @@ -75,16 +94,19 @@ describe("users", function () { username: "NUser1", password: "NUser1", email: "nuser1@users.com", - }) + } as IUserSignupBody) .expect("Content-Type", /json/) .expect(200); - expect(response.body.error).to.be.false; + const body = response.body as IUserSignupRespBody; - const { jwt: _, ...user } = response.body.data as IUserAuthJSON; + if (body.error !== false) { + assert(false); + return; + } + const { jwt: _, ...user } = body.data; const newUser = await User.findOneOrFail({ username: "NUser1" }); - expect(user).to.deep.equal(newUser.toJSON()); }); @@ -96,11 +118,13 @@ describe("users", function () { username: "User1", password: "NUser1", email: "user1@users.com", - }) + } as IUserSignupBody) .expect(400); - expect(response.body.error).to.be.equal("User already exists"); - expect(response.body.data).to.be.false; + const body = response.body as IUserSignupRespBody; + + expect(body.error).to.be.equal("User already exists"); + expect(body.data).to.be.false; }); it("should change user's password", async function () { @@ -112,31 +136,46 @@ describe("users", function () { }) .send({ password: "User1NewPass", - }) + } as IUserEditBody) .expect("Content-Type", /json/) .expect(200); - expect(response.body.error).to.be.false; + const body = response.body as IUserEditRespBody; + + if (body.error !== false) { + assert(false); + return; + } const loginResponse = await request(callback) .post("/users/login") .set({ "Content-Type": "application/json" }) - .send({ username: "User1", password: "User1NewPass" }) + .send({ + username: "User1", + password: "User1NewPass", + } as IUserLoginBody) .expect("Content-Type", /json/) .expect(200); - expect(loginResponse.body.error).to.be.false; + const loginBody = loginResponse.body as IUserLoginRespBody; - const { jwt: _, ...user } = response.body.data as IUserAuthJSON; + if (loginBody.error !== false) { + assert(false); + return; + } + + const { jwt: _, ...user } = loginBody.data; expect(user).to.deep.equal(seed.user1.toJSON()); const badLoginResponse = await request(callback) .post("/users/login") .set({ "Content-Type": "application/json" }) - .send({ username: "User1", password: "User1" }) + .send({ username: "User1", password: "User1" } as IUserLoginBody) .expect(404); - expect(badLoginResponse.body.error).to.be.equal("User not found"); - expect(badLoginResponse.body.data).to.be.false; + const badLoginBody = badLoginResponse.body as IUserLoginRespBody; + + expect(badLoginBody.error).to.be.equal("User not found"); + expect(badLoginBody.data).to.be.false; }); }); diff --git a/src/tests/integration/util.ts b/src/tests/integration/util.ts index 4e712e9..e4bf876 100644 --- a/src/tests/integration/util.ts +++ b/src/tests/integration/util.ts @@ -1,15 +1,44 @@ +import * as fs from "fs/promises"; + import { User } from "entity/User"; -//import { Document } from "~entity/Document"; +import { Photo } from "~entity/Photo"; +import { getHash, getSize } from "~util"; + +export const dogPath = "./src/tests/integration/photos/dog.jpg"; +export const catPath = "./src/tests/integration/photos/cat.jpg"; export interface ISeed { user1: User; user2: User; - // doc1: Document; - // doc2p: Document; + dogPhoto: Photo; + catPhoto: Photo; +} + +export let dogHash = ""; +export let dogSize = ""; +export let dogFileSize = 0; +export const dogFormat = "image/jpeg"; +export let catHash = ""; +export let catSize = ""; +export let catFileSize = 0; +export const catFormat = "image/jpeg"; + +export async function prepareMetadata(): Promise { + dogHash = await getHash(dogPath); + dogSize = await getSize(dogPath); + dogFileSize = (await fs.stat(dogPath)).size; + catHash = await getHash(catPath); + catSize = await getSize(catPath); + catFileSize = (await fs.stat(catPath)).size; } export async function seedDB(): Promise { - //await Document.remove(await Document.find()); + dogHash = await getHash(dogPath); + dogSize = await getSize(dogPath); + catHash = await getHash(catPath); + catSize = await getSize(catPath); + + await Photo.remove(await Photo.find()); await User.remove(await User.find()); const user1 = new User("User1", "user1@users.com"); @@ -20,11 +49,14 @@ export async function seedDB(): Promise { await user2.setPassword("User2"); await user2.save(); - //const doc1 = new Document(user1, "Doc1", "Doc1", false); - //const doc2p = new Document(user1, "Doc2", "Doc2", true); + const dogPhoto = new Photo(user2, dogHash, dogSize, dogFormat); + const catPhoto = new Photo(user2, catHash, catSize, catFormat); - //await doc1.save(); - //await doc2p.save(); + await fs.copyFile(dogPath, dogPhoto.getPath()); + await fs.copyFile(catPath, catPhoto.getPath()); - return { user1, user2 }; // doc1, doc2p }; + await dogPhoto.save(); + await catPhoto.save(); + + return { user1, user2, dogPhoto, catPhoto }; } diff --git a/src/types.ts b/src/types.ts index 88b4e40..d33e7c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,12 @@ -export interface IAPIResponse { - data: T | null; - error: string | null; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +interface IAPIErrorResponse { + data: null; + error: string; } + +interface IAPISuccessResponse { + error: false; + data: T; +} + +export type IAPIResponse = IAPIErrorResponse | IAPISuccessResponse; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..c71ff87 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,24 @@ +import deasync = require("deasync"); +import { fromFile } from "hasha"; +import sharp = require("sharp"); + +export async function getHash(file: string): Promise { + return await fromFile(file, { + algorithm: "md5", + }); +} + +export async function getSize(file: string): Promise { + const metadata = await sharp(file).metadata(); + if (!(metadata.width && metadata.height)) { + throw new Error( + `The ${file} doesn't have width and height... how did we get there?`, + ); + } + return `${metadata.width}x${metadata.height}`; +} + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +export const getHashSync: (file: string) => string = deasync(getHash); +// eslint-disable-next-line @typescript-eslint/no-misused-promises +export const getSizeSync: (file: string) => string = deasync(getSize);