(hopefully) working photos upload backend

This commit is contained in:
2020-10-13 23:58:49 +03:00
committed by Stepan Usatiuk
parent 755e849ded
commit 2887aedc3c
19 changed files with 1255 additions and 104 deletions

3
.gitignore vendored
View File

@@ -10,3 +10,6 @@ ormconfig.test.json
.env .env
.directory .directory
.history .history
data_test
data_dev
data

View File

@@ -5,5 +5,8 @@
"eslint.workingDirectories": [ "eslint.workingDirectories": [
".", ".",
"./frontend" "./frontend"
] ],
"search.exclude": {
"**/package-lock.json": true
}
} }

View File

@@ -8,7 +8,7 @@ export interface IOverviewComponentProps {
fetching: boolean; fetching: boolean;
spinner: boolean; spinner: boolean;
fetchDocs: () => void; fetchPhotos: () => void;
} }
export function OverviewComponent() { export function OverviewComponent() {

398
package-lock.json generated
View File

@@ -79,6 +79,33 @@
"vary": "^1.1.2" "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": { "@nodelib/fs.scandir": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
@@ -180,6 +207,12 @@
"@types/node": "*" "@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": { "@types/eslint": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.3.tgz",
@@ -318,15 +351,6 @@
"@types/koa": "*" "@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": { "@types/koa-send": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@types/koa-send/-/koa-send-4.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/koa-send/-/koa-send-4.1.2.tgz",
@@ -364,18 +388,42 @@
"@types/koa": "*" "@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": { "@types/mime": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true "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": { "@types/mocha": {
"version": "8.0.3", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz",
"integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==", "integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==",
"dev": true "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": { "@types/node": {
"version": "14.11.5", "version": "14.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.5.tgz",
@@ -414,6 +462,15 @@
"@types/mime": "*" "@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": { "@types/strip-bom": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" "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": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "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", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" "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": { "color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "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", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" "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": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "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": { "debug": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" "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": { "deep-eql": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" "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": { "enquirer": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "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", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" "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": { "external-editor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -2101,6 +2243,11 @@
"flat-cache": "^2.0.1" "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": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" "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": { "fs-minipass": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "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", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==" "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": { "glob": {
"version": "7.1.6", "version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "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", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" "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": { "he": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz",
"integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==" "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": { "is-string": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", "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": { "koa-send": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" "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": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -3332,6 +3481,11 @@
"minimist": "^1.2.5" "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": { "mocha": {
"version": "8.1.3", "version": "8.1.3",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz",
@@ -3487,6 +3641,11 @@
"thenify-all": "^1.0.0" "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": { "natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "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", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" "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": { "node-addon-api": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.2.tgz", "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": { "nopt": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
@@ -3924,6 +4103,40 @@
"semver-compare": "^1.0.0" "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": { "prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -4241,6 +4454,15 @@
"iterate-value": "^1.0.0" "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": { "punycode": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -4532,6 +4754,22 @@
"safe-buffer": "^5.0.1" "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": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" "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": { "slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -4796,6 +5079,41 @@
"yallist": "^3.0.3" "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": { "text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -4949,6 +5267,14 @@
"tslib": "^1.8.1" "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": { "type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -16,37 +16,43 @@
"lint-all": "npm run lint && npm run lint-frontend", "lint-all": "npm run lint && npm run lint-frontend",
"lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix", "lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix",
"prettier-check": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --check", "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", "license": "MIT",
"dependencies": { "dependencies": {
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@koa/router": "^9.4.0",
"@typescript-eslint/eslint-plugin": "^4.4.0", "@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0", "@typescript-eslint/parser": "^4.4.0",
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"deasync": "^0.1.20",
"eslint": "^7.10.0", "eslint": "^7.10.0",
"eslint-config-prettier": "^6.12.0", "eslint-config-prettier": "^6.12.0",
"eslint-import-resolver-typescript": "^2.3.0", "eslint-import-resolver-typescript": "^2.3.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-mocha": "^8.0.0", "eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"hasha": "^5.2.2",
"husky": "^4.3.0", "husky": "^4.3.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.13.0", "koa": "^2.13.0",
"koa-body": "^4.2.0", "koa-body": "^4.2.0",
"koa-jwt": "^4.0.0", "koa-jwt": "^4.0.0",
"koa-logger": "^3.2.1", "koa-logger": "^3.2.1",
"koa-router": "^9.4.0",
"koa-send": "^5.0.1", "koa-send": "^5.0.1",
"koa-sslify": "^4.0.3", "koa-sslify": "^4.0.3",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"mime-types": "^2.1.27",
"mocha": "^8.1.3", "mocha": "^8.1.3",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"prettier-eslint": "^11.0.0", "prettier-eslint": "^11.0.0",
"sharp": "^0.26.1",
"supertest": "^5.0.0", "supertest": "^5.0.0",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"ts-node-dev": "^1.0.0-pre.63", "ts-node-dev": "^1.0.0-pre.63",
@@ -58,18 +64,22 @@
"@types/bcrypt": "^3.0.0", "@types/bcrypt": "^3.0.0",
"@types/chai": "^4.2.13", "@types/chai": "^4.2.13",
"@types/concurrently": "^5.2.1", "@types/concurrently": "^5.2.1",
"@types/deasync": "^0.1.1",
"@types/eslint": "^7.2.3", "@types/eslint": "^7.2.3",
"@types/eslint-plugin-prettier": "^3.1.0", "@types/eslint-plugin-prettier": "^3.1.0",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.0",
"@types/koa": "^2.11.4", "@types/koa": "^2.11.4",
"@types/koa-logger": "^3.1.1", "@types/koa-logger": "^3.1.1",
"@types/koa-router": "^7.4.1",
"@types/koa-send": "^4.1.2", "@types/koa-send": "^4.1.2",
"@types/koa-sslify": "^4.0.1", "@types/koa-sslify": "^4.0.1",
"@types/koa-static": "^4.0.1", "@types/koa-static": "^4.0.1",
"@types/koa__cors": "^3.0.2", "@types/koa__cors": "^3.0.2",
"@types/koa__router": "^8.0.3",
"@types/mime-types": "^2.1.0",
"@types/mocha": "^8.0.3", "@types/mocha": "^8.0.3",
"@types/mysql": "^2.15.15",
"@types/prettier": "^2.1.1", "@types/prettier": "^2.1.1",
"@types/sharp": "^0.26.0",
"@types/supertest": "^2.0.10" "@types/supertest": "^2.0.10"
}, },
"husky": { "husky": {

View File

@@ -8,6 +8,8 @@ import * as logger from "koa-logger";
import * as send from "koa-send"; import * as send from "koa-send";
import sslify, { xForwardedProtoResolver } from "koa-sslify"; import sslify, { xForwardedProtoResolver } from "koa-sslify";
import * as serve from "koa-static"; import * as serve from "koa-static";
import * as path from "path";
import * as fs from "fs";
import { config, EnvType } from "~config"; import { config, EnvType } from "~config";
import { userRouter } from "~routes/users"; import { userRouter } from "~routes/users";
@@ -16,9 +18,20 @@ import { photosRouter } from "~routes/photos";
export const app = new Koa(); 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(cors());
app.use(logger()); app.use(logger());
app.use(bodyParser()); app.use(
bodyParser({
multipart: true,
formidable: { uploadDir: tmpPath },
}),
);
if (config.env === EnvType.production) { if (config.env === EnvType.production) {
app.use(sslify({ resolver: xForwardedProtoResolver })); 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) => { app.use(async (ctx, next) => {
try { try {
await next(); await next();

View File

@@ -11,12 +11,20 @@ export interface IConfig {
env: EnvType; env: EnvType;
port: number; port: number;
jwtSecret: string; jwtSecret: string;
dataDir: string;
dbConnectionOptions: ConnectionOptions | null; dbConnectionOptions: ConnectionOptions | null;
} }
function getJwtSecret(): string { function getJwtSecret(): string {
switch (process.env.NODE_ENV) { switch (process.env.NODE_ENV) {
case "development":
return "DEVSECRET";
break;
case "test":
return "TESTSECRET";
break;
case "production": case "production":
default:
if (process.env.JWT_SECRET === undefined) { if (process.env.JWT_SECRET === undefined) {
console.log("JWT_SECRET is not set"); console.log("JWT_SECRET is not set");
process.exit(1); process.exit(1);
@@ -24,15 +32,26 @@ function getJwtSecret(): string {
return process.env.JWT_SECRET; return process.env.JWT_SECRET;
} }
break; break;
}
}
function getDataDir(): string {
switch (process.env.NODE_ENV) {
case "development": case "development":
return "DEVSECRET"; return "./data_dev";
break; break;
case "test": case "test":
return "TESTSECRET"; return "./data_test";
break; break;
case "production":
default: default:
console.log("Unknown NODE_ENV"); if (process.env.DATA_DIR === undefined) {
console.log("DATA_DIR is not set");
process.exit(1); process.exit(1);
} else {
return process.env.DATA_DIR;
}
break; break;
} }
} }
@@ -41,6 +60,7 @@ const production: IConfig = {
env: EnvType.production, env: EnvType.production,
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
jwtSecret: getJwtSecret(), jwtSecret: getJwtSecret(),
dataDir: getDataDir(),
dbConnectionOptions: null, dbConnectionOptions: null,
}; };

View File

@@ -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 { import {
AfterRemove,
BaseEntity, BaseEntity,
BeforeRemove,
Column, Column,
Entity, Entity,
Index, Index,
ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from "typeorm"; } from "typeorm";
import { User } from "./User";
export interface IPhotoJSON {
id: number;
user: number;
hash: string;
size: string;
format: string;
createdAt: number;
editedAt: number;
}
@Entity() @Entity()
export class Photo extends BaseEntity { export class Photo extends BaseEntity {
@@ -14,4 +33,73 @@ export class Photo extends BaseEntity {
@Column({ length: 190 }) @Column({ length: 190 })
@Index() @Index()
public hash: string; 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<void> {
try {
await fs.unlink(this.getPath());
} catch (e) {
if (e.code !== "ENOENT") {
throw e;
}
}
}
public async isUploaded(): Promise<boolean> {
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(),
};
}
} }

View File

@@ -1,13 +1,21 @@
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import * as path from "path";
import * as fs from "fs/promises";
import { import {
AfterInsert,
AfterRemove,
BaseEntity, BaseEntity,
BeforeInsert,
BeforeRemove,
Column, Column,
Entity, Entity,
Index, Index,
OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from "typeorm"; } from "typeorm";
import { config } from "../config"; import { config } from "../config";
import { Photo } from "./Photo";
export type IUserJSON = Pick<User, "id" | "username">; export type IUserJSON = Pick<User, "id" | "username">;
@@ -36,6 +44,9 @@ export class User extends BaseEntity {
@Column({ length: 190 }) @Column({ length: 190 })
public passwordHash: string; public passwordHash: string;
@OneToMany(() => Photo, (photo) => photo.user)
photos: Promise<Photo[]>;
constructor(username: string, email: string) { constructor(username: string, email: string) {
super(); super();
this.username = username; this.username = username;
@@ -50,6 +61,20 @@ export class User extends BaseEntity {
this.passwordHash = await bcrypt.hash(password, 10); this.passwordHash = await bcrypt.hash(password, 10);
} }
public getDataPath(): string {
return path.join(config.dataDir, this.id.toString());
}
@AfterInsert()
async createDataDir(): Promise<void> {
await fs.mkdir(this.getDataPath());
}
@BeforeRemove()
async removeDataDir(): Promise<void> {
await fs.rmdir(this.getDataPath(), { recursive: true });
}
public toJSON(): IUserJSON { public toJSON(): IUserJSON {
const { id, username } = this; const { id, username } = this;
return { id, username }; return { id, username };

View File

@@ -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"; import { User } from "~entity/User";
export const devRouter = new Router(); export const devRouter = new Router();
devRouter.post("/dev/clean", async (ctx) => { devRouter.post("/dev/clean", async (ctx) => {
await Photo.remove(await Photo.find());
await User.remove(await User.find()); await User.remove(await User.find());
ctx.body = { success: true }; ctx.body = { success: true };
}); });

View File

@@ -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 const photosRouter = new Router();
export interface IPhotosNewPostBody {
hash: string | undefined;
size: string | undefined;
format: string | undefined;
}
export type IPhotosNewRespBody = IAPIResponse<IPhotoJSON>;
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<IPhotoJSON>;
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<IPhotoJSON>;
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<IPhotoJSON[]>;
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<IPhotoJSON>;
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<boolean>;
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;
});

View File

@@ -1,8 +1,10 @@
import * as Router from "koa-router"; import * as Router from "@koa/router";
import { IUserJWT, User } from "~entity/User"; import { IUserAuthJSON, IUserJWT, User } from "~entity/User";
import { IAPIResponse } from "~types";
export const userRouter = new Router(); export const userRouter = new Router();
export type IUserGetRespBody = IAPIResponse<IUserAuthJSON>;
userRouter.get("/users/user", async (ctx) => { userRouter.get("/users/user", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -17,19 +19,21 @@ userRouter.get("/users/user", async (ctx) => {
return; 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<IUserAuthJSON>;
userRouter.post("/users/login", async (ctx) => { userRouter.post("/users/login", async (ctx) => {
const request = ctx.request; const request = ctx.request;
if (!request.body) { if (!request.body) {
ctx.throw(400); ctx.throw(400);
} }
const { username, password } = request.body as { const { username, password } = request.body as IUserLoginBody;
username: string | undefined;
password: string | undefined;
};
if (!(username && password)) { if (!(username && password)) {
ctx.throw(400); ctx.throw(400);
@@ -42,9 +46,15 @@ userRouter.post("/users/login", async (ctx) => {
return; 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<IUserAuthJSON>;
userRouter.post("/users/signup", async (ctx) => { userRouter.post("/users/signup", async (ctx) => {
const request = ctx.request; const request = ctx.request;
@@ -52,11 +62,7 @@ userRouter.post("/users/signup", async (ctx) => {
ctx.throw(400); ctx.throw(400);
} }
const { username, password, email } = request.body as { const { username, password, email } = request.body as IUserSignupBody;
username: string | undefined;
password: string | undefined;
email: string | undefined;
};
if (!(username && password && email)) { if (!(username && password && email)) {
ctx.throw(400); 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<IUserAuthJSON>;
userRouter.post("/users/edit", async (ctx) => { userRouter.post("/users/edit", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -96,9 +106,7 @@ userRouter.post("/users/edit", async (ctx) => {
return; return;
} }
const { password } = request.body as { const { password } = request.body as IUserEditBody;
password: string | undefined;
};
if (!password) { if (!password) {
ctx.throw(400); ctx.throw(400);
@@ -113,5 +121,5 @@ userRouter.post("/users/edit", async (ctx) => {
ctx.throw(400); ctx.throw(400);
} }
ctx.body = { error: false, data: user.toAuthJSON() }; ctx.body = { error: false, data: user.toAuthJSON() } as IUserEditRespBody;
}); });

View File

@@ -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);
}
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,9 +1,18 @@
import { expect } from "chai"; import { assert, expect } from "chai";
import { connect } from "config/database"; import { connect } from "config/database";
import * as request from "supertest"; import * as request from "supertest";
import { getConnection } from "typeorm"; import { getConnection } from "typeorm";
import { app } from "~app"; import { app } from "~app";
import { IUserAuthJSON, User } from "~entity/User"; import { IUserAuthJSON, User } from "~entity/User";
import {
IUserEditBody,
IUserEditRespBody,
IUserGetRespBody,
IUserLoginBody,
IUserLoginRespBody,
IUserSignupBody,
IUserSignupRespBody,
} from "~routes/users";
import { ISeed, seedDB } from "./util"; import { ISeed, seedDB } from "./util";
@@ -34,9 +43,14 @@ describe("users", function () {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .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()); expect(user).to.deep.equal(seed.user1.toJSON());
}); });
@@ -45,14 +59,18 @@ describe("users", function () {
const response = await request(callback) const response = await request(callback)
.post("/users/login") .post("/users/login")
.set({ "Content-Type": "application/json" }) .set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" }) .send({ username: "User1", password: "User1" } as IUserLoginBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .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()); expect(user).to.deep.equal(seed.user1.toJSON());
}); });
@@ -60,11 +78,12 @@ describe("users", function () {
const response = await request(callback) const response = await request(callback)
.post("/users/login") .post("/users/login")
.set({ "Content-Type": "application/json" }) .set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "asdf" }) .send({ username: "User1", password: "asdf" } as IUserLoginBody)
.expect(404); .expect(404);
expect(response.body.error).to.be.equal("User not found"); const body = response.body as IUserLoginRespBody;
expect(response.body.data).to.be.false; expect(body.error).to.be.equal("User not found");
expect(body.data).to.be.false;
}); });
it("should signup user", async function () { it("should signup user", async function () {
@@ -75,16 +94,19 @@ describe("users", function () {
username: "NUser1", username: "NUser1",
password: "NUser1", password: "NUser1",
email: "nuser1@users.com", email: "nuser1@users.com",
}) } as IUserSignupBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .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" }); const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON()); expect(user).to.deep.equal(newUser.toJSON());
}); });
@@ -96,11 +118,13 @@ describe("users", function () {
username: "User1", username: "User1",
password: "NUser1", password: "NUser1",
email: "user1@users.com", email: "user1@users.com",
}) } as IUserSignupBody)
.expect(400); .expect(400);
expect(response.body.error).to.be.equal("User already exists"); const body = response.body as IUserSignupRespBody;
expect(response.body.data).to.be.false;
expect(body.error).to.be.equal("User already exists");
expect(body.data).to.be.false;
}); });
it("should change user's password", async function () { it("should change user's password", async function () {
@@ -112,31 +136,46 @@ describe("users", function () {
}) })
.send({ .send({
password: "User1NewPass", password: "User1NewPass",
}) } as IUserEditBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .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) const loginResponse = await request(callback)
.post("/users/login") .post("/users/login")
.set({ "Content-Type": "application/json" }) .set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1NewPass" }) .send({
username: "User1",
password: "User1NewPass",
} as IUserLoginBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .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()); expect(user).to.deep.equal(seed.user1.toJSON());
const badLoginResponse = await request(callback) const badLoginResponse = await request(callback)
.post("/users/login") .post("/users/login")
.set({ "Content-Type": "application/json" }) .set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" }) .send({ username: "User1", password: "User1" } as IUserLoginBody)
.expect(404); .expect(404);
expect(badLoginResponse.body.error).to.be.equal("User not found"); const badLoginBody = badLoginResponse.body as IUserLoginRespBody;
expect(badLoginResponse.body.data).to.be.false;
expect(badLoginBody.error).to.be.equal("User not found");
expect(badLoginBody.data).to.be.false;
}); });
}); });

View File

@@ -1,15 +1,44 @@
import * as fs from "fs/promises";
import { User } from "entity/User"; 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 { export interface ISeed {
user1: User; user1: User;
user2: User; user2: User;
// doc1: Document; dogPhoto: Photo;
// doc2p: Document; 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<void> {
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<ISeed> { export async function seedDB(): Promise<ISeed> {
//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()); await User.remove(await User.find());
const user1 = new User("User1", "user1@users.com"); const user1 = new User("User1", "user1@users.com");
@@ -20,11 +49,14 @@ export async function seedDB(): Promise<ISeed> {
await user2.setPassword("User2"); await user2.setPassword("User2");
await user2.save(); await user2.save();
//const doc1 = new Document(user1, "Doc1", "Doc1", false); const dogPhoto = new Photo(user2, dogHash, dogSize, dogFormat);
//const doc2p = new Document(user1, "Doc2", "Doc2", true); const catPhoto = new Photo(user2, catHash, catSize, catFormat);
//await doc1.save(); await fs.copyFile(dogPath, dogPhoto.getPath());
//await doc2p.save(); await fs.copyFile(catPath, catPhoto.getPath());
return { user1, user2 }; // doc1, doc2p }; await dogPhoto.save();
await catPhoto.save();
return { user1, user2, dogPhoto, catPhoto };
} }

View File

@@ -1,4 +1,12 @@
export interface IAPIResponse<T> { // eslint-disable-next-line @typescript-eslint/no-unused-vars
data: T | null; interface IAPIErrorResponse<T> {
error: string | null; data: null;
error: string;
} }
interface IAPISuccessResponse<T> {
error: false;
data: T;
}
export type IAPIResponse<T> = IAPIErrorResponse<T> | IAPISuccessResponse<T>;

24
src/util.ts Normal file
View File

@@ -0,0 +1,24 @@
import deasync = require("deasync");
import { fromFile } from "hasha";
import sharp = require("sharp");
export async function getHash(file: string): Promise<string> {
return await fromFile(file, {
algorithm: "md5",
});
}
export async function getSize(file: string): Promise<string> {
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);