diff --git a/.gitignore b/.gitignore index 3595cc7..6a74c48 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ pnpm-debug.log* # macOS-specific files .DS_Store -slicers/* \ No newline at end of file +slicers/* + +# Coverage +coverage/ diff --git a/Dockerfile b/Dockerfile index 1204b15..00242bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,15 +47,15 @@ ENV CONFIGS_PATH=./configs # ENV SLICER_PATH slic3r # Download & install PrusaSlicer -ADD https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.5.2/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201.tar.bz2 ./ -RUN tar -xvf PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201.tar.bz2 -C /opt +ADD https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.6.0/PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220.tar.bz2 ./ +RUN tar -xvf PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220.tar.bz2 -C /opt RUN apt-get update \ && apt-get install -y --no-install-recommends \ prusa-slicer \ && apt-get remove prusa-slicer -y \ && rm -rf /var/lib/apt/lists/* -ENV PATH /opt/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201/bin:$PATH +ENV PATH /opt/PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220/bin:$PATH ENV SLICER_PATH prusa-slicer # run as non root user diff --git a/package-lock.json b/package-lock.json index 0c058e0..afe9015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,21 @@ "dependencies": { "@astrojs/node": "^5.2.0", "@astrojs/tailwind": "^3.1.3", + "@dzeio/logger": "^3.0.0", "@dzeio/object-util": "^1.5.0", "@dzeio/url-manager": "^1.0.9", "astro": "^2.6.4", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.0", "mathjs": "^11.8.1", "mongoose": "^7.3.0", "tailwindcss": "^3.3.2" }, "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.3.1", + "@vitest/coverage-v8": "^0.32.2", "cypress": "^12.15.0", "vitest": "^0.32.2" } @@ -519,6 +525,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -577,6 +589,14 @@ "ms": "^2.1.1" } }, + "node_modules/@dzeio/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@dzeio/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-CmqmIblP6XZdapWOwmoYG3/lBX7HX1vK0i6hY6ArjCyjg8dWpLierx6vagnfhk5NIIs1jpIS3hR759QJwFE9xw==", + "dependencies": { + "ansi-colors": "^4.1.1" + } + }, "node_modules/@dzeio/object-util": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.5.0.tgz", @@ -941,6 +961,15 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -1091,6 +1120,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "node_modules/@types/chai": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", @@ -1122,11 +1157,26 @@ "@types/unist": "*" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, "node_modules/@types/json5": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.30.tgz", "integrity": "sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", @@ -1209,6 +1259,43 @@ "@types/node": "*" } }, + "node_modules/@vitest/coverage-v8": { + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.32.2.tgz", + "integrity": "sha512-/+V3nB3fyeuuSeKxCfi6XmWjDIxpky7AWSkGVfaMjAk7di8igBwRsThLjultwIZdTDH1RAxpjmCXEfSqsMFZOA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "magic-string": "^0.30.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.32.0 <1" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@vitest/expect": { "version": "0.32.2", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.2.tgz", @@ -1394,7 +1481,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, "engines": { "node": ">=6" } @@ -1734,6 +1820,11 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -1965,6 +2056,11 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -3099,6 +3195,14 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4224,6 +4328,83 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports/node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -4319,6 +4500,21 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -4334,6 +4530,25 @@ "verror": "1.10.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", @@ -4569,8 +4784,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.once": { "version": "4.1.1", @@ -4813,6 +5027,30 @@ "node": ">=12" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/markdown-table": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", @@ -7375,6 +7613,15 @@ "npm": ">= 3.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -7685,6 +7932,20 @@ "node": ">=10.13.0" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8159,6 +8420,20 @@ "node": ">=8" } }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index fddbae8..b260831 100644 --- a/package.json +++ b/package.json @@ -9,20 +9,26 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", - "test": "vitest" + "test": "vitest --coverage" }, "dependencies": { "@astrojs/node": "^5.2.0", "@astrojs/tailwind": "^3.1.3", + "@dzeio/logger": "^3.0.0", "@dzeio/object-util": "^1.5.0", "@dzeio/url-manager": "^1.0.9", "astro": "^2.6.4", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.0", "mathjs": "^11.8.1", "mongoose": "^7.3.0", "tailwindcss": "^3.3.2" }, "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.3.1", + "@vitest/coverage-v8": "^0.32.2", "cypress": "^12.15.0", "vitest": "^0.32.2" } diff --git a/src/env.d.ts b/src/env.d.ts index 45c3685..092db3c 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -3,7 +3,10 @@ interface ImportMetaEnv { CONFIGS_PATH?: string PRUSASLICER_PATH?: string + BAMBUSTUDIO_PATH?: string MONGODB?: string + PRIVATE_KEY?: string + PUBLIC_KEY?: string } interface ImportMeta { diff --git a/src/libs/AuthUtils.ts b/src/libs/AuthUtils.ts new file mode 100644 index 0000000..867f269 --- /dev/null +++ b/src/libs/AuthUtils.ts @@ -0,0 +1,9 @@ +import bcryptjs from 'bcryptjs' + +export function hashPassword(password: string): Promise { + return bcryptjs.hash(password, 10) +} + +export function comparePassword(password: string, hash: string): Promise { + return bcryptjs.compare(password, hash) +} diff --git a/src/libs/CookieManager.ts b/src/libs/CookieManager.ts index 949e326..bb347eb 100644 --- a/src/libs/CookieManager.ts +++ b/src/libs/CookieManager.ts @@ -1,11 +1,12 @@ -import type { ServerResponse } from 'node:http' - export default class CookieManager { private cookies: Record = {} public constructor(data?: string | Record) { if (typeof data === 'string') { data.split(';').forEach((keyValuePair) => { const [key, value] = keyValuePair.split('=') + if (!key || !value) { + return + } this.cookies[key.trim()] = value.trim().replace(/%3B/g, ';') }) } else if (typeof data === 'object') { @@ -13,7 +14,7 @@ export default class CookieManager { } } - public static addCookie(res: ServerResponse, cookie: { + public static addCookie(res: ResponseInit & { readonly headers: Headers; }, cookie: { key: string value: string expire?: string @@ -46,7 +47,7 @@ export default class CookieManager { if (cookie.sameSite) { items.push(`SameSite=${cookie.sameSite}`) } - res.setHeader('Set-Cookie', items.join('; ')) + res.headers.append('Set-Cookie', items.join('; ')) } public get(key: string): string | undefined { diff --git a/src/libs/validateAuth.ts b/src/libs/validateAuth.ts index 6447524..160077e 100644 --- a/src/libs/validateAuth.ts +++ b/src/libs/validateAuth.ts @@ -1,6 +1,6 @@ import DaoFactory from '../models/DaoFactory' import CookieManager from './CookieManager' -import RFC7807, { buildRFC7807 } from './RFCs/RFC7807' +import { buildRFC7807 } from './RFCs/RFC7807' interface Permission { name: string diff --git a/src/models/Config/ConfigDao.ts b/src/models/Config/ConfigDao.ts index df3e11a..bb77dd4 100644 --- a/src/models/Config/ConfigDao.ts +++ b/src/models/Config/ConfigDao.ts @@ -1,21 +1,83 @@ +import { objectOmit } from '@dzeio/object-util' +import mongoose from 'mongoose' import type Config from '.' +import Client from '../Client' import Dao from '../Dao' export default class ConfigDao extends Dao { - private idx = 0 - public async create(obj: Omit): Promise { - console.log('pouet', this.idx++) - return null - // throw new Error('Method not implemented.') + + // @ts-expect-error typing fix + private model = mongoose.models['Config'] as null ?? mongoose.model('Config', new mongoose.Schema({ + user: { type: String, required: true }, + type: { type: String, required: true}, + files: [{ + name: { type: String, unique: true, required: true}, + data: { type: Buffer, required: true } + }] + }, { + timestamps: true + })) + + + public async create(obj: Omit): Promise { + await Client.get() + return this.fromSource(await this.model.create(obj)) } + public async findAll(query?: Partial | undefined): Promise { - throw new Error('Method not implemented.') + await Client.get() + try { + if (query?.id) { + const item = await this.model.findById(new mongoose.Types.ObjectId(query.id)) + if (!item) { + return [] + } + return [this.fromSource(item)] + } + const resp = await this.model.find(query ? this.toSource(query as Config) : {}) + return resp.map(this.fromSource) + } catch (e) { + console.error(e) + return [] + } + } + public async update(obj: Config): Promise { - throw new Error('Method not implemented.') + await Client.get() + + const query = await this.model.updateOne({ + _id: new mongoose.Types.ObjectId(obj.id) + }, this.toSource(obj)) + if (query.matchedCount >= 1) { + obj.updated = new Date() + return obj + } + return null + // return this.fromSource() } + public async delete(obj: Config): Promise { - throw new Error('Method not implemented.') + await Client.get() + const res = await this.model.deleteOne({ + _id: new mongoose.Types.ObjectId(obj.id) + }) + return res.deletedCount > 0 + } + + private toSource(obj: Config): Omit { + return objectOmit(obj, 'id', 'updated', 'created') + } + + private fromSource(doc: mongoose.Document): Config { + return { + id: doc._id.toString(), + user: doc.get('user'), + type: doc.get('type'), + files: doc.get('files') ?? [], + updated: doc.get('updatedAt'), + created: doc.get('createdAt') + } } } diff --git a/src/models/Config/index.ts b/src/models/Config/index.ts index fe3d8a7..26a5b9e 100644 --- a/src/models/Config/index.ts +++ b/src/models/Config/index.ts @@ -1,6 +1,11 @@ -import type User from '../User' - export default interface Config { id: string user: string + type: 'prusa' + files: Array<{ + name: string + data: Buffer + }> + created: Date + updated: Date } diff --git a/src/models/DaoFactory.ts b/src/models/DaoFactory.ts index 3646e70..68077a6 100644 --- a/src/models/DaoFactory.ts +++ b/src/models/DaoFactory.ts @@ -1,6 +1,6 @@ import APIKeyDao from './APIKey/APIKeyDao' import ConfigDao from './Config/ConfigDao' -import Dao from './Dao' +import SessionDao from './Session/SessionDao' import UserDao from './User/UserDao' /** @@ -18,6 +18,7 @@ interface DaoItem { config: ConfigDao user: UserDao apiKey: APIKeyDao + session: SessionDao } /** @@ -54,11 +55,12 @@ export default class DaoFactory { * @param item the element to init * @returns a new initialized dao or undefined if no dao is linked */ - private static initDao(item: keyof DaoItem): Dao | undefined { + private static initDao(item: keyof DaoItem): any | undefined { switch (item) { case 'config': return new ConfigDao() case 'user': return new UserDao() case 'apiKey': return new APIKeyDao() + case 'session': return new SessionDao() default: return undefined } } diff --git a/src/models/Session/SessionDao.ts b/src/models/Session/SessionDao.ts new file mode 100644 index 0000000..c7554a6 --- /dev/null +++ b/src/models/Session/SessionDao.ts @@ -0,0 +1,53 @@ +import jwt, { SignOptions } from 'jsonwebtoken' +import type Session from '.' +import CookieManager from '../../libs/CookieManager' + +export interface SessionOptions { + cookieName: string + security: SignOptions + key?: string + privateKey?: string + publicKey?: string +} + + +export default class SessionDao { + + private options: SessionOptions = { + cookieName: 'session', + security: { + algorithm: 'ES512' + }, + privateKey: import.meta.env.PRIVATE_KEY ?? '', + publicKey: import.meta.env.PUBLIC_KEY ?? '' + } + + public getSession(req: Request): Session | null { + const cookie = new CookieManager(req.headers.get('Cookie') ?? '').get(this.options.cookieName) + if (!cookie) { + return null + } + try { + return jwt.verify(cookie, (this.options.publicKey || this.options.key) as string) as Session + } catch { + return null + } + } + + public setSession(session: Session, res: ResponseInit & { readonly headers: Headers; }) { + const token = jwt.sign(session, (this.options.privateKey || this.options.key) as string, this.options.security) + CookieManager.addCookie(res, { + key: this.options.cookieName, + value: token, + httpOnly: true, + path: '/', + secure: true, + sameSite: 'Strict', + maxAge: 365000 + }) + } + + public removeSession(res: ResponseInit & { readonly headers: Headers; }) { + + } +} diff --git a/src/models/Session/index.ts b/src/models/Session/index.ts new file mode 100644 index 0000000..df263da --- /dev/null +++ b/src/models/Session/index.ts @@ -0,0 +1,3 @@ +export default interface Session { + userId: string +} diff --git a/src/models/User/UserDao.ts b/src/models/User/UserDao.ts index e8cae67..4151aaa 100644 --- a/src/models/User/UserDao.ts +++ b/src/models/User/UserDao.ts @@ -1,6 +1,6 @@ import { objectOmit } from '@dzeio/object-util' -import mongoose, { ObjectId } from 'mongoose' -import User from '.' +import mongoose from 'mongoose' +import type User from '.' import Client from '../Client' import Dao from '../Dao' @@ -8,7 +8,8 @@ export default class UserDao extends Dao { // @ts-expect-error typing fix private model = mongoose.models['User'] as null ?? mongoose.model('User', new mongoose.Schema({ - email: { type: String, required: true } + email: { type: String, required: true }, + password: { type: String, required: true } }, { timestamps: true })) @@ -62,6 +63,7 @@ export default class UserDao extends Dao { return { id: doc._id.toString(), email: doc.get('email'), + password: doc.get('password'), updated: doc.get('updatedAt'), created: doc.get('createdAt') } diff --git a/src/models/User/index.ts b/src/models/User/index.ts index 25839b7..70e8c57 100644 --- a/src/models/User/index.ts +++ b/src/models/User/index.ts @@ -1,6 +1,7 @@ export default interface User { id: string email: string + password: string created: Date updated: Date } diff --git a/src/pages/account/login.astro b/src/pages/account/login.astro index 792489e..974d999 100644 --- a/src/pages/account/login.astro +++ b/src/pages/account/login.astro @@ -3,16 +3,23 @@ import URLManager from '@dzeio/url-manager' import Layout from '../../layouts/Layout.astro' import DaoFactory from '../../models/DaoFactory' import { comparePassword } from '../../libs/AuthUtils' +import Passthrough from '../../components/Passthrough.astro' -const logout = new URLManager(Astro.url).query('logout') +const logout = typeof new URLManager(Astro.url).query('logout') === 'string' -if (typeof logout === 'string') { +if (logout) { DaoFactory.get('session').removeSession(Astro.response) } // DaoFactory.get('session').removeSession(Astro.response) -if (Astro.request.method === 'POST') { +let connected = false +const sessionDao = DaoFactory.get('session') +if (sessionDao.getSession(Astro.request) && !logout) { + connected = true +} + +if (!connected && Astro.request.method === 'POST') { const form = await Astro.request.formData() const email = form.get('email') as string const password = form.get('password') as string @@ -21,27 +28,39 @@ if (Astro.request.method === 'POST') { email }) - if (!account) { - return + if (account) { + const valid = await comparePassword(password, account.password) + if (valid) { + DaoFactory.get('session').setSession({ + userId: account.id + }, Astro.response) + connected = true + } } - const valid = await comparePassword(password, account.password) - if (!valid) { - return - } - DaoFactory.get('session').setSession({ - userId: account.id - }, Astro.response) } ---
-
+
+
+ + diff --git a/src/pages/admin.astro b/src/pages/admin.astro index 16afba9..a9a1864 100644 --- a/src/pages/admin.astro +++ b/src/pages/admin.astro @@ -7,19 +7,31 @@ const user = await DaoFactory.get('user').get('648f81f857503c7d29465318') const list = await DaoFactory.get('apiKey').findAll({ user: user!.id }) +const configs = await DaoFactory.get('config').findAll({ + user: user!.id +}) const userId = user?.id ?? 'unknown' ---
+

{user?.id}

    +
  • API Keys
  • {list.map((it) => (
  • access key: {it.key}

    permissions: {it.permissions}

  • ))} +
  • Configurations
  • + {configs.map((it) => ( +
  • +

    {it.id}: {it.type}

    +

    {it.files.map((it) => it.name)}

    +
  • + ))}
diff --git a/src/pages/api/process/[configId].ts b/src/pages/api/process/[configId].ts index 2f9982b..edce7ac 100644 --- a/src/pages/api/process/[configId].ts +++ b/src/pages/api/process/[configId].ts @@ -1,15 +1,17 @@ +import Logger from '@dzeio/logger' import { objectMap, objectOmit } from '@dzeio/object-util' import URLManager from '@dzeio/url-manager' import type { APIRoute } from 'astro' import { evaluate } from 'mathjs' -import { exec as execSync } from 'node:child_process' +import { exec as execSync, spawn } from 'node:child_process' import fs from 'node:fs/promises' import os from 'node:os' -import path from 'node:path/posix' +import path from 'node:path' import { promisify } from 'node:util' -import FilesUtils from '../../../libs/FilesUtils' import { buildRFC7807 } from '../../../libs/RFCs/RFC7807' import { getParams } from '../../../libs/gcodeUtils' +import { validateAuth } from '../../../libs/validateAuth' +import DaoFactory from '../../../models/DaoFactory' const exec = promisify(execSync) @@ -21,50 +23,101 @@ let tmpDir: string * price: algorithm from settings * adionnal settings from https://manual.slic3r.org/advanced/command-line */ -export const post: APIRoute = async ({ request }) => { +export const post: APIRoute = async ({ params, request }) => { + const res = await validateAuth(request, { + name: 'slicing.slice', + api: true, + cookie: true + }) + if (res) { + return res + } if (!tmpDir) { - tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'paas-')) + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'saas-')) + } + + const configId = params.configId ?? 'undefined' + const config = await DaoFactory.get('config').get(configId) + + if (!config) { + return buildRFC7807({ + type: '/missing-config', + status: 404, + title: 'The configuration does not exists', + details: `The configuration ${configId} does not exists` + }) } const query = new URLManager(request.url).query() - const file = (Math.random() * 1000000).toFixed(0) - console.log('started processing new request', file) - await fs.mkdir(`${tmpDir}/files`, { recursive: true }) - const overrides = objectOmit(query, 'algo', 'config') - const configName = query?.config ?? 'config' - let config = `${import.meta.env.CONFIGS_PATH}/` + configName + '.ini' - if (!await FilesUtils.exists(config)) { - console.log('request finished in error :(', file) - return buildRFC7807({ - type: '/missing-config-file', - status: 404, - title: 'Configuration file is missing', - details: `the configuration file "${configName}" is not available on the remote server` - }) + const processId = (Math.random() * 1000000).toFixed(0) + const logger = new Logger(`process-${processId}`) + const processFolder = `${tmpDir}/${processId}` + const pouet = await fs.mkdir(processFolder, { recursive: true }) + + logger.log('poeut', pouet) + + logger.log('started processing request') + + logger.log('writing configs to dir') + for (const file of config.files) { + await fs.writeFile(`${processFolder}/${file.name}`, file.data) } - const stlPath = `${tmpDir}/files/${file}.stl` - const gcodePath = `${tmpDir}/files/${file}.gcode` - // write file + + const overrides = objectOmit(query, 'algo') + + const stlPath = `${processFolder}/input.stl` + const gcodePath = `${processFolder}/output.gcode` + + logger.log('writing STL to filesystem') + // write input await fs.writeFile(stlPath, new Uint8Array(Buffer.from(await request.arrayBuffer())), { encoding: null }) - // console.log(fs.statSync(stlPath).size, req.body.length) + + // additionnal parameters let additionnalParams = objectMap(overrides, (value, key) => `--${(key as string).replace(/_/g, '-')} ${value}`).join(' ') - const slicer = import.meta.env.PRUSASLICER_PATH ?? 'prusa-slicer' - if (slicer.includes('prusa')) { + + + let slicerPath: string + let slicerCommand: string + + if (config.type === 'prusa' || true) { + slicerPath = import.meta.env.PRUSASLICER_PATH ?? 'prusa-slicer' additionnalParams += ' --export-gcode' + slicerCommand = `${path.normalize(stlPath)} --load ${path.normalize(`${processFolder}/config.ini`)} --output ${path.normalize(gcodePath)} ${additionnalParams}` } - const cmd = `${slicer} ${stlPath} --load ${config} --output ${gcodePath} ${additionnalParams}` + try { - await exec(cmd, { - timeout: 60000 + logger.log('Running', slicerPath, slicerCommand) + await new Promise((res, rej) => { + const slicer = spawn(slicerPath, slicerCommand.split(' ')) + slicer.stdout.on('data', (data) => { + logger.log('[stdout]',data.toString('utf8')) + }) + slicer.stderr.on('data', (data: Buffer) => { + logger.log('[stderr]', data.toString('utf8')) + }) + slicer.on('error', (err) => { + logger.log('error', err) + rej(err) + }) + slicer.on('close', (code, signal) => { + logger.log('code', code) + logger.log('signal', signal) + if (typeof code === 'number' && code > 0) { + + rej(code) + return + } + res() + }) }) } catch (e: any) { - console.log('request finished in error :(', file) + logger.log('request finished in error :(', processId) const line = e.toString() - console.error(e) + logger.error(e) if (line.includes('Objects could not fit on the bed')) { await fs.rm(stlPath) return buildRFC7807({ @@ -78,7 +131,7 @@ export const post: APIRoute = async ({ request }) => { type: '/missing-config-file', status: 404, title: 'Configuration file is missing', - details: `the configuration file "${configName}" is not available on the remote server` + details: `the configuration file "${configId}" is not available on the remote server` }) } else if (line.includes('Unknown option')) { await fs.rm(stlPath) @@ -115,18 +168,18 @@ export const post: APIRoute = async ({ request }) => { status: 500, title: 'General I/O error', details: 'A server error make it impossible to slice the file, please contact an administrator with the json error', - fileId: file, - config: configName, + fileId: processId, + config: configId, // fileSize: req.body.length, overrides: overrides, serverMessage: - e.toString().replace(cmd, '***SLICER***').replace(stlPath, configName ?? `***FILE***`).replace(`${path}/configs/`, '').replace('.ini', '') + e.toString().replace(new RegExp(stlPath), `***FILE***`).replace(new RegExp(processFolder), '') }) } const gcode = await fs.readFile(gcodePath, 'utf-8') - await fs.rm(stlPath) - await fs.rm(gcodePath) - const params = getParams(gcode) + await fs.rm(processFolder, { recursive: true, force: true }) + logger.log('Getting parameters') + const gcodeParams = getParams(gcode) let price: string | undefined if (query?.algo) { let algo = decodeURI(query.algo as string) @@ -139,7 +192,8 @@ export const post: APIRoute = async ({ request }) => { // } // }) try { - const tmp = evaluate(algo, params) + logger.log('Evaluating Alogrithm') + const tmp = evaluate(algo, gcodeParams) if (typeof tmp === 'number') { price = tmp.toFixed(2) } else { @@ -150,11 +204,11 @@ export const post: APIRoute = async ({ request }) => { details: 'It seems the algorithm resolution failed', algorithm: algo, algorithmError: 'Algorithm return a Unit', - parameters: params + parameters: gcodeParams }) } } catch (e) { - console.dir(e) + logger.dir(e) return buildRFC7807({ type: '/algorithm-error', status: 500, @@ -162,11 +216,11 @@ export const post: APIRoute = async ({ request }) => { details: 'It seems the algorithm resolution failed', algorithm: algo, algorithmError: e, - parameters: params + parameters: gcodeParams }) } } - console.log('request successfull :)', file) + logger.log('request successfull :)') return { status: 200, body: JSON.stringify({ diff --git a/src/pages/api/users/[userId]/configs/[configId]/files/[fileName].ts b/src/pages/api/users/[userId]/configs/[configId]/files/[fileName].ts new file mode 100644 index 0000000..14639c4 --- /dev/null +++ b/src/pages/api/users/[userId]/configs/[configId]/files/[fileName].ts @@ -0,0 +1,26 @@ +import { objectOmit } from '@dzeio/object-util' +import type { APIRoute } from 'astro' +import { buildRFC7807 } from '../../../../../../../libs/RFCs/RFC7807' +import DaoFactory from '../../../../../../../models/DaoFactory' + +export const get: APIRoute = async ({ params, request }) => { + const userId = params.userId as string + const configId = params.configId as string + const fileName = params.fileName as string + + + const dao = await DaoFactory.get('config').get(configId) + + if (!dao) { + return buildRFC7807({ + title: 'Config does not exists :(' + }) + } + + const file = dao.files.find((it) => it.name === fileName) + + return { + status: 200, + body: file?.data + } +} diff --git a/src/pages/api/users/[userId]/configs/index.ts b/src/pages/api/users/[userId]/configs/index.ts new file mode 100644 index 0000000..9c3ea5c --- /dev/null +++ b/src/pages/api/users/[userId]/configs/index.ts @@ -0,0 +1,44 @@ +import { objectOmit } from '@dzeio/object-util' +import type { APIRoute } from 'astro' +import { buildRFC7807 } from '../../../../../libs/RFCs/RFC7807' +import DaoFactory from '../../../../../models/DaoFactory' + +export const post: APIRoute = async ({ params, request }) => { + const userId = params.userId as string + + const body = request.body + + if (!body) { + return buildRFC7807({ + title: 'Missing config file' + }) + } + + const reader = body.getReader() + + const chunks: Array = [] + + let finished= false + do { + const { done, value } = await reader.read() + finished = done + if (value) { + chunks.push(value) + } + } while (!finished) + + const buffer = Buffer.concat(chunks) + + const dao = await DaoFactory.get('config').create({ + user: userId, + type: 'prusa', + files: [{ + name: 'config.ini', + data: buffer + }] + }) + return { + status: 201, + body: JSON.stringify(objectOmit(dao ?? {}, 'files')) + } +} diff --git a/src/pages/api/users/[userId]/keys/index.ts b/src/pages/api/users/[userId]/keys/index.ts index 4015342..ff6344c 100644 --- a/src/pages/api/users/[userId]/keys/index.ts +++ b/src/pages/api/users/[userId]/keys/index.ts @@ -1,8 +1,14 @@ import type { APIRoute } from 'astro' import crypto from 'node:crypto' +import { validateAuth } from '../../../../../libs/validateAuth' import DaoFactory from '../../../../../models/DaoFactory' export const post: APIRoute = async ({ params, request }) => { + validateAuth(request, { + name: 'keys.create', + cookie: true, + api: false + }) const userId = params.userId as string const dao = await DaoFactory.get('apiKey').create({ diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 6fa3aab..7f548ad 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -1,4 +1,5 @@ import { assert, expect, test } from 'vitest' +import { comparePassword, hashPassword } from '../src/libs/AuthUtils' // Edit an assertion and save to see HMR in action @@ -19,3 +20,14 @@ test('JSON', () => { expect(output).eq('{"foo":"hello","bar":"world"}'); assert.deepEqual(JSON.parse(output), input, 'matches original'); }); + +test('auth util', async () => { + const password = 'pouet' + + const out1 = await hashPassword(password) + expect(out1).not.toBe(password) + const out2 = await hashPassword(password) + expect(out2).not.toBe(out1) + expect(await comparePassword(password, out1)).toBe(true) + expect(await comparePassword(password, out2)).toBe(true) +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff1a0be --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/astro/tsconfigs/strictest.json" +}