From c3fbaabcb079d102013a2e73aa3bad744f6ba300 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:34:41 +0300 Subject: [PATCH 01/14] Prettier integration --- package-lock.json | 16 ++++++++++++++++ package.json | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8d94049..dc5bc11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@types/node": "^22.1.0", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.8", + "prettier": "3.3.3", "ts-node": "^10.9.2", "typescript": "^5.2.2" } @@ -2308,6 +2309,21 @@ "node": ">= 0.4" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index 275d219..58f790e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@types/node": "^22.1.0", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.8", + "prettier": "3.3.3", "ts-node": "^10.9.2", "typescript": "^5.2.2" }, @@ -44,4 +45,4 @@ "swagger-ui-express": "^5.0.1", "uuid": "^9.0.1" } -} \ No newline at end of file +} From 5c2c148157e8097f8e7079d071f95ca5ab9c6344 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:35:16 +0300 Subject: [PATCH 02/14] feat: Prettier scripts --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 58f790e..335c0b8 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "@types/uuid": "^9.0.8", "prettier": "3.3.3", "ts-node": "^10.9.2", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "format:write": "prettier . --write", + "format:check": "prettier . --check" }, "scripts": { "build": "npx tsc --build", From e72ef4549d21f39533f4a9e6c5f3339d4a09d977 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:35:43 +0300 Subject: [PATCH 03/14] feat: prettier configuration file --- .prettierrc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} From 019bb5ee5a29824baa9877fd588123c1d69ba3a8 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:37:20 +0300 Subject: [PATCH 04/14] feat: gh prettier workflow --- .github/workflows/format.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/format.yaml diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 0000000..20fc16e --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,12 @@ +name: Prettier Workflow +run-name: ${{ github.actor }} running the job +on: [push] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: npm install + - name: Run prettier check + run: npm run format:check From 687f2a1ed77a2fa13dc4870bfe6fbf99b4b874d2 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:40:17 +0300 Subject: [PATCH 05/14] patch: scripts fix --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 335c0b8..758a98a 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,7 @@ "@types/uuid": "^9.0.8", "prettier": "3.3.3", "ts-node": "^10.9.2", - "typescript": "^5.2.2", - "format:write": "prettier . --write", - "format:check": "prettier . --check" + "typescript": "^5.2.2" }, "scripts": { "build": "npx tsc --build", @@ -24,7 +22,9 @@ "stop-playground": "docker compose -p playground -f ./docker/docker-compose.playground.yml down", "dev": "docker compose -p dev -f ./docker/docker-compose.dev.yml down && docker compose -p dev -f ./docker/docker-compose.dev.yml up --build", "dev:watch-api": "nodemon --config nodemon-api.json", - "dev:watch-jobs": "nodemon --config nodemon-jobs.json" + "dev:watch-jobs": "nodemon --config nodemon-jobs.json", + "format:write": "prettier . --write", + "format:check": "prettier . --check" }, "dependencies": { "@types/express": "^4.17.17", From ca04a6239bee4a66f5c6b8a828c2e9cb78a99f90 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:42:04 +0300 Subject: [PATCH 06/14] feat: linter --- eslint.config.mjs | 12 + package-lock.json | 1367 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 6 +- 3 files changed, 1359 insertions(+), 26 deletions(-) create mode 100644 eslint.config.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..8cbf6eb --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,12 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {files: ["**/*.{js,mjs,cjs,ts}"]}, + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dc5bc11..2db6780 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,15 +30,19 @@ "uuid": "^9.0.1" }, "devDependencies": { + "@eslint/js": "^9.9.1", "@types/cors": "^2.8.17", "@types/express-session": "^1.18.0", "@types/multer": "^1.4.11", "@types/node": "^22.1.0", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.8", + "eslint": "^9.9.1", + "globals": "^15.9.0", "prettier": "3.3.3", "ts-node": "^10.9.2", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "typescript-eslint": "^8.2.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -93,6 +97,181 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -123,6 +302,41 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -340,27 +554,314 @@ "@types/node": "*" } }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", - "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", "dev": true, - "license": "MIT", "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" + "@typescript-eslint/types": "8.2.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true - }, - "node_modules/@types/validator": { - "version": "13.11.9", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", - "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", @@ -397,9 +898,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -408,6 +909,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", @@ -498,6 +1008,15 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -733,11 +1252,57 @@ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/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/chalk/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/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -930,6 +1495,20 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -957,6 +1536,12 @@ "node": ">=0.10" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1015,6 +1600,18 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1093,6 +1690,190 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1204,11 +1985,33 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -1230,6 +2033,27 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1267,6 +2091,41 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -1454,6 +2313,38 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1465,6 +2356,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1600,10 +2497,44 @@ "node": ">=0.10.0" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } }, "node_modules/inflection": { "version": "1.13.4", @@ -1722,6 +2653,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -1751,6 +2691,12 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -1781,6 +2727,12 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1791,6 +2743,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -1810,6 +2768,43 @@ "node": ">=0.6.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1825,6 +2820,12 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -1865,6 +2866,15 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1873,6 +2883,19 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -2064,6 +3087,12 @@ "node": ">=12" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2259,6 +3288,65 @@ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "peer": true }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2267,6 +3355,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2275,11 +3372,29 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -2309,6 +3424,15 @@ "node": ">= 0.4" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", @@ -2395,6 +3519,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -2529,11 +3673,53 @@ "uuid": "bin/uuid" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/retry-as-promised": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2743,6 +3929,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -2771,6 +3978,15 @@ "node": ">=10" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -2885,6 +4101,18 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strnum": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", @@ -2951,6 +4179,12 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -3025,6 +4259,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -3084,6 +4330,18 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3114,6 +4372,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.2.0.tgz", + "integrity": "sha512-DmnqaPcML0xYwUzgNbM1XaKXpEb7BShYf2P1tkUmmcl8hyeG7Pj08Er7R9bNy6AufabywzJcOybQAtnD/c9DGw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.2.0", + "@typescript-eslint/parser": "8.2.0", + "@typescript-eslint/utils": "8.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -3261,6 +4542,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", @@ -3287,6 +4583,15 @@ "@types/node": "*" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3358,6 +4663,18 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/package.json b/package.json index 758a98a..8516730 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,19 @@ "main": "index.js", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.9.1", "@types/cors": "^2.8.17", "@types/express-session": "^1.18.0", "@types/multer": "^1.4.11", "@types/node": "^22.1.0", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.8", + "eslint": "^9.9.1", + "globals": "^15.9.0", "prettier": "3.3.3", "ts-node": "^10.9.2", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "typescript-eslint": "^8.2.0" }, "scripts": { "build": "npx tsc --build", From b152d48f22152ab873739499b150cf4155542262 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:43:36 +0300 Subject: [PATCH 07/14] feat: linter script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8516730..44ec435 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "dev:watch-api": "nodemon --config nodemon-api.json", "dev:watch-jobs": "nodemon --config nodemon-jobs.json", "format:write": "prettier . --write", - "format:check": "prettier . --check" + "format:check": "prettier . --check", + "lint": "eslint --fix ." }, "dependencies": { "@types/express": "^4.17.17", From 21c3f8ee2f7d856437bbfa7647d0dc421d740f5b Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:44:12 +0300 Subject: [PATCH 08/14] Lint workflow --- .github/workflows/lint.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/lint.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..6a87cfe --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,12 @@ +name: Lint Workflow +run-name: ${{ github.actor }} running the job +on: [push] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: npm install + - name: Run ESLint + run: npm run lint \ No newline at end of file From 71358458955fdc8276b5d214fae78c0538408f1e Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:45:52 +0300 Subject: [PATCH 09/14] fix: formatting --- .github/ISSUE_TEMPLATE/bug_report.md | 14 +- .github/ISSUE_TEMPLATE/feature_request.md | 1 - .github/ISSUE_TEMPLATE/help.md | 1 - .github/ISSUE_TEMPLATE/task.md | 2 +- .github/pull_request_template.md | 2 +- .github/workflows/lint.yaml | 2 +- LICENSE.md | 2 +- README.md | 29 +- docker/docker-compose.dev.yml | 11 +- docker/docker-compose.playground.yml | 11 +- docker/docker-compose.yml | 11 +- docs/DeveloperGuide.md | 94 ++- eslint.config.mjs | 9 +- nodemon-api.json | 16 +- nodemon-jobs.json | 16 +- src/api/controllers/authController.ts | 89 +- src/api/controllers/themeController.ts | 236 +++--- src/api/controllers/userController.ts | 264 +++--- src/api/databases/redis.ts | 35 +- src/api/databases/sql/models/FavoriteTheme.ts | 2 +- .../sql/models/LinkedAuthProvider.ts | 65 +- src/api/databases/sql/models/Theme.ts | 81 +- src/api/databases/sql/models/ThemeJobQueue.ts | 69 +- src/api/databases/sql/models/ThemeVersion.ts | 69 +- src/api/databases/sql/models/User.ts | 88 +- .../databases/sql/models/UserRefreshToken.ts | 59 +- src/api/databases/sql/sql.ts | 49 +- src/api/index.ts | 106 ++- src/api/interfaces/GitHubRepoContent.ts | 32 +- src/api/interfaces/ThemeMetaData.ts | 16 +- src/api/interfaces/TokenResponse.ts | 12 +- src/api/interfaces/UserData.ts | 26 +- src/api/interfaces/UserProviderData.ts | 22 +- src/api/middleware/userSessionMiddleware.ts | 28 +- src/api/routes/authRoutes.ts | 4 +- src/api/routes/themeRoutes.ts | 61 +- src/api/routes/userRoutes.ts | 14 +- .../services/authentication/authentication.ts | 475 ++++++----- .../authentication/providers/github.ts | 244 +++--- src/api/services/authorization.ts | 8 +- src/api/services/cryptoService.ts | 55 +- src/api/services/github/pullRequest.ts | 114 +-- src/api/services/minioService.ts | 131 +-- src/api/swagger.js | 14 +- src/api/swagger/auth.js | 298 +++---- src/api/swagger/theme.js | 704 ++++++++-------- src/api/swagger/user.js | 791 +++++++++--------- src/jobs/index.ts | 2 +- src/jobs/processQueuedThemes.ts | 50 +- src/jobs/syncThemesFromGitHub.ts | 172 ++-- tsconfig.json | 22 +- types/session.d.ts | 22 +- 52 files changed, 2429 insertions(+), 2321 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7b9a2d8..9d02ab8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Report bug(s) encountered with the gallery api! title: "[Bug]" labels: bug assignees: tjtanjin - --- **Bug Description:** @@ -12,6 +11,7 @@ Provide a clear and concise description of the bug. **Steps To Reproduce:** Steps to reproduce the bug behavior: + 1. Run the command '...' 2. Visit the API url '...' 3. See error @@ -23,13 +23,15 @@ Describe the behavior that is expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser: [e.g. chrome, safari] + +- OS: [e.g. iOS] +- Browser: [e.g. chrome, safari] **Mobile (please complete the following information):** - - Device: [e.g. iPhone12] - - OS: [e.g. iOS8.1] - - Browser: [e.g. edge, firefox] + +- Device: [e.g. iPhone12] +- OS: [e.g. iOS8.1] +- Browser: [e.g. edge, firefox] **Additional Context:** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 21a9d73..f1e10ed 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea to improve the gallery api! title: "[Feat]" labels: enhancement assignees: tjtanjin - --- **Is Your Feature Request Related to a Problem? Please describe:** diff --git a/.github/ISSUE_TEMPLATE/help.md b/.github/ISSUE_TEMPLATE/help.md index 0880541..e657bab 100644 --- a/.github/ISSUE_TEMPLATE/help.md +++ b/.github/ISSUE_TEMPLATE/help.md @@ -4,7 +4,6 @@ about: Seek help with using the gallery api! title: "[Help]" labels: help wanted assignees: tjtanjin - --- **Help Description:** diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md index 3b3390a..98f9f26 100644 --- a/.github/ISSUE_TEMPLATE/task.md +++ b/.github/ISSUE_TEMPLATE/task.md @@ -2,7 +2,6 @@ name: Task about: Create a task to track work to be done for the gallery api (internal use)! title: "[Task]" - --- **Task Description:** @@ -15,6 +14,7 @@ State the expected deliverable(s) for this task (e.g. lint.yml file, configured Add any other context about the task here. **Reminders:** + - Assign task to a project (required) - Assign task to a sprint (required) - Assign task to a developer (optional) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index efd4c49..d28d99b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,4 +23,4 @@ Please give a short overview/explanation on the approach taken to resolve the is - [ ] The commit message follows our adopted [guidelines](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] Testing has been done for the change(s) added (for bug fixes/features) -- [ ] Relevant comments/docs have been added/updated (for bug fixes/features) \ No newline at end of file +- [ ] Relevant comments/docs have been added/updated (for bug fixes/features) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 6a87cfe..d7f3eef 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -9,4 +9,4 @@ jobs: - name: Install run: npm install - name: Run ESLint - run: npm run lint \ No newline at end of file + run: npm run lint diff --git a/LICENSE.md b/LICENSE.md index c8dddbe..162b937 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 4e9a925..80c2969 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,16 @@

## Table of Contents -* [Introduction](#introduction) -* [Features](#features) -* [Technologies](#technologies) -* [Quickstart](#quickstart) -* [Documentation](#documentation) -* [Team](#team) -* [Contributing](#contributing) -* [Support](#support) -* [Attributions](#attributions) + +- [Introduction](#introduction) +- [Features](#features) +- [Technologies](#technologies) +- [Quickstart](#quickstart) +- [Documentation](#documentation) +- [Team](#team) +- [Contributing](#contributing) +- [Support](#support) +- [Attributions](#attributions) ### Introduction @@ -27,7 +28,9 @@ Apart from a handful of background jobs, the Gallery API largely supports reques For more information on what this project delivers, you may wish to check out the implementation section of the [**Developer Guide**](https://github.com/tjtanjin/react-chatbotify-gallery-api/blob/main/docs/DeveloperGuide.md). ### Technologies + Technologies used by React ChatBotify Gallery API are as below: + #### Done with:

@@ -44,14 +47,17 @@ Typescript

#### Project Repository + - https://github.com/tjtanjin/react-chatbotify-gallery-api ### Team -* [Tan Jin](https://github.com/tjtanjin) + +- [Tan Jin](https://github.com/tjtanjin) // todo: the team will be expanded once members are confirmed ### Contributing + If you are looking to contribute to the project, you may find the [**Developer Guide**](https://github.com/tjtanjin/react-chatbotify-gallery-api/blob/main/docs/DeveloperGuide.md) useful. In general, the forking workflow is encouraged and you may open a pull request with clear descriptions on the changes and what they are intended to do (enhancement, bug fixes etc). Alternatively, you may simply raise bugs or suggestions by opening an [**issue**](https://github.com/tjtanjin/react-chatbotify-gallery-api/issues) or raising it up on [**discord**](https://discord.gg/6R4DK4G5Zh). @@ -59,4 +65,5 @@ In general, the forking workflow is encouraged and you may open a pull request w Note: Templates have been created for pull requests and issues to guide you in the process. ### Support -If there are any questions pertaining to the application itself (usage or implementation wise), you may create an [**issue**](https://github.com/tjtanjin/react-chatbotify-gallery-api/issues), raise it up on [**discord**](https://discord.gg/6R4DK4G5Zh), or drop me an email at: **cjtanjin@gmail.com.** \ No newline at end of file + +If there are any questions pertaining to the application itself (usage or implementation wise), you may create an [**issue**](https://github.com/tjtanjin/react-chatbotify-gallery-api/issues), raise it up on [**discord**](https://discord.gg/6R4DK4G5Zh), or drop me an email at: **cjtanjin@gmail.com.** diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index d500a68..9e9ac21 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -92,7 +92,7 @@ services: volumes: - redis-sessions-data-dev:/data - ./config/redis/redis.dev.conf:/usr/local/etc/redis/redis.conf:ro - command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ] + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] healthcheck: test: "exit 0" @@ -131,13 +131,7 @@ services: networks: - core-network-dev healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "http://localhost:9000/minio/health/live" - ] + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 @@ -162,7 +156,6 @@ volumes: minio-data-dev: redis-sessions-data-dev: - networks: nginx-network-dev: driver: bridge diff --git a/docker/docker-compose.playground.yml b/docker/docker-compose.playground.yml index 4e8230c..80d5afc 100644 --- a/docker/docker-compose.playground.yml +++ b/docker/docker-compose.playground.yml @@ -76,7 +76,7 @@ services: volumes: - redis-sessions-data-playground:/data - ./config/redis/redis.playground.conf:/usr/local/etc/redis/redis.conf:ro - command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ] + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] healthcheck: test: "exit 0" @@ -116,13 +116,7 @@ services: networks: - core-network-playground healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "http://localhost:9000/minio/health/live" - ] + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 @@ -147,7 +141,6 @@ volumes: minio-data-playground: redis-sessions-data-playground: - networks: nginx-network-playground: driver: bridge diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 12f6095..d796cf9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -75,7 +75,7 @@ services: volumes: - redis-sessions-data-prod:/data - ./config/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro - command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ] + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] healthcheck: test: "exit 0" @@ -115,13 +115,7 @@ services: networks: - core-network-prod healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "http://localhost:9000/minio/health/live" - ] + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 @@ -146,7 +140,6 @@ volumes: minio-data-prod: redis-sessions-data-prod: - networks: nginx-network-prod: driver: bridge diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index e27c30a..20ea606 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -5,20 +5,21 @@ ## Table of Contents -* [Introduction](#introduction) -* [Navigating this Developer Guide](#navigating-this-developer-guide) -* [Design](#design) -* [Implementation](#implementation) -* [Project Management](#project-management) -* [Code Documentation](#code-documentation) -* [Testing](#testing) -* [Final Notes](#final-notes) +- [Introduction](#introduction) +- [Navigating this Developer Guide](#navigating-this-developer-guide) +- [Design](#design) +- [Implementation](#implementation) +- [Project Management](#project-management) +- [Code Documentation](#code-documentation) +- [Testing](#testing) +- [Final Notes](#final-notes)
## Introduction -Welcome to the Developer Guide for the React Chatbotify Gallery API project. Before diving into this guide, ensure you have gone through the project [*README*](https://github.com/your-repo/react-chatbotify-gallery-website/blob/main/README.md) for an overview. This guide assumes you have a **basic understanding** of the following tools & technologies (or are **at least able to read up and learn about them**): +Welcome to the Developer Guide for the React Chatbotify Gallery API project. Before diving into this guide, ensure you have gone through the project [_README_](https://github.com/your-repo/react-chatbotify-gallery-website/blob/main/README.md) for an overview. This guide assumes you have a **basic understanding** of the following tools & technologies (or are **at least able to read up and learn about them**): + - [**NodeJS**](https://nodejs.org/en) - [**ExpressJS**](https://expressjs.com/) - [**TypeScript**](https://www.typescriptlang.org/) @@ -36,11 +37,11 @@ In addition, you should also have a brief familiarity with [**React ChatBotify** To facilitate your reading, take note of the following syntaxes used throughout this guide: -| Syntax | Description | -|--------------|-----------------------------------------------------------------------------------------------| -| `Code` | Denotes functions, components, or code-related references (e.g., `App`, `useEffect`) | -| *Italics* | Refers to folder or file names (e.g., *App.js*, *components*) | -| **Bold** | Highlights important keywords or concepts | +| Syntax | Description | +| --------- | ------------------------------------------------------------------------------------ | +| `Code` | Denotes functions, components, or code-related references (e.g., `App`, `useEffect`) | +| _Italics_ | Refers to folder or file names (e.g., _App.js_, _components_) | +| **Bold** | Highlights important keywords or concepts |
@@ -49,30 +50,31 @@ To facilitate your reading, take note of the following syntaxes used throughout Setting up the project is relatively simple with [**Docker**](https://www.docker.com/). While it is technically feasible to setup the services of the project individually, it requires significantly more time and effort so you're **strongly discouraged** from doing so. The rest of this guide will assume that you have docker installed and have basic familiarity with [**Docker Compose**](https://docs.docker.com/compose/). To setup the project locally, follow the steps below: -1) Fork the [project repository](https://github.com/tjtanjin/react-chatbotify-gallery-api). -2) Clone the **forked project** into your desired directory with: - ``` - git clone {the-forked-project}.git - ``` -3) Next, `cd` into the project and run the following command: - ``` - npm run dev - ``` -4) The API server will be available on **http://localhost:3102**, and you may quickly verify that it is running by visiting the endpoint for fetching themes: http://localhost:3102/api/v1/themes?pageSize=30&pageNum=1 - -**Note:** For internal developers, you will be provided with a *.env.development* file which contains the variables for for the development environment. Notably, you'll be able to interact with the **GitHub Application** meant for development. The development environment is also setup to only **strictly** accept requests from a frontend served at **localhost:3000**. Thus, if you're keen to setup the frontend project, bear in mind to check the port number before calling the backend. For public contributors, you will have to populate the values in *.env.template* from scratch. If you require assistance with that however, feel free to **reach out**! + +1. Fork the [project repository](https://github.com/tjtanjin/react-chatbotify-gallery-api). +2. Clone the **forked project** into your desired directory with: + ``` + git clone {the-forked-project}.git + ``` +3. Next, `cd` into the project and run the following command: + ``` + npm run dev + ``` +4. The API server will be available on **http://localhost:3102**, and you may quickly verify that it is running by visiting the endpoint for fetching themes: http://localhost:3102/api/v1/themes?pageSize=30&pageNum=1 + +**Note:** For internal developers, you will be provided with a _.env.development_ file which contains the variables for for the development environment. Notably, you'll be able to interact with the **GitHub Application** meant for development. The development environment is also setup to only **strictly** accept requests from a frontend served at **localhost:3000**. Thus, if you're keen to setup the frontend project, bear in mind to check the port number before calling the backend. For public contributors, you will have to populate the values in _.env.template_ from scratch. If you require assistance with that however, feel free to **reach out**! ## Design ### Overview -At the root of the project, there are three key directories to be aware of: *config*, *docker*, and *src*. Other files and folders follow typical conventions for such projects and will not be covered in this guide. +At the root of the project, there are three key directories to be aware of: _config_, _docker_, and _src_. Other files and folders follow typical conventions for such projects and will not be covered in this guide. -The *config* directory, as the name implies, contains configuration files. Within this directory, there are three subfolders: *env*, *redis* and *nginx*. The *env* and *redis* subfolder holds environment-specific variables, while the *nginx* subfolder contains a shared NGINX configuration file. +The _config_ directory, as the name implies, contains configuration files. Within this directory, there are three subfolders: _env_, _redis_ and _nginx_. The _env_ and _redis_ subfolder holds environment-specific variables, while the _nginx_ subfolder contains a shared NGINX configuration file. -The *docker* directory includes all files related to Docker. Specifically, it contains Dockerfiles for the api and jobs services, along with Docker Compose files that orchestrate the entire setup. These files are also environment-specific. +The _docker_ directory includes all files related to Docker. Specifically, it contains Dockerfiles for the api and jobs services, along with Docker Compose files that orchestrate the entire setup. These files are also environment-specific. -Lastly, the *src* directory contains all of the application code. It is divided into two subdirectories: *api* and *jobs*, corresponding to the two custom services in the project (as indicated by the separate Dockerfiles). The internal structure of *api* and *jobs* is straightforward and follows common patterns. It is assumed that developers have the necessary expertise to navigate and understand the project structure independently. Therefore, we will focus on discussing the project architecture. +Lastly, the _src_ directory contains all of the application code. It is divided into two subdirectories: _api_ and _jobs_, corresponding to the two custom services in the project (as indicated by the separate Dockerfiles). The internal structure of _api_ and _jobs_ is straightforward and follows common patterns. It is assumed that developers have the necessary expertise to navigate and understand the project structure independently. Therefore, we will focus on discussing the project architecture. ### Architecture @@ -84,7 +86,7 @@ We will briefly describe each of these services below. #### Nginx -The NGINX service acts as the entry point for our backend services. Configurations for NGINX can be found in the *config/nginx* folder. NGINX functions as a load balancer, distributing incoming requests between two API instances (referred to as **api1** and **api2**). If one API instance fails to respond, NGINX will reroute the request to the other instance. +The NGINX service acts as the entry point for our backend services. Configurations for NGINX can be found in the _config/nginx_ folder. NGINX functions as a load balancer, distributing incoming requests between two API instances (referred to as **api1** and **api2**). If one API instance fails to respond, NGINX will reroute the request to the other instance. #### API @@ -92,13 +94,15 @@ The API service handles user requests and interacts with other services to perfo #### Redis -The Redis service is responsible for caching user sessions, user data, and encrypted access tokens. Configuration files for Redis can be found in the *config/redis* folder. There are two Redis instances in the project: +The Redis service is responsible for caching user sessions, user data, and encrypted access tokens. Configuration files for Redis can be found in the _config/redis_ folder. There are two Redis instances in the project: + - **redis-session:** Caches user sessions and is persistent, meaning data is retained across restarts. - **redis-ephemeral:** Caches user data and encrypted access tokens. Data is not retained across restarts, which is acceptable since the refresh token can be used to regenerate this information. #### MySQL The MySQL service functions as the primary database, storing essential user and theme-related data. The following tables are present: + - Users - UserRefreshTokens - Themes @@ -107,7 +111,7 @@ The MySQL service functions as the primary database, storing essential user and - FavoriteThemes - LinkedAuthProviders -The schema for these tables is located in *src/api/databases/sql/models*. Detailed explanations of these tables will be provided later in the guide as they pertain to specific implementations. +The schema for these tables is located in _src/api/databases/sql/models_. Detailed explanations of these tables will be provided later in the guide as they pertain to specific implementations. #### Minio @@ -116,6 +120,7 @@ The MinIO service serves as a temporary storage bucket for theme-related files. #### Jobs There are currently two background jobs: + - **Sync Themes From GitHub:** Runs every 24 hours to ensure the themes data in MySQL is synchronized with the data on GitHub. - **Process Queued Themes:** Runs every 15 minutes to handle the processing of themes queued for creation or deletion. @@ -128,6 +133,7 @@ On top of serving as the backend for the Gallery website, the Gallery API also p ### Nginx Proxies If you've read the **Design** section above, you will be aware that there is an nginx service that proxies requests to 2 API instances. The configurations for nginx are provided below: + ``` events {} @@ -148,7 +154,7 @@ http { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - + # forward the protocol forwarded by the host nginx # note that the gallery platform sits behind 2 proxies proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; @@ -156,6 +162,7 @@ http { } } ``` + Notice that if an API instance returns 500 error codes, nginx will attempt to make a request to the other API instance instead. This is so if an instance unexpectedly crashes, the other instance can continue to serve requests normally as remedy work is done on the other instance. Apart from that, there's another important configuration: `proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;`. This was previously misconfigured as `proxy_set_header X-Forwarded-Proto $scheme;` and took over a day to debug. The reason why `$scheme` **will not work** is because there is actually another nginx server on the host machine as well. The host nginx actually does a proxy pass to the docker nginx with **http** and thus, if you use `$scheme`, it will be **http** instead. @@ -172,12 +179,13 @@ Every 3 months, a session expires and users will be required to login again. Use The application **does not store username or passwords**. Instead, it relies fully on third-party OAuth providers for authenticating users. This allows us to avoid the hassle of having to deal with storage of user passwords, and we can solely focus on delivering the core features of the application. -To flexibly support multiple providers, a set of common fields are defined within *src/api/interfaces/UserProviderData*. Each provider also implements the following set of functions which can be found in their respective files: +To flexibly support multiple providers, a set of common fields are defined within _src/api/interfaces/UserProviderData_. Each provider also implements the following set of functions which can be found in their respective files: + - getUserData, - getUserTokensWithCode, - getUserTokensWithRefresh -Thus, to add additional providers, it is as simple as creating a new file and implementing these functions before introducing them into *src/api/services/authentication*. +Thus, to add additional providers, it is as simple as creating a new file and implementing these functions before introducing them into _src/api/services/authentication_. ### Routes & Endpoints @@ -220,16 +228,19 @@ The progress of the project is tracked using [**GitHub Projects**](https://githu If you are looking to contribute, you are strongly encouraged to take up **good-first-issues** if it is your first time working on the project. If you're not part of the project team but feel confident in taking up issues prefixed with [Task], still feel free to comment and indicate your interest. ### Forking Workflow + This project adopts the [**Forking Workflow**](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow). In short, here are the steps required: -1) Fork the repository -2) Clone the forked repository to your local device -3) Make your code changes -4) Push to your forked remote repository -5) Open a pull request from your forked repository to the upstream repository (i.e. the main repository) + +1. Fork the repository +2. Clone the forked repository to your local device +3. Make your code changes +4. Push to your forked remote repository +5. Open a pull request from your forked repository to the upstream repository (i.e. the main repository) In addition, developers should fill up the pull requests template diligently. This ensures that changes are well-documented and reviewed before merging. ### Commit Messages + This project adopts [**Conventional Commits**](https://www.conventionalcommits.org/en/v1.0.0/), with a minor difference that **the first word after the commit type is always capitalised**. For example, notice how "A" in "Add" is capitalised in this commit message: `feat: Add initial theme builder layout`. ## Code Documentation @@ -248,6 +259,7 @@ const getUserTokensWithRefresh = async (refreshToken: string) => { // Implementation... } ``` + The above shows an example code comment for a function that fetches new user tokens with the refresh token. Finally, any leftover tasks or areas in the code to be revisited should be flagged with a comment like the one below: diff --git a/eslint.config.mjs b/eslint.config.mjs index 8cbf6eb..f75ba7b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,11 +2,10 @@ import globals from "globals"; import pluginJs from "@eslint/js"; import tseslint from "typescript-eslint"; - export default [ - {files: ["**/*.{js,mjs,cjs,ts}"]}, - {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, - {languageOptions: { globals: globals.browser }}, + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { files: ["**/*.js"], languageOptions: { sourceType: "script" } }, + { languageOptions: { globals: globals.browser } }, pluginJs.configs.recommended, ...tseslint.configs.recommended, -]; \ No newline at end of file +]; diff --git a/nodemon-api.json b/nodemon-api.json index 88d98fc..4782a94 100644 --- a/nodemon-api.json +++ b/nodemon-api.json @@ -1,12 +1,6 @@ { - "watch": [ - "src", - "types", - "config" - ], - "ext": "ts,js,json", - "ignore": [ - "node_modules" - ], - "exec": "ts-node ./src/api/index.ts" -} \ No newline at end of file + "watch": ["src", "types", "config"], + "ext": "ts,js,json", + "ignore": ["node_modules"], + "exec": "ts-node ./src/api/index.ts" +} diff --git a/nodemon-jobs.json b/nodemon-jobs.json index 1b92bc3..f5d9a43 100644 --- a/nodemon-jobs.json +++ b/nodemon-jobs.json @@ -1,12 +1,6 @@ { - "watch": [ - "src", - "types", - "config" - ], - "ext": "ts,js,json", - "ignore": [ - "node_modules" - ], - "exec": "ts-node ./src/jobs/index.ts" -} \ No newline at end of file + "watch": ["src", "types", "config"], + "ext": "ts,js,json", + "ignore": ["node_modules"], + "exec": "ts-node ./src/jobs/index.ts" +} diff --git a/src/api/controllers/authController.ts b/src/api/controllers/authController.ts index de9a433..54dbb9a 100644 --- a/src/api/controllers/authController.ts +++ b/src/api/controllers/authController.ts @@ -1,68 +1,77 @@ import { Request, Response } from "express"; -import { fetchTokensWithCode, getUserData, saveUserTokens } from "../services/authentication/authentication"; +import { + fetchTokensWithCode, + getUserData, + saveUserTokens, +} from "../services/authentication/authentication"; import { encrypt } from "../services/cryptoService"; /** * Handles the callback when a user authorizes or rejects the application. - * + * * @param req request from call * @param res response to call - * + * * @returns redirects user to login process page on frontend on success, error page otherwise */ const handleCallback = async (req: Request, res: Response) => { - // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team - if (req.query.error === "access_denied") { - return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); - } - try { - const key = encrypt(req.query.code as string); - res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/login/process?provider=${process.env.GITHUB_LOGIN_PROVIDER}&key=${key}`); - } catch { - // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team - return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); - } + // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team + if (req.query.error === "access_denied") { + return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); + } + try { + const key = encrypt(req.query.code as string); + res.redirect( + `${process.env.FRONTEND_WEBSITE_URL}/login/process?provider=${process.env.GITHUB_LOGIN_PROVIDER}&key=${key}`, + ); + } catch { + // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team + return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); + } }; /** * Handles login processing by first using the auth code to retrieve the access and refresh token. * Following which, use the access token to fetch user data. Tokens and user data are both stored * in a redis cache for ease of retrieval - * + * * @param req request from call * @param res response to call - * + * * @returns user data on success, 401 unauthorized otherwise */ const handleLoginProcess = async (req: Request, res: Response) => { - const sessionId = req.sessionID; - const provider = req.query.provider as string; + const sessionId = req.sessionID; + const provider = req.query.provider as string; - // if no provider specified, unable to login - if (!provider) { - return res.status(401).json({ error: "No login provider found, please try again." }); - } + // if no provider specified, unable to login + if (!provider) { + return res + .status(401) + .json({ error: "No login provider found, please try again." }); + } - // if unable to fetch user tokens, get user to login again - const tokenResponse = await fetchTokensWithCode(sessionId, req.query.key as string, provider); - if (!tokenResponse) { - return res.status(401).json({ error: "Login failed, please try again." }); - } + // if unable to fetch user tokens, get user to login again + const tokenResponse = await fetchTokensWithCode( + sessionId, + req.query.key as string, + provider, + ); + if (!tokenResponse) { + return res.status(401).json({ error: "Login failed, please try again." }); + } - // get user data (will create user if new) - const userData = await getUserData(sessionId, null, provider); - if (!userData) { - return res.status(401).json({ error: "Login failed, please try again." }); - } + // get user data (will create user if new) + const userData = await getUserData(sessionId, null, provider); + if (!userData) { + return res.status(401).json({ error: "Login failed, please try again." }); + } - req.session.provider = provider; - req.session.userId = userData.id; - saveUserTokens(sessionId, userData.id, tokenResponse); - res.json(userData); + req.session.provider = provider; + req.session.userId = userData.id; + saveUserTokens(sessionId, userData.id, tokenResponse); + res.json(userData); }; -export { - handleCallback, - handleLoginProcess -}; +export { handleCallback, handleLoginProcess }; diff --git a/src/api/controllers/themeController.ts b/src/api/controllers/themeController.ts index 60b3123..725f607 100644 --- a/src/api/controllers/themeController.ts +++ b/src/api/controllers/themeController.ts @@ -14,32 +14,34 @@ import { checkIsAdminUser } from "../services/authorization"; * @returns list of themes on success, 500 error otherwise */ const getThemes = async (req: Request, res: Response) => { - // default to returning 30 themes per page - // default to returning only first page if not specified - // default to no searches - const { pageSize = 30, pageNum = 1, searchQuery = "" } = req.query; - - // construct clause for searching themes - const limit = parseInt(pageSize as string, 30); - const offset = (parseInt(pageNum as string, 30) - 1) * limit; - const whereClause = searchQuery ? { - [Op.or]: [ - { name: { [Op.like]: `%${searchQuery}%` } }, - { description: { [Op.like]: `%${searchQuery}%` } } - ] - } : {}; - - // fetch themes according to page size, page num and search query - try { - const themes = await Theme.findAll({ - where: whereClause, - limit, - offset - }); - res.json(themes); - } catch (error) { - res.status(500).json({ error: "Failed to fetch themes" }); - } + // default to returning 30 themes per page + // default to returning only first page if not specified + // default to no searches + const { pageSize = 30, pageNum = 1, searchQuery = "" } = req.query; + + // construct clause for searching themes + const limit = parseInt(pageSize as string, 30); + const offset = (parseInt(pageNum as string, 30) - 1) * limit; + const whereClause = searchQuery + ? { + [Op.or]: [ + { name: { [Op.like]: `%${searchQuery}%` } }, + { description: { [Op.like]: `%${searchQuery}%` } }, + ], + } + : {}; + + // fetch themes according to page size, page num and search query + try { + const themes = await Theme.findAll({ + where: whereClause, + limit, + offset, + }); + res.json(themes); + } catch (error) { + res.status(500).json({ error: "Failed to fetch themes" }); + } }; /** @@ -51,59 +53,63 @@ const getThemes = async (req: Request, res: Response) => { * @returns list of theme versions on success, 500 error otherwise */ const getThemeVersions = async (req: Request, res: Response) => { - try { - const versions = await ThemeVersion.findAll({ - where: { theme_id: req.query.themeId } - }); - - res.json(versions); - } catch (error) { - console.error("Error fetching theme versions:", error); - res.status(500).json({ error: "Failed to fetch theme versions" }); - } + try { + const versions = await ThemeVersion.findAll({ + where: { theme_id: req.query.themeId }, + }); + + res.json(versions); + } catch (error) { + console.error("Error fetching theme versions:", error); + res.status(500).json({ error: "Failed to fetch theme versions" }); + } }; /** * Publishes a new theme (including version bumps). - * + * * @param req request from call * @param res response to call * * @returns 201 on success, 500 otherwise */ const publishTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id, name, description, version } = req.body; - - // todo: perform checks in the following steps: - // 1) if theme_id already exist and user is not author, 403 - // 2) if theme_id already exist and user is author but version already exist, 400 - // 3) if theme_id does not exist or user is author of theme but has no existing version, continue below - // 4) rigorously validate file inputs (styles.json, styles.css, settings.json) - // 5) if fail checks, immediately return and don't do any further queuing or processing - // 6) provide verbose reasons for frontend to render to user - const validationPassed = true; - if (!validationPassed) { - return res.status(400).json({ error: "Failed to publish theme, validation failed." }); - } - - // add the new creation to theme job queue for processing later - try { - await ThemeJobQueue.create({ - user_id: userData.id, - theme_id, - name, - description, - action: "CREATE" - }); - - // todo: push files into minio bucket with theme_id for process queue job to pick up - - res.status(201); - } catch (error) { - console.error("Error publishing theme:", error); - res.status(500).json({ error: "Failed to publish theme, please try again." }); - } + const userData = req.userData; + const { theme_id, name, description, version } = req.body; + + // todo: perform checks in the following steps: + // 1) if theme_id already exist and user is not author, 403 + // 2) if theme_id already exist and user is author but version already exist, 400 + // 3) if theme_id does not exist or user is author of theme but has no existing version, continue below + // 4) rigorously validate file inputs (styles.json, styles.css, settings.json) + // 5) if fail checks, immediately return and don't do any further queuing or processing + // 6) provide verbose reasons for frontend to render to user + const validationPassed = true; + if (!validationPassed) { + return res + .status(400) + .json({ error: "Failed to publish theme, validation failed." }); + } + + // add the new creation to theme job queue for processing later + try { + await ThemeJobQueue.create({ + user_id: userData.id, + theme_id, + name, + description, + action: "CREATE", + }); + + // todo: push files into minio bucket with theme_id for process queue job to pick up + + res.status(201); + } catch (error) { + console.error("Error publishing theme:", error); + res + .status(500) + .json({ error: "Failed to publish theme, please try again." }); + } }; /** @@ -115,51 +121,51 @@ const publishTheme = async (req: Request, res: Response) => { * @returns 200 on success, 500 otherwise */ const unpublishTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id } = req.params; - - // check if the theme exists and is owned by the user - try { - const theme = await Theme.findOne({ - where: { - id: theme_id, - } - }); - - // if theme does not exist, cannot delete - if (!theme) { - return res.status(404).json({ error: "Failed to unpublish theme, the theme does not exist." }); - } - - // if theme exist and user is admin, can delete - const isAdminUser = checkIsAdminUser(userData); - if (isAdminUser) { - // todo: allow admins to forcibly unpublish themes - } - - // todo: review how to handle unpublishing of themes, authors should not be allowed to delete themes anytime - // as there may be existing projects using their themes - perhaps separately have a support system for such action - return res.status(400).json({ error: "Feature not allowed." }); - - // if theme exist but user is not the theme author, cannot delete - // if (theme.dataValues.user_id != req.session.userId) { - // return res.status(403).json({ error: "Failed to unpublish theme, you are not the theme author." }); - // } - - // delete the theme - // await theme.destroy(); - - // res.status(200); - } catch (error) { - console.error("Error unpublishing theme:", error); - res.status(500).json({ error: "Failed to unpublish theme, please try again." }); - } -}; - -export { - getThemes, - getThemeVersions, - publishTheme, - unpublishTheme + const userData = req.userData; + const { theme_id } = req.params; + + // check if the theme exists and is owned by the user + try { + const theme = await Theme.findOne({ + where: { + id: theme_id, + }, + }); + + // if theme does not exist, cannot delete + if (!theme) { + return res + .status(404) + .json({ + error: "Failed to unpublish theme, the theme does not exist.", + }); + } + + // if theme exist and user is admin, can delete + const isAdminUser = checkIsAdminUser(userData); + if (isAdminUser) { + // todo: allow admins to forcibly unpublish themes + } + + // todo: review how to handle unpublishing of themes, authors should not be allowed to delete themes anytime + // as there may be existing projects using their themes - perhaps separately have a support system for such action + return res.status(400).json({ error: "Feature not allowed." }); + + // if theme exist but user is not the theme author, cannot delete + // if (theme.dataValues.user_id != req.session.userId) { + // return res.status(403).json({ error: "Failed to unpublish theme, you are not the theme author." }); + // } + + // delete the theme + // await theme.destroy(); + + // res.status(200); + } catch (error) { + console.error("Error unpublishing theme:", error); + res + .status(500) + .json({ error: "Failed to unpublish theme, please try again." }); + } }; +export { getThemes, getThemeVersions, publishTheme, unpublishTheme }; diff --git a/src/api/controllers/userController.ts b/src/api/controllers/userController.ts index db4dc9a..2351fa4 100644 --- a/src/api/controllers/userController.ts +++ b/src/api/controllers/userController.ts @@ -13,17 +13,21 @@ import { checkIsAdminUser } from "../services/authorization"; * @returns user data if successful, 403 otherwise */ const getUserProfile = async (req: Request, res: Response) => { - const userData = req.userData; - const queryUserId = req.query.userId as string; - const sessionUserId = req.session.userId; - - // if user id matches or user is admin, can retrieve user data - if (!queryUserId || queryUserId === sessionUserId || checkIsAdminUser(userData)) { - return res.json(userData); - } - - // all other cases unauthorized - return res.status(403).json({ error: "Unauthorized access" }); + const userData = req.userData; + const queryUserId = req.query.userId as string; + const sessionUserId = req.session.userId; + + // if user id matches or user is admin, can retrieve user data + if ( + !queryUserId || + queryUserId === sessionUserId || + checkIsAdminUser(userData) + ) { + return res.json(userData); + } + + // all other cases unauthorized + return res.status(403).json({ error: "Unauthorized access" }); }; /** @@ -35,25 +39,28 @@ const getUserProfile = async (req: Request, res: Response) => { * @returns list of user's themes if successful, 403 otherwise */ const getUserThemes = async (req: Request, res: Response) => { - const userData = req.userData; - const queryUserId = req.query.userId as string; - const sessionUserId = req.session.userId; - - // if user id matches or user is admin, can retrieve user themes - if (!queryUserId || queryUserId === sessionUserId || checkIsAdminUser(userData)) { - try { - const themes = await Theme.findAll({ - where: { - user_id: userData.id - } - }); - return res.json(themes); - } catch { - } - } - - // all other cases unauthorized - return res.status(403).json({ error: "Unauthorized access" }); + const userData = req.userData; + const queryUserId = req.query.userId as string; + const sessionUserId = req.session.userId; + + // if user id matches or user is admin, can retrieve user themes + if ( + !queryUserId || + queryUserId === sessionUserId || + checkIsAdminUser(userData) + ) { + try { + const themes = await Theme.findAll({ + where: { + user_id: userData.id, + }, + }); + return res.json(themes); + } catch {} + } + + // all other cases unauthorized + return res.status(403).json({ error: "Unauthorized access" }); }; /** @@ -65,26 +72,29 @@ const getUserThemes = async (req: Request, res: Response) => { * @returns list of user's favorited themes if successful, 403 otherwise */ const getUserFavoriteThemes = async (req: Request, res: Response) => { - const userData = req.userData; - const queryUserId = req.query.userId as string; - const sessionUserId = req.session.userId; - - // if user id matches or user is admin, can retrieve user favorited themes - if (!queryUserId || queryUserId === sessionUserId || checkIsAdminUser(userData)) { - try { - const userFavoriteThemes = await FavoriteTheme.findAll({ - where: { - user_id: userData.id - }, - include: [Theme] - }); - res.json(userFavoriteThemes); - } catch { - } - } - - // all other cases unauthorized - return res.status(403).json({ error: "Unauthorized access" }); + const userData = req.userData; + const queryUserId = req.query.userId as string; + const sessionUserId = req.session.userId; + + // if user id matches or user is admin, can retrieve user favorited themes + if ( + !queryUserId || + queryUserId === sessionUserId || + checkIsAdminUser(userData) + ) { + try { + const userFavoriteThemes = await FavoriteTheme.findAll({ + where: { + user_id: userData.id, + }, + include: [Theme], + }); + res.json(userFavoriteThemes); + } catch {} + } + + // all other cases unauthorized + return res.status(403).json({ error: "Unauthorized access" }); }; /** @@ -96,45 +106,48 @@ const getUserFavoriteThemes = async (req: Request, res: Response) => { * @returns 201 if successful, 404 if theme not found, 400 if already favorited, 500 otherwise */ const addUserFavoriteTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id } = req.body; - - try { - await sequelize.transaction(async (transaction) => { - // check if the theme exists - const theme = await Theme.findByPk(theme_id, { transaction }); - if (!theme) { - return res.status(404).json({ error: "Theme not found." }); - } - - // check if theme already favorited - const existingFavorite = await FavoriteTheme.findOne({ - where: { - user_id: userData.id, - id: theme_id - }, - transaction - }); - - if (existingFavorite) { - return res.status(400).json({ error: "Theme already favorited." }); - } - - // add favorite theme - await FavoriteTheme.create({ - user_id: userData.id, - id: theme_id - }, { transaction }); - - // increment the favorites count in the theme table - await theme.increment("favorites_count", { by: 1, transaction }); - }); - - res.status(201); - } catch (error) { - console.error("Error adding favorite theme:", error); - res.status(500).json({ error: "Failed to add favorite theme." }); - } + const userData = req.userData; + const { theme_id } = req.body; + + try { + await sequelize.transaction(async (transaction) => { + // check if the theme exists + const theme = await Theme.findByPk(theme_id, { transaction }); + if (!theme) { + return res.status(404).json({ error: "Theme not found." }); + } + + // check if theme already favorited + const existingFavorite = await FavoriteTheme.findOne({ + where: { + user_id: userData.id, + id: theme_id, + }, + transaction, + }); + + if (existingFavorite) { + return res.status(400).json({ error: "Theme already favorited." }); + } + + // add favorite theme + await FavoriteTheme.create( + { + user_id: userData.id, + id: theme_id, + }, + { transaction }, + ); + + // increment the favorites count in the theme table + await theme.increment("favorites_count", { by: 1, transaction }); + }); + + res.status(201); + } catch (error) { + console.error("Error adding favorite theme:", error); + res.status(500).json({ error: "Failed to add favorite theme." }); + } }; /** @@ -146,42 +159,45 @@ const addUserFavoriteTheme = async (req: Request, res: Response) => { * @returns 200 if successful, 404 if theme not found, 500 otherwise */ const removeUserFavoriteTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id } = req.params; - - try { - await sequelize.transaction(async (transaction) => { - // check if theme is favorited - const existingFavorite = await FavoriteTheme.findOne({ - where: { - user_id: userData.id, - id: theme_id - }, - transaction - }); - - if (!existingFavorite) { - return res.status(404).json({ error: "Favorite theme not found" }); - } - - // remove favorite theme - await existingFavorite.destroy({ transaction }); - - // decrement the favorites count in the theme table - const theme = await Theme.findByPk(theme_id, { transaction }); - if (theme) { - await theme.decrement("favorites_count", { by: 1, transaction }); - } - }); - - res.status(200); - } catch (error) { - console.error("Error removing favorite theme:", error); - res.status(500).json({ error: "Failed to remove favorite theme" }); - } + const userData = req.userData; + const { theme_id } = req.params; + + try { + await sequelize.transaction(async (transaction) => { + // check if theme is favorited + const existingFavorite = await FavoriteTheme.findOne({ + where: { + user_id: userData.id, + id: theme_id, + }, + transaction, + }); + + if (!existingFavorite) { + return res.status(404).json({ error: "Favorite theme not found" }); + } + + // remove favorite theme + await existingFavorite.destroy({ transaction }); + + // decrement the favorites count in the theme table + const theme = await Theme.findByPk(theme_id, { transaction }); + if (theme) { + await theme.decrement("favorites_count", { by: 1, transaction }); + } + }); + + res.status(200); + } catch (error) { + console.error("Error removing favorite theme:", error); + res.status(500).json({ error: "Failed to remove favorite theme" }); + } }; export { - addUserFavoriteTheme, getUserFavoriteThemes, getUserProfile, - getUserThemes, removeUserFavoriteTheme + addUserFavoriteTheme, + getUserFavoriteThemes, + getUserProfile, + getUserThemes, + removeUserFavoriteTheme, }; diff --git a/src/api/databases/redis.ts b/src/api/databases/redis.ts index 4624df0..ea14709 100644 --- a/src/api/databases/redis.ts +++ b/src/api/databases/redis.ts @@ -3,31 +3,28 @@ import { createClient } from "redis"; // initialize redis session client const redisSessionClient = createClient({ - socket: { - host: "redis-sessions", - port: 6379, - // todo: protect with passphrase? - } -}) -redisSessionClient.connect().catch(console.error) + socket: { + host: "redis-sessions", + port: 6379, + // todo: protect with passphrase? + }, +}); +redisSessionClient.connect().catch(console.error); const redisSessionStore = new RedisStore({ - client: redisSessionClient, - // matches express cookie expiry duration (redis store specifies ttl in seconds) - ttl: 7776000 + client: redisSessionClient, + // matches express cookie expiry duration (redis store specifies ttl in seconds) + ttl: 7776000, }); // initialize redis ephemeral client const redisEphemeralClient = createClient({ - socket: { - host: "redis-ephemeral", - port: 6379, - // todo: protect with passphrase? - } + socket: { + host: "redis-ephemeral", + port: 6379, + // todo: protect with passphrase? + }, }); redisEphemeralClient.connect().catch(console.error); -export { - redisEphemeralClient, - redisSessionStore -}; +export { redisEphemeralClient, redisSessionStore }; diff --git a/src/api/databases/sql/models/FavoriteTheme.ts b/src/api/databases/sql/models/FavoriteTheme.ts index 520eea7..8726b3b 100644 --- a/src/api/databases/sql/models/FavoriteTheme.ts +++ b/src/api/databases/sql/models/FavoriteTheme.ts @@ -6,7 +6,7 @@ import User from "./User"; /** * Association table between a user and a theme (user favorite theme). */ -class FavoriteTheme extends Model { } +class FavoriteTheme extends Model {} FavoriteTheme.init({}, { sequelize, modelName: "FavoriteTheme" }); diff --git a/src/api/databases/sql/models/LinkedAuthProvider.ts b/src/api/databases/sql/models/LinkedAuthProvider.ts index 2b2ccef..766fe1d 100644 --- a/src/api/databases/sql/models/LinkedAuthProvider.ts +++ b/src/api/databases/sql/models/LinkedAuthProvider.ts @@ -5,37 +5,40 @@ import User from "./User"; /** * Stores the login providers associated with a user. */ -class LinkedAuthProvider extends Model { } +class LinkedAuthProvider extends Model {} -LinkedAuthProvider.init({ - // user id from the login provider (e.g. github user id) - provider_user_id: { - type: DataTypes.STRING, - primaryKey: true - }, - // user id for the user created in this application - user_id: { - type: DataTypes.UUID, - allowNull: false, - references: { - model: User, - key: "id" - } - }, - // name of the login provider - provider: { - type: DataTypes.STRING, - allowNull: false - }, - // date when the link was done - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - }, -}, { - sequelize, - modelName: "LinkedAuthProvider", - timestamps: false -}); +LinkedAuthProvider.init( + { + // user id from the login provider (e.g. github user id) + provider_user_id: { + type: DataTypes.STRING, + primaryKey: true, + }, + // user id for the user created in this application + user_id: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: User, + key: "id", + }, + }, + // name of the login provider + provider: { + type: DataTypes.STRING, + allowNull: false, + }, + // date when the link was done + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "LinkedAuthProvider", + timestamps: false, + }, +); export default LinkedAuthProvider; diff --git a/src/api/databases/sql/models/Theme.ts b/src/api/databases/sql/models/Theme.ts index eaf1d94..98e5795 100644 --- a/src/api/databases/sql/models/Theme.ts +++ b/src/api/databases/sql/models/Theme.ts @@ -5,48 +5,51 @@ import User from "./User"; /** * Stores the data of themes. */ -class Theme extends Model { } +class Theme extends Model {} -Theme.init({ - // unique identifier for the theme, matches github themes folder name (e.g. minimal_midnight) - id: { - type: DataTypes.STRING, - primaryKey: true - }, - // name of the theme, a more human-readable friendly identifier but may not be unique - name: { - type: DataTypes.STRING, - allowNull: false - }, - // a brief description of the theme - description: { - type: DataTypes.TEXT, - allowNull: true - }, - // number of favorites given to the theme - favorites_count: { - type: DataTypes.INTEGER, - defaultValue: 0 - }, - // date when the theme is created, based on when it was synced in from github - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - }, - // todo: currently this field isn"t updated because version isn"t integrated yet - // date when the theme was last updated, based on when sync detects a version upgrade - updated_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "Theme", - timestamps: false -}); +Theme.init( + { + // unique identifier for the theme, matches github themes folder name (e.g. minimal_midnight) + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + // name of the theme, a more human-readable friendly identifier but may not be unique + name: { + type: DataTypes.STRING, + allowNull: false, + }, + // a brief description of the theme + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + // number of favorites given to the theme + favorites_count: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + // date when the theme is created, based on when it was synced in from github + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + // todo: currently this field isn"t updated because version isn"t integrated yet + // date when the theme was last updated, based on when sync detects a version upgrade + updated_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "Theme", + timestamps: false, + }, +); // theme belongs to a user, but permitted to be empty (for direct theme contributions to github repository) // todo: perhaps the sync job csn attempt to reconcile theme ownership each time it is run based on meta.json author? Theme.belongsTo(User, { foreignKey: "user_id" }); -export default Theme; \ No newline at end of file +export default Theme; diff --git a/src/api/databases/sql/models/ThemeJobQueue.ts b/src/api/databases/sql/models/ThemeJobQueue.ts index 68d7031..59fb7f5 100644 --- a/src/api/databases/sql/models/ThemeJobQueue.ts +++ b/src/api/databases/sql/models/ThemeJobQueue.ts @@ -4,39 +4,42 @@ import { sequelize } from "../sql"; /** * Serves as a job queue for the themes that need to be processed. */ -class ThemeJobQueue extends Model { } +class ThemeJobQueue extends Model {} -ThemeJobQueue.init({ - // id to uniquely identify the job - id: { - type: DataTypes.STRING, - primaryKey: true - }, - // name of the theme, used to generate meta.json and copied into the theme table - name: { - type: DataTypes.STRING, - allowNull: false - }, - // description of the theme, used to generate meta.json and copied into the theme table - description: { - type: DataTypes.TEXT, - allowNull: true - }, - // action for this job (create or delete theme) - action: { - type: DataTypes.ENUM, - values: ["CREATE", "DELETE"], - allowNull: false - }, - // date of job creation - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "ThemeJob", - timestamps: false -}); +ThemeJobQueue.init( + { + // id to uniquely identify the job + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + // name of the theme, used to generate meta.json and copied into the theme table + name: { + type: DataTypes.STRING, + allowNull: false, + }, + // description of the theme, used to generate meta.json and copied into the theme table + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + // action for this job (create or delete theme) + action: { + type: DataTypes.ENUM, + values: ["CREATE", "DELETE"], + allowNull: false, + }, + // date of job creation + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "ThemeJob", + timestamps: false, + }, +); export default ThemeJobQueue; diff --git a/src/api/databases/sql/models/ThemeVersion.ts b/src/api/databases/sql/models/ThemeVersion.ts index 99230a1..f583be1 100644 --- a/src/api/databases/sql/models/ThemeVersion.ts +++ b/src/api/databases/sql/models/ThemeVersion.ts @@ -6,40 +6,43 @@ import Theme from "./Theme"; * Represents a version of a theme in the application. * Tracks the theme id, version number, and release date. */ -class ThemeVersion extends Model { } +class ThemeVersion extends Model {} -ThemeVersion.init({ - // id to uniquely identify published theme version - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - // the identifier of the theme this version belongs to - theme_id: { - type: DataTypes.STRING, - allowNull: false, - references: { - model: Theme, - key: "id" - } - }, - // the version number of the theme - version: { - type: DataTypes.STRING, - allowNull: false - }, - // date when this version of the theme was released - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "ThemeVersion", - timestamps: false -}); +ThemeVersion.init( + { + // id to uniquely identify published theme version + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + // the identifier of the theme this version belongs to + theme_id: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: Theme, + key: "id", + }, + }, + // the version number of the theme + version: { + type: DataTypes.STRING, + allowNull: false, + }, + // date when this version of the theme was released + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "ThemeVersion", + timestamps: false, + }, +); ThemeVersion.belongsTo(Theme, { foreignKey: "theme_id" }); -export default ThemeVersion; \ No newline at end of file +export default ThemeVersion; diff --git a/src/api/databases/sql/models/User.ts b/src/api/databases/sql/models/User.ts index 34df25e..c826574 100644 --- a/src/api/databases/sql/models/User.ts +++ b/src/api/databases/sql/models/User.ts @@ -1,51 +1,53 @@ import { DataTypes, Model } from "sequelize"; import { sequelize } from "../sql"; - /** * Stores the data of users. */ -class User extends Model { } +class User extends Model {} -User.init({ - // id to uniquely identify the user - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - // email, also uniquely identifies the user - email: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - // the role of the user - role: { - type: DataTypes.ENUM("USER", "MODERATOR", "ADMIN"), - defaultValue: "USER", - allowNull: false - }, - // date when the user accepted the author agreement (necessary to publish themes/plugins) - accepted_author_agreement: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: null // default to null which is not agreed yet - }, - // date when user was created - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - }, - // date when user was last updated - updated_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "User", - timestamps: false -}); +User.init( + { + // id to uniquely identify the user + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + // email, also uniquely identifies the user + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + // the role of the user + role: { + type: DataTypes.ENUM("USER", "MODERATOR", "ADMIN"), + defaultValue: "USER", + allowNull: false, + }, + // date when the user accepted the author agreement (necessary to publish themes/plugins) + accepted_author_agreement: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: null, // default to null which is not agreed yet + }, + // date when user was created + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + // date when user was last updated + updated_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "User", + timestamps: false, + }, +); -export default User; \ No newline at end of file +export default User; diff --git a/src/api/databases/sql/models/UserRefreshToken.ts b/src/api/databases/sql/models/UserRefreshToken.ts index 13fa663..2e7b4d1 100644 --- a/src/api/databases/sql/models/UserRefreshToken.ts +++ b/src/api/databases/sql/models/UserRefreshToken.ts @@ -5,34 +5,37 @@ import User from "./User"; /** * Stores the refresh token for users. */ -class UserRefreshToken extends Model { } +class UserRefreshToken extends Model {} -UserRefreshToken.init({ - // user id to identify who the refresh token belongs to - user_id: { - type: DataTypes.UUID, - allowNull: false, - references: { - model: User, - key: "id" - }, - onDelete: "CASCADE", - primaryKey: true - }, - // actual refresh token - refresh_token: { - type: DataTypes.STRING, - allowNull: false - }, - // date when refresh token expires - expiry_date: { - type: DataTypes.DATE, - allowNull: false - } -}, { - sequelize, - modelName: "UserRefreshToken", - timestamps: false -}); +UserRefreshToken.init( + { + // user id to identify who the refresh token belongs to + user_id: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: User, + key: "id", + }, + onDelete: "CASCADE", + primaryKey: true, + }, + // actual refresh token + refresh_token: { + type: DataTypes.STRING, + allowNull: false, + }, + // date when refresh token expires + expiry_date: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + sequelize, + modelName: "UserRefreshToken", + timestamps: false, + }, +); export default UserRefreshToken; diff --git a/src/api/databases/sql/sql.ts b/src/api/databases/sql/sql.ts index 304ee9f..f06dbd1 100644 --- a/src/api/databases/sql/sql.ts +++ b/src/api/databases/sql/sql.ts @@ -2,36 +2,35 @@ import { Sequelize } from "sequelize"; // setup sequelize with provided parameters const sequelize = new Sequelize({ - dialect: "mysql", - host: "mysql", - port: parseInt(process.env.MYSQL_PORT as string, 10) || 3306, - username: process.env.MYSQL_USER || "", - password: process.env.MYSQL_PASSWORD || "", - database: process.env.MYSQL_DATABASE || "" + dialect: "mysql", + host: "mysql", + port: parseInt(process.env.MYSQL_PORT as string, 10) || 3306, + username: process.env.MYSQL_USER || "", + password: process.env.MYSQL_PASSWORD || "", + database: process.env.MYSQL_DATABASE || "", }); // initialize databases const initializeDatabase = async () => { - try { - // connect to the database - await sequelize.authenticate(); - console.info("Connection to the database has been established successfully."); + try { + // connect to the database + await sequelize.authenticate(); + console.info( + "Connection to the database has been established successfully.", + ); - // a primary instance is assigned to alter tables if necessary in development/playground - // not ideal, but works and good enough for now - if (process.env.NODE_ENV !== "production" && process.env.IS_PRIMARY) { - await sequelize.sync({ alter: true }); - } else { - await sequelize.sync(); - } + // a primary instance is assigned to alter tables if necessary in development/playground + // not ideal, but works and good enough for now + if (process.env.NODE_ENV !== "production" && process.env.IS_PRIMARY) { + await sequelize.sync({ alter: true }); + } else { + await sequelize.sync(); + } - console.info("All models were synchronized successfully."); - } catch (error) { - console.error("Unable to connect to the database:", error); - } + console.info("All models were synchronized successfully."); + } catch (error) { + console.error("Unable to connect to the database:", error); + } }; -export { - initializeDatabase, - sequelize -}; +export { initializeDatabase, sequelize }; diff --git a/src/api/index.ts b/src/api/index.ts index 700ca97..4fc4e1e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -21,7 +21,7 @@ dotenv.config(); // enable express session debugging if not in prod if (process.env.NODE_ENV !== "production") { - process.env.DEBUG = "express-session"; + process.env.DEBUG = "express-session"; } // initialize database @@ -33,33 +33,40 @@ setUpMinioBucket(); const app = express(); // handle cors -app.use(cors({ - origin: process.env.FRONTEND_WEBSITE_URL, - credentials: true -})); +app.use( + cors({ + origin: process.env.FRONTEND_WEBSITE_URL, + credentials: true, + }), +); app.use(bodyParser.json()); // needed to ensure correct protocol due to nginx proxies app.set("trust proxy", true); // handles user session -app.use(session({ - store: redisSessionStore, - secret: process.env.SESSION_SECRET as string, - resave: false, - saveUninitialized: true, - cookie: { - httpOnly: true, - // if developing locally, set to insecure - secure: process.env.NODE_ENV !== "development", - // in production, use "lax" as frontend and backend have the same root domain - sameSite: process.env.NODE_ENV === "production" ? "lax" : "none", - // if not in production, leave domain as undefined - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_WEBSITE_DOMAIN : undefined, - // expire after 3 months (milliseconds) - maxAge: 7776000000 - }, -})); +app.use( + session({ + store: redisSessionStore, + secret: process.env.SESSION_SECRET as string, + resave: false, + saveUninitialized: true, + cookie: { + httpOnly: true, + // if developing locally, set to insecure + secure: process.env.NODE_ENV !== "development", + // in production, use "lax" as frontend and backend have the same root domain + sameSite: process.env.NODE_ENV === "production" ? "lax" : "none", + // if not in production, leave domain as undefined + domain: + process.env.NODE_ENV === "production" + ? process.env.FRONTEND_WEBSITE_DOMAIN + : undefined, + // expire after 3 months (milliseconds) + maxAge: 7776000000, + }, + }), +); // handle routes const API_PREFIX = `/api/${process.env.API_VERSION}`; @@ -69,33 +76,40 @@ app.use(`${API_PREFIX}/users`, userRoutes); // load the swagger docs only if not in production if (process.env.NODE_ENV !== "production") { - const tsFilesInDir = fs.readdirSync(path.join(__dirname, "./swagger")).filter(file => path.extname(file) === ".js"); - let result = {}; - - const loadSwaggerFiles = async () => { - for (const file of tsFilesInDir) { - const filePath = path.join(__dirname, "./swagger", file); - const fileData = await import(filePath); - result = { ...result, ...fileData.default }; - } - - (swaggerDocument as any).paths = result; - - app.use("/api-docs", (req: any, res: any, next: any) => { - req.swaggerDoc = swaggerDocument; - next(); - }, swaggerUi.serveFiles(swaggerDocument), swaggerUi.setup()); - - console.info(`Swagger docs loaded.`); - }; - - loadSwaggerFiles(); + const tsFilesInDir = fs + .readdirSync(path.join(__dirname, "./swagger")) + .filter((file) => path.extname(file) === ".js"); + let result = {}; + + const loadSwaggerFiles = async () => { + for (const file of tsFilesInDir) { + const filePath = path.join(__dirname, "./swagger", file); + const fileData = await import(filePath); + result = { ...result, ...fileData.default }; + } + + (swaggerDocument as any).paths = result; + + app.use( + "/api-docs", + (req: any, res: any, next: any) => { + req.swaggerDoc = swaggerDocument; + next(); + }, + swaggerUi.serveFiles(swaggerDocument), + swaggerUi.setup(), + ); + + console.info(`Swagger docs loaded.`); + }; + + loadSwaggerFiles(); } else { - console.info("Swagger docs are disabled in production."); + console.info("Swagger docs are disabled in production."); } // start server, default to port 3000 if not specified const PORT = process.env.PORT || 3000; app.listen(PORT, () => { - console.info(`Server is running on port ${PORT}`); -}); \ No newline at end of file + console.info(`Server is running on port ${PORT}`); +}); diff --git a/src/api/interfaces/GitHubRepoContent.ts b/src/api/interfaces/GitHubRepoContent.ts index 7b7eb6f..a5a017d 100644 --- a/src/api/interfaces/GitHubRepoContent.ts +++ b/src/api/interfaces/GitHubRepoContent.ts @@ -1,22 +1,20 @@ // contents retrieved from github repo api // todo: check if fields are all present interface GitHubRepoContent { - name: string; - path: string; - sha: string; - size: number; - url: string; - html_url: string; - git_url: string; - download_url: string | null; - type: string; - _links: { - self: string; - git: string; - html: string; - }; + name: string; + path: string; + sha: string; + size: number; + url: string; + html_url: string; + git_url: string; + download_url: string | null; + type: string; + _links: { + self: string; + git: string; + html: string; + }; } -export { - GitHubRepoContent -}; +export { GitHubRepoContent }; diff --git a/src/api/interfaces/ThemeMetaData.ts b/src/api/interfaces/ThemeMetaData.ts index 8fa6336..2516f82 100644 --- a/src/api/interfaces/ThemeMetaData.ts +++ b/src/api/interfaces/ThemeMetaData.ts @@ -1,13 +1,11 @@ // contents retrieved from theme meta data (i.e. github meta.json) interface ThemeMetaData { - name: string; - description: string; - author: string; - github: string; - tags: string[]; - version: string; + name: string; + description: string; + author: string; + github: string; + tags: string[]; + version: string; } -export { - ThemeMetaData -}; +export { ThemeMetaData }; diff --git a/src/api/interfaces/TokenResponse.ts b/src/api/interfaces/TokenResponse.ts index f2437fa..d1724ff 100644 --- a/src/api/interfaces/TokenResponse.ts +++ b/src/api/interfaces/TokenResponse.ts @@ -1,11 +1,9 @@ // token information consolidated from provider interface TokenResponse { - access_token: string; - access_token_expiry: number; - refresh_token: string; - refresh_token_expiry: number; + access_token: string; + access_token_expiry: number; + refresh_token: string; + refresh_token_expiry: number; } -export { - TokenResponse -}; +export { TokenResponse }; diff --git a/src/api/interfaces/UserData.ts b/src/api/interfaces/UserData.ts index 12f4a1c..6100263 100644 --- a/src/api/interfaces/UserData.ts +++ b/src/api/interfaces/UserData.ts @@ -1,18 +1,16 @@ // user data persisted for a session, similar to user provider data but contains additional id and role fields interface UserData { - id: string; - role: string; - name: string; - email: string; - handle: string; - avatar_url: string; - status: string; - location: string; - profile_url: string; - provider: string; - provider_user_id: string; + id: string; + role: string; + name: string; + email: string; + handle: string; + avatar_url: string; + status: string; + location: string; + profile_url: string; + provider: string; + provider_user_id: string; } -export { - UserData -}; +export { UserData }; diff --git a/src/api/interfaces/UserProviderData.ts b/src/api/interfaces/UserProviderData.ts index 6e7aa63..d8e8f66 100644 --- a/src/api/interfaces/UserProviderData.ts +++ b/src/api/interfaces/UserProviderData.ts @@ -1,16 +1,14 @@ // common user data retrieved from providers interface UserProviderData { - name: string; - email: string; - handle: string; - avatar_url: string; - status: string; - location: string; - profile_url: string; - provider: string; - provider_user_id: string; + name: string; + email: string; + handle: string; + avatar_url: string; + status: string; + location: string; + profile_url: string; + provider: string; + provider_user_id: string; } -export { - UserProviderData -}; +export { UserProviderData }; diff --git a/src/api/middleware/userSessionMiddleware.ts b/src/api/middleware/userSessionMiddleware.ts index 3554da0..a5760e5 100644 --- a/src/api/middleware/userSessionMiddleware.ts +++ b/src/api/middleware/userSessionMiddleware.ts @@ -3,22 +3,30 @@ import { getUserData } from "../services/authentication/authentication"; /** * Checks if an existing user session exists and if does, attach user data. - * + * * @param req request from call * @param res response to call * @param next next to proceed - * + * * @returns 403 if session not found, else proceed */ -const checkUserSession = async (req: Request, res: Response, next: NextFunction) => { - const userData = await getUserData(req.sessionID, req.session.userId || null, req.session.provider as string); +const checkUserSession = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const userData = await getUserData( + req.sessionID, + req.session.userId || null, + req.session.provider as string, + ); - if (!userData) { - return res.status(401).json({ error: "User session not found" }); - } + if (!userData) { + return res.status(401).json({ error: "User session not found" }); + } - req.userData = userData; - next(); + req.userData = userData; + next(); }; -export default checkUserSession; \ No newline at end of file +export default checkUserSession; diff --git a/src/api/routes/authRoutes.ts b/src/api/routes/authRoutes.ts index 72d456a..e073059 100644 --- a/src/api/routes/authRoutes.ts +++ b/src/api/routes/authRoutes.ts @@ -1,7 +1,7 @@ import express from "express"; import { - handleCallback, - handleLoginProcess, + handleCallback, + handleLoginProcess, } from "../controllers/authController"; const router = express.Router(); diff --git a/src/api/routes/themeRoutes.ts b/src/api/routes/themeRoutes.ts index 7e4d518..bfc9ae1 100644 --- a/src/api/routes/themeRoutes.ts +++ b/src/api/routes/themeRoutes.ts @@ -1,10 +1,10 @@ import express from "express"; import multer from "multer"; import { - getThemes, - getThemeVersions, - publishTheme, - unpublishTheme, + getThemes, + getThemeVersions, + publishTheme, + unpublishTheme, } from "../controllers/themeController"; import checkUserSession from "../middleware/userSessionMiddleware"; @@ -13,27 +13,29 @@ const storage = multer.memoryStorage(); // file upload middleware with file type filter and limits const upload = multer({ - storage: storage, - // todo: review this limit - limits: { - fileSize: 5 * 1024 * 1024, // default to 5mb - }, - fileFilter: (req, file, cb) => { - // allow only these file extensions - const allowedExtensions = [".css", ".json", ".png"]; - const fileExtension = getFileExtension(file.originalname); - // todo: can enforce file name together with extension as well - if (allowedExtensions.includes(fileExtension)) { - cb(null, true); - } else { - cb(new Error("Invalid file extension")); - } - } + storage: storage, + // todo: review this limit + limits: { + fileSize: 5 * 1024 * 1024, // default to 5mb + }, + fileFilter: (req, file, cb) => { + // allow only these file extensions + const allowedExtensions = [".css", ".json", ".png"]; + const fileExtension = getFileExtension(file.originalname); + // todo: can enforce file name together with extension as well + if (allowedExtensions.includes(fileExtension)) { + cb(null, true); + } else { + cb(new Error("Invalid file extension")); + } + }, }); // helper function to get file extension function getFileExtension(filename: string) { - return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2).toLowerCase(); + return filename + .slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2) + .toLowerCase(); } const router = express.Router(); @@ -45,13 +47,18 @@ router.get("/", getThemes); router.get("/versions", getThemeVersions); // publish theme -router.post("/publish", checkUserSession, upload.fields([ - { name: "styles", maxCount: 1 }, - { name: "options", maxCount: 1 }, - { name: "display", maxCount: 1 } -]), publishTheme); +router.post( + "/publish", + checkUserSession, + upload.fields([ + { name: "styles", maxCount: 1 }, + { name: "options", maxCount: 1 }, + { name: "display", maxCount: 1 }, + ]), + publishTheme, +); // unpublish theme router.delete("/unpublish", checkUserSession, unpublishTheme); -export default router; \ No newline at end of file +export default router; diff --git a/src/api/routes/userRoutes.ts b/src/api/routes/userRoutes.ts index f8673b0..20d26af 100644 --- a/src/api/routes/userRoutes.ts +++ b/src/api/routes/userRoutes.ts @@ -1,10 +1,10 @@ import express from "express"; import { - addUserFavoriteTheme, - getUserFavoriteThemes, - getUserProfile, - getUserThemes, - removeUserFavoriteTheme + addUserFavoriteTheme, + getUserFavoriteThemes, + getUserProfile, + getUserThemes, + removeUserFavoriteTheme, } from "../controllers/userController"; import checkUserSession from "../middleware/userSessionMiddleware"; @@ -23,7 +23,7 @@ router.get("/themes/favorited", checkUserSession, getUserFavoriteThemes); router.post("/themes/favorited", checkUserSession, addUserFavoriteTheme); // unfavorites a theme for user -router.delete("/themes/favorited", checkUserSession, removeUserFavoriteTheme) +router.delete("/themes/favorited", checkUserSession, removeUserFavoriteTheme); // todo: add an endpoint for users to attempt to claim theme ownership // required if a theme is on github but the author has never logged @@ -31,4 +31,4 @@ router.delete("/themes/favorited", checkUserSession, removeUserFavoriteTheme) // since this means themes were directly added to github without going // through the website -export default router; \ No newline at end of file +export default router; diff --git a/src/api/services/authentication/authentication.ts b/src/api/services/authentication/authentication.ts index f47d28e..d69a714 100644 --- a/src/api/services/authentication/authentication.ts +++ b/src/api/services/authentication/authentication.ts @@ -19,182 +19,204 @@ import * as GitHubProvider from "./providers/github"; * * @returns token response if successful, null otherwise */ -const fetchTokensWithCode = async (sessionId: string, key: string, provider: string): Promise => { - let tokenResponse = null; - if (provider === process.env.GITHUB_LOGIN_PROVIDER) { - tokenResponse = await GitHubProvider.getUserTokensWithCode(key); - } - - // if unable to get valid token response, return null - if (!tokenResponse) { - return null; - } - - try { - // save access token to Redis - await redisEphemeralClient.set( - `${process.env.USER_TOKEN_PREFIX as string}:${sessionId}`, - encrypt(tokenResponse.access_token), - { EX: 27900 } - ); - } catch (error) { - } - - return tokenResponse; -} +const fetchTokensWithCode = async ( + sessionId: string, + key: string, + provider: string, +): Promise => { + let tokenResponse = null; + if (provider === process.env.GITHUB_LOGIN_PROVIDER) { + tokenResponse = await GitHubProvider.getUserTokensWithCode(key); + } + + // if unable to get valid token response, return null + if (!tokenResponse) { + return null; + } + + try { + // save access token to Redis + await redisEphemeralClient.set( + `${process.env.USER_TOKEN_PREFIX as string}:${sessionId}`, + encrypt(tokenResponse.access_token), + { EX: 27900 }, + ); + } catch (error) {} + + return tokenResponse; +}; /** * Retrieves user session data with given session id, user id and provider. - * + * * @param sessionId id of the session * @param userId id of the user, can be null if first time logging in * @param provider provider that this session was created (logged in) with * * @returns session data of user if successfully retrieved, null otherwise */ -const getUserData = async (sessionId: string, userId: string | null, provider: string): Promise => { - // if user data is still in cache, parse and return - try { - const cachedUserData = await redisEphemeralClient.get(`${process.env.USER_DATA_PREFIX}:${sessionId}`); - if (cachedUserData) { - return JSON.parse(cachedUserData as string); - } - } catch { - // if cannot get from cache, then below we try to get user data from provider again - } - - // should not be empty but if for whatever reason there is no provider found, then user has to relogin - if (!provider) { - return null; - } - - // if user data not in cache, then try to fetch data from the provider with access token - try { - const encryptedToken = await redisEphemeralClient.get(`${process.env.USER_TOKEN_PREFIX as string}:${sessionId}`); - const accessToken = encryptedToken ? decrypt(encryptedToken) : null; - const userProviderData = await getUserProviderDataFromProvider(sessionId, userId, accessToken, provider); - if (userProviderData) { - // get user data, will create user if user does not exist - const user = await getOrCreateUser(userProviderData); - if (!user) { - return null; - } - - const userData: UserData = { - id: user.dataValues.id, - role: user.dataValues.role, - ...userProviderData - } - - // save user data to cache, expires every 15mins to update - await redisEphemeralClient.set( - `${process.env.USER_DATA_PREFIX as string}:${sessionId}`, - JSON.stringify(userProviderData), - { EX: 900 } - ); - - return userData; - } - return null; - } catch { - return null; - } -} +const getUserData = async ( + sessionId: string, + userId: string | null, + provider: string, +): Promise => { + // if user data is still in cache, parse and return + try { + const cachedUserData = await redisEphemeralClient.get( + `${process.env.USER_DATA_PREFIX}:${sessionId}`, + ); + if (cachedUserData) { + return JSON.parse(cachedUserData as string); + } + } catch { + // if cannot get from cache, then below we try to get user data from provider again + } + + // should not be empty but if for whatever reason there is no provider found, then user has to relogin + if (!provider) { + return null; + } + + // if user data not in cache, then try to fetch data from the provider with access token + try { + const encryptedToken = await redisEphemeralClient.get( + `${process.env.USER_TOKEN_PREFIX as string}:${sessionId}`, + ); + const accessToken = encryptedToken ? decrypt(encryptedToken) : null; + const userProviderData = await getUserProviderDataFromProvider( + sessionId, + userId, + accessToken, + provider, + ); + if (userProviderData) { + // get user data, will create user if user does not exist + const user = await getOrCreateUser(userProviderData); + if (!user) { + return null; + } + + const userData: UserData = { + id: user.dataValues.id, + role: user.dataValues.role, + ...userProviderData, + }; + + // save user data to cache, expires every 15mins to update + await redisEphemeralClient.set( + `${process.env.USER_DATA_PREFIX as string}:${sessionId}`, + JSON.stringify(userProviderData), + { EX: 900 }, + ); + + return userData; + } + return null; + } catch { + return null; + } +}; /** * Retrieves a user and creates a new user if not exist. - * + * * @param userProviderData user data belonging to the current session - * + * * @returns existing or newly created user */ -const getOrCreateUser = async (userProviderData: UserProviderData): Promise => { - try { - // check if the email exists in the User table - const existingUser = await User.findOne({ - where: { - email: userProviderData.email - } - }); - - if (existingUser) { - // if user exist, check if the provider is newly linked - const linkedAuthProvider = await LinkedAuthProvider.findOne({ - where: { - user_id: existingUser.dataValues.id, - provider: userProviderData.provider - } - }); - - // if the provider with user doesn"t exist, add a new entry in LinkedAuthProvider - if (!linkedAuthProvider) { - await LinkedAuthProvider.create({ - provider_user_id: userProviderData.provider_user_id, - user_id: existingUser.dataValues.id, - provider: userProviderData.provider, - }); - } - - return existingUser; - } - - // if user does not exist, create a new user entry - const newUser = await User.create({ - email: userProviderData.email - }); - - // Add mapping in the LinkedAuthProvider table - await LinkedAuthProvider.create({ - provider_user_id: userProviderData.provider_user_id, - user_id: newUser.dataValues.id, - provider: userProviderData.provider - }); - - return newUser; - } catch (error) { - return null; - } -} +const getOrCreateUser = async ( + userProviderData: UserProviderData, +): Promise => { + try { + // check if the email exists in the User table + const existingUser = await User.findOne({ + where: { + email: userProviderData.email, + }, + }); + + if (existingUser) { + // if user exist, check if the provider is newly linked + const linkedAuthProvider = await LinkedAuthProvider.findOne({ + where: { + user_id: existingUser.dataValues.id, + provider: userProviderData.provider, + }, + }); + + // if the provider with user doesn"t exist, add a new entry in LinkedAuthProvider + if (!linkedAuthProvider) { + await LinkedAuthProvider.create({ + provider_user_id: userProviderData.provider_user_id, + user_id: existingUser.dataValues.id, + provider: userProviderData.provider, + }); + } + + return existingUser; + } + + // if user does not exist, create a new user entry + const newUser = await User.create({ + email: userProviderData.email, + }); + + // Add mapping in the LinkedAuthProvider table + await LinkedAuthProvider.create({ + provider_user_id: userProviderData.provider_user_id, + user_id: newUser.dataValues.id, + provider: userProviderData.provider, + }); + + return newUser; + } catch (error) { + return null; + } +}; /** * Saves user access token into cache and refresh token into mysql (both are encrypted). - * + * * @param sessionId id of the session * @param userId id of the user * @param tokenResponse token response to retrieve token information from * * @returns true if successfully saved, false otherwise */ -const saveUserTokens = async (sessionId: string, userId: string, tokenResponse: TokenResponse): Promise => { - try { - // save access token to Redis - await redisEphemeralClient.set( - `${process.env.USER_TOKEN_PREFIX as string}:${sessionId}`, - encrypt(tokenResponse.access_token), - { EX: 27900 } - ); - - // store refresh token into MySQL (upsert to overwrite if user_id exists) - await UserRefreshToken.upsert({ - user_id: userId, - refresh_token: encrypt(tokenResponse.refresh_token), - expiry_date: tokenResponse.refresh_token_expiry - }); - - return true; - } catch (error) { - console.error("Error saving user tokens:", error); - return false; - } +const saveUserTokens = async ( + sessionId: string, + userId: string, + tokenResponse: TokenResponse, +): Promise => { + try { + // save access token to Redis + await redisEphemeralClient.set( + `${process.env.USER_TOKEN_PREFIX as string}:${sessionId}`, + encrypt(tokenResponse.access_token), + { EX: 27900 }, + ); + + // store refresh token into MySQL (upsert to overwrite if user_id exists) + await UserRefreshToken.upsert({ + user_id: userId, + refresh_token: encrypt(tokenResponse.refresh_token), + expiry_date: tokenResponse.refresh_token_expiry, + }); + + return true; + } catch (error) { + console.error("Error saving user tokens:", error); + return false; + } }; -// +// // Functions below are used internally to call the auth providers (currently only github) // /** * Retrieves user provider data from the provider. - * + * * @param sessionId id of the session * @param userId id of the user, can be null if first time logging in * @param accessToken access token for the user @@ -203,28 +225,32 @@ const saveUserTokens = async (sessionId: string, userId: string, tokenResponse: * @returns session data of user if successfully retrieved, null otherwise */ const getUserProviderDataFromProvider = async ( - sessionId: string, - userId: string | null, - accessToken: string | null, - provider: string + sessionId: string, + userId: string | null, + accessToken: string | null, + provider: string, ) => { - if (!accessToken) { - const tokenResponse = await refreshProviderTokens(sessionId, userId, provider); - accessToken = tokenResponse ? tokenResponse.access_token : null; - } - - // if access token is null even after trying to refresh access token, get user to relogin - if (!accessToken) { - return null; - } - - let userProviderData = null; - if (provider === process.env.GITHUB_LOGIN_PROVIDER) { - userProviderData = await GitHubProvider.getUserData(accessToken); - } - - return userProviderData; -} + if (!accessToken) { + const tokenResponse = await refreshProviderTokens( + sessionId, + userId, + provider, + ); + accessToken = tokenResponse ? tokenResponse.access_token : null; + } + + // if access token is null even after trying to refresh access token, get user to relogin + if (!accessToken) { + return null; + } + + let userProviderData = null; + if (provider === process.env.GITHUB_LOGIN_PROVIDER) { + userProviderData = await GitHubProvider.getUserData(accessToken); + } + + return userProviderData; +}; /** * Refreshes the tokens from the provider. @@ -232,64 +258,65 @@ const getUserProviderDataFromProvider = async ( * @param sessionId id of the session * @param userId id of the user * @param provider provider that this session was created (logged in) with - * @returns + * @returns */ -const refreshProviderTokens = async (sessionId: string, userId: string | null, provider: string) => { - // if no user id provided, cannot refresh - if (!userId) { - return; - } - - try { - // get refresh token from database - const refreshTokenRecord = await UserRefreshToken.findOne({ - where: { - user_id: userId, - expiry_date: { - [Op.gt]: new Date() // Ensure the token has not expired - } - } - }); - - // if no valid refresh token is found, return null - if (!refreshTokenRecord) { - return null; - } - - // check token expiry and if expired, return null - const refreshTokenExpired = new Date(refreshTokenRecord.dataValues.expiry_date) <= new Date(); - if (refreshTokenExpired) { - return null; - } - - // decrypt and get refresh token - const refreshToken = decrypt(refreshTokenRecord.dataValues.refresh_token); - - // get new access token - let tokenResponse = null; - if (provider === process.env.GITHUB_LOGIN_PROVIDER) { - // Assuming you have a function that uses the refresh token to get new tokens from GitHub - tokenResponse = await GitHubProvider.getUserTokensWithRefresh(refreshToken); - } - - // save user tokens if response is valid - if (tokenResponse) { - if (await saveUserTokens(sessionId, userId, tokenResponse)) { - return tokenResponse; - } - } - - // if unable to save a valid token response, return null - return null; - } catch (error) { - console.error("Error during token refresh:", error); - return null; - } -}; - -export { - fetchTokensWithCode, - getUserData, - saveUserTokens +const refreshProviderTokens = async ( + sessionId: string, + userId: string | null, + provider: string, +) => { + // if no user id provided, cannot refresh + if (!userId) { + return; + } + + try { + // get refresh token from database + const refreshTokenRecord = await UserRefreshToken.findOne({ + where: { + user_id: userId, + expiry_date: { + [Op.gt]: new Date(), // Ensure the token has not expired + }, + }, + }); + + // if no valid refresh token is found, return null + if (!refreshTokenRecord) { + return null; + } + + // check token expiry and if expired, return null + const refreshTokenExpired = + new Date(refreshTokenRecord.dataValues.expiry_date) <= new Date(); + if (refreshTokenExpired) { + return null; + } + + // decrypt and get refresh token + const refreshToken = decrypt(refreshTokenRecord.dataValues.refresh_token); + + // get new access token + let tokenResponse = null; + if (provider === process.env.GITHUB_LOGIN_PROVIDER) { + // Assuming you have a function that uses the refresh token to get new tokens from GitHub + tokenResponse = + await GitHubProvider.getUserTokensWithRefresh(refreshToken); + } + + // save user tokens if response is valid + if (tokenResponse) { + if (await saveUserTokens(sessionId, userId, tokenResponse)) { + return tokenResponse; + } + } + + // if unable to save a valid token response, return null + return null; + } catch (error) { + console.error("Error during token refresh:", error); + return null; + } }; +export { fetchTokensWithCode, getUserData, saveUserTokens }; diff --git a/src/api/services/authentication/providers/github.ts b/src/api/services/authentication/providers/github.ts index 14456e6..e776b9e 100644 --- a/src/api/services/authentication/providers/github.ts +++ b/src/api/services/authentication/providers/github.ts @@ -12,38 +12,40 @@ import { decrypt } from "../../cryptoService"; * @returns token response object if successful, null otherwise */ const getUserTokensWithCode = async (key: string) => { - const code = decrypt(key); - try { - const response = await axios({ - method: "post", - url: `https://github.com/login/oauth/access_token?` + - `client_id=${process.env.GITHUB_APP_CLIENT_ID}&` + - `client_secret=${process.env.GITHUB_APP_CLIENT_SECRET}&` + - `code=${code}&` + - `scope=user:email,repo`, - headers: { - accept: "application/json" - } - }); - - if (response.status >= 300) { - return null; - } - - // buffer 15 minutes from token expiry times, hence -900 - // multiply expiry time by 1000 since it is given in seconds - const tokenResponse: TokenResponse = { - access_token: response.data.access_token, - access_token_expiry: Date.now() + (response.data.expires_in * 1000) - 900, - refresh_token: response.data.refresh_token, - refresh_token_expiry: Date.now() + (response.data.refresh_token_expires_in * 1000) - 900 - } - return tokenResponse; - } catch (error) { - console.error("Error getting access token from GitHub:", error); - return null; - } -} + const code = decrypt(key); + try { + const response = await axios({ + method: "post", + url: + `https://github.com/login/oauth/access_token?` + + `client_id=${process.env.GITHUB_APP_CLIENT_ID}&` + + `client_secret=${process.env.GITHUB_APP_CLIENT_SECRET}&` + + `code=${code}&` + + `scope=user:email,repo`, + headers: { + accept: "application/json", + }, + }); + + if (response.status >= 300) { + return null; + } + + // buffer 15 minutes from token expiry times, hence -900 + // multiply expiry time by 1000 since it is given in seconds + const tokenResponse: TokenResponse = { + access_token: response.data.access_token, + access_token_expiry: Date.now() + response.data.expires_in * 1000 - 900, + refresh_token: response.data.refresh_token, + refresh_token_expiry: + Date.now() + response.data.refresh_token_expires_in * 1000 - 900, + }; + return tokenResponse; + } catch (error) { + console.error("Error getting access token from GitHub:", error); + return null; + } +}; /** * Retrieves access and refresh token with current refresh token. @@ -53,36 +55,38 @@ const getUserTokensWithCode = async (key: string) => { * @returns token response object if successful, null otherwise */ const getUserTokensWithRefresh = async (refreshToken: string) => { - try { - const response = await axios({ - method: "post", - url: `https://github.com/login/oauth/access_token?` + - `client_id=${process.env.GITHUB_APP_CLIENT_ID}&` + - `client_secret=${process.env.GITHUB_APP_CLIENT_SECRET}&` + - "grant_type=refresh_token&" + - `refresh_token=${refreshToken}`, - headers: { - accept: "application/json" - } - }); - - if (response.status >= 300) { - return null; - } - - // buffer 15 minutes from token expiry times, hence -900 - // multiply expiry time by 1000 since it is given in seconds - const tokenResponse: TokenResponse = { - access_token: response.data.access_token, - access_token_expiry: Date.now() + (response.data.expires_in * 1000) - 900, - refresh_token: response.data.refresh_token, - refresh_token_expiry: Date.now() + (response.data.refresh_token_expires_in * 1000) - 900 - } - return tokenResponse; - } catch (error) { - console.error("Error refreshing GitHub tokens:", error); - throw error; - } + try { + const response = await axios({ + method: "post", + url: + `https://github.com/login/oauth/access_token?` + + `client_id=${process.env.GITHUB_APP_CLIENT_ID}&` + + `client_secret=${process.env.GITHUB_APP_CLIENT_SECRET}&` + + "grant_type=refresh_token&" + + `refresh_token=${refreshToken}`, + headers: { + accept: "application/json", + }, + }); + + if (response.status >= 300) { + return null; + } + + // buffer 15 minutes from token expiry times, hence -900 + // multiply expiry time by 1000 since it is given in seconds + const tokenResponse: TokenResponse = { + access_token: response.data.access_token, + access_token_expiry: Date.now() + response.data.expires_in * 1000 - 900, + refresh_token: response.data.refresh_token, + refresh_token_expiry: + Date.now() + response.data.refresh_token_expires_in * 1000 - 900, + }; + return tokenResponse; + } catch (error) { + console.error("Error refreshing GitHub tokens:", error); + throw error; + } }; /** @@ -92,64 +96,62 @@ const getUserTokensWithRefresh = async (refreshToken: string) => { * * @returns user data from github */ -const getUserData = async (accessToken: string): Promise => { - try { - const userResponse = await axios({ - method: "get", - url: `https://api.github.com/user`, - headers: { - Authorization: "Bearer " + accessToken - } - }); - - if (userResponse.status >= 300) { - return null; - } - const data = userResponse.data - - // handles cases where users make email private, need to fetch specially - if (!data.email) { - const emailResponse = await axios({ - method: "get", - url: `https://api.github.com/user/emails`, - headers: { - Authorization: "Bearer " + accessToken - } - }); - - if (emailResponse.status >= 300) { - return null; - } - - const result = emailResponse.data.find((email: { - email: string, verified: boolean, primary: boolean; - }) => email.primary === true); - - data.email = result.email; - } - - // insert data into common provider data fields - const userProviderData: UserProviderData = { - name: data.name, - email: data.email, - handle: data.login, - avatar_url: data.avatar_url, - status: data.bio, - location: data.location, - profile_url: data.html_url, - provider_user_id: data.id, - provider: process.env.GITHUB_LOGIN_PROVIDER as string - }; - return userProviderData; - } catch (error) { - console.error("Error fetching user data from GitHub:", error); - return null; - } -} - -export { - getUserData, - getUserTokensWithCode, - getUserTokensWithRefresh +const getUserData = async ( + accessToken: string, +): Promise => { + try { + const userResponse = await axios({ + method: "get", + url: `https://api.github.com/user`, + headers: { + Authorization: "Bearer " + accessToken, + }, + }); + + if (userResponse.status >= 300) { + return null; + } + const data = userResponse.data; + + // handles cases where users make email private, need to fetch specially + if (!data.email) { + const emailResponse = await axios({ + method: "get", + url: `https://api.github.com/user/emails`, + headers: { + Authorization: "Bearer " + accessToken, + }, + }); + + if (emailResponse.status >= 300) { + return null; + } + + const result = emailResponse.data.find( + (email: { email: string; verified: boolean; primary: boolean }) => + email.primary === true, + ); + + data.email = result.email; + } + + // insert data into common provider data fields + const userProviderData: UserProviderData = { + name: data.name, + email: data.email, + handle: data.login, + avatar_url: data.avatar_url, + status: data.bio, + location: data.location, + profile_url: data.html_url, + provider_user_id: data.id, + provider: process.env.GITHUB_LOGIN_PROVIDER as string, + }; + return userProviderData; + } catch (error) { + console.error("Error fetching user data from GitHub:", error); + return null; + } }; +export { getUserData, getUserTokensWithCode, getUserTokensWithRefresh }; diff --git a/src/api/services/authorization.ts b/src/api/services/authorization.ts index 57bf65a..3ddf73d 100644 --- a/src/api/services/authorization.ts +++ b/src/api/services/authorization.ts @@ -8,9 +8,7 @@ import { UserData } from "../interfaces/UserData"; * @returns true if user is admin, false otherwise */ const checkIsAdminUser = (userData: UserData) => { - return userData.role === "ADMIN"; -} - -export { - checkIsAdminUser + return userData.role === "ADMIN"; }; + +export { checkIsAdminUser }; diff --git a/src/api/services/cryptoService.ts b/src/api/services/cryptoService.ts index 56297b0..d421be5 100644 --- a/src/api/services/cryptoService.ts +++ b/src/api/services/cryptoService.ts @@ -8,15 +8,19 @@ import crypto from "crypto"; * @returns encrypted text */ const encrypt = (text: string): string => { - const algorithm = "aes-256-cbc"; - const key = process.env.GITHUB_TOKEN_ENCRYPTION_KEY as string; - const iv = crypto.randomBytes(16); - - const cipher = crypto.createCipheriv(algorithm, Buffer.from(key, "base64"), iv); - let encrypted = cipher.update(text, "utf-8"); - encrypted = Buffer.concat([encrypted, cipher.final()]); - - return iv.toString("hex") + ":" + encrypted.toString("hex"); + const algorithm = "aes-256-cbc"; + const key = process.env.GITHUB_TOKEN_ENCRYPTION_KEY as string; + const iv = crypto.randomBytes(16); + + const cipher = crypto.createCipheriv( + algorithm, + Buffer.from(key, "base64"), + iv, + ); + let encrypted = cipher.update(text, "utf-8"); + encrypted = Buffer.concat([encrypted, cipher.final()]); + + return iv.toString("hex") + ":" + encrypted.toString("hex"); }; /** @@ -27,21 +31,22 @@ const encrypt = (text: string): string => { * @returns decrypted text */ const decrypt = (text: string): string => { - const algorithm = "aes-256-cbc"; - const key = process.env.GITHUB_TOKEN_ENCRYPTION_KEY as string; - - const parts = text.split(":"); - const iv = Buffer.from(parts.shift() as string, "hex"); - const encryptedText = Buffer.from(parts.join(":"), "hex"); - - const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key, "base64"), iv); - let decrypted = decipher.update(encryptedText); - decrypted = Buffer.concat([decrypted, decipher.final()]); - - return decrypted.toString(); -}; - -export { - decrypt, encrypt + const algorithm = "aes-256-cbc"; + const key = process.env.GITHUB_TOKEN_ENCRYPTION_KEY as string; + + const parts = text.split(":"); + const iv = Buffer.from(parts.shift() as string, "hex"); + const encryptedText = Buffer.from(parts.join(":"), "hex"); + + const decipher = crypto.createDecipheriv( + algorithm, + Buffer.from(key, "base64"), + iv, + ); + let decrypted = decipher.update(encryptedText); + decrypted = Buffer.concat([decrypted, decipher.final()]); + + return decrypted.toString(); }; +export { decrypt, encrypt }; diff --git a/src/api/services/github/pullRequest.ts b/src/api/services/github/pullRequest.ts index 8c44063..1f224fc 100644 --- a/src/api/services/github/pullRequest.ts +++ b/src/api/services/github/pullRequest.ts @@ -11,74 +11,74 @@ const token = "your_personal_access_token"; const baseUrl = "https://api.github.com"; const headers = { - Authorization: `token ${token}`, - Accept: "application/vnd.github.v3+json" + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json", }; async function createBranch() { - const config: AxiosRequestConfig = { - headers: headers - }; + const config: AxiosRequestConfig = { + headers: headers, + }; - try { - // create a new branch - const response = await axios.post( - `${baseUrl}/repos/${owner}/${repo}/git/refs`, - { - ref: `refs/heads/${newBranch}`, - sha: baseBranch - }, - config - ); - console.info("Branch created:", response.data); - return true; - } catch (error) { - console.error("Error creating branch:", error); - return false; - } + try { + // create a new branch + const response = await axios.post( + `${baseUrl}/repos/${owner}/${repo}/git/refs`, + { + ref: `refs/heads/${newBranch}`, + sha: baseBranch, + }, + config, + ); + console.info("Branch created:", response.data); + return true; + } catch (error) { + console.error("Error creating branch:", error); + return false; + } } async function addFile() { - try { - // read the content of styles.css - const cssContent = readFileSync("path/to/styles.css", "utf-8"); + try { + // read the content of styles.css + const cssContent = readFileSync("path/to/styles.css", "utf-8"); - // add a new file - const response = await axios.put( - `${baseUrl}/repos/${owner}/${repo}/contents/styles/styles.css`, - { - message: "Add styles.css", - content: Buffer.from(cssContent).toString("base64"), - branch: newBranch - }, - { headers: headers } - ); - console.info("File added:", response.data); - return true; - } catch (error) { - console.error("Error adding file:", error); - return false; - } + // add a new file + const response = await axios.put( + `${baseUrl}/repos/${owner}/${repo}/contents/styles/styles.css`, + { + message: "Add styles.css", + content: Buffer.from(cssContent).toString("base64"), + branch: newBranch, + }, + { headers: headers }, + ); + console.info("File added:", response.data); + return true; + } catch (error) { + console.error("Error adding file:", error); + return false; + } } async function createPullRequest() { - try { - // create a pull request - const response = await axios.post( - `${baseUrl}/repos/${owner}/${repo}/pulls`, - { - title: "Add styles.css", - head: newBranch, - base: baseBranch - }, - { headers: headers } - ); - console.info("Pull request created:", response.data); - return true; - } catch (error) { - console.error("Error creating pull request:", error); - return false; - } + try { + // create a pull request + const response = await axios.post( + `${baseUrl}/repos/${owner}/${repo}/pulls`, + { + title: "Add styles.css", + head: newBranch, + base: baseBranch, + }, + { headers: headers }, + ); + console.info("Pull request created:", response.data); + return true; + } catch (error) { + console.error("Error creating pull request:", error); + return false; + } } // const branchCreated = await createBranch(); diff --git a/src/api/services/minioService.ts b/src/api/services/minioService.ts index 7d1987d..b30c7e7 100644 --- a/src/api/services/minioService.ts +++ b/src/api/services/minioService.ts @@ -2,11 +2,11 @@ import { BucketItem, Client, ClientOptions } from "minio"; // define minio client options based on parameters const minioClientOptions: ClientOptions = { - endPoint: "minio", - port: 9000, - useSSL: false, - accessKey: process.env.MINIO_ROOT_USER as string, - secretKey: process.env.MINIO_ROOT_PASSWORD as string, + endPoint: "minio", + port: 9000, + useSSL: false, + accessKey: process.env.MINIO_ROOT_USER as string, + secretKey: process.env.MINIO_ROOT_PASSWORD as string, }; // initialize minio client @@ -14,11 +14,11 @@ const minioClient = new Client(minioClientOptions); // setup minio bucket for theme jobs const setUpMinioBucket = async () => { - try { - await createBucketIfNotExists("theme-jobs-queue"); - } catch (err) { - console.error("Failed to initialize MinIO bucket:", err); - } + try { + await createBucketIfNotExists("theme-jobs-queue"); + } catch (err) { + console.error("Failed to initialize MinIO bucket:", err); + } }; /** @@ -27,22 +27,22 @@ const setUpMinioBucket = async () => { * @param bucketName name of bucket to create */ const createBucketIfNotExists = async (bucketName: string): Promise => { - try { - const bucketExists = await minioClient.bucketExists(bucketName); - if (!bucketExists) { - await minioClient.makeBucket(bucketName, ""); - console.info(`Bucket ${bucketName} created successfully.`); - } else { - console.info(`Bucket ${bucketName} already exists.`); - } - } catch (err: any) { - // if bucket already owned, not an error (possible due to multiple api instances) - if (err.code == "BucketAlreadyOwnedByYou") { - return; - } - console.error("Error checking or creating bucket:", err); - throw err; - } + try { + const bucketExists = await minioClient.bucketExists(bucketName); + if (!bucketExists) { + await minioClient.makeBucket(bucketName, ""); + console.info(`Bucket ${bucketName} created successfully.`); + } else { + console.info(`Bucket ${bucketName} already exists.`); + } + } catch (err: any) { + // if bucket already owned, not an error (possible due to multiple api instances) + if (err.code == "BucketAlreadyOwnedByYou") { + return; + } + console.error("Error checking or creating bucket:", err); + throw err; + } }; /** @@ -52,14 +52,18 @@ const createBucketIfNotExists = async (bucketName: string): Promise => { * @param objectName name of file * @param filePath path to file */ -const uploadFile = async (bucketName: string, objectName: string, filePath: string): Promise => { - try { - await minioClient.fPutObject(bucketName, objectName, filePath); - console.info(`File ${objectName} uploaded successfully.`); - } catch (err: any) { - console.error("Error uploading file:", err); - throw err; - } +const uploadFile = async ( + bucketName: string, + objectName: string, + filePath: string, +): Promise => { + try { + await minioClient.fPutObject(bucketName, objectName, filePath); + console.info(`File ${objectName} uploaded successfully.`); + } catch (err: any) { + console.error("Error uploading file:", err); + throw err; + } }; /** @@ -70,20 +74,27 @@ const uploadFile = async (bucketName: string, objectName: string, filePath: stri * * @returns file if found, null otherwise */ -const getFile = async (bucketName: string, objectName: string): Promise => { - try { - const objectsStream = minioClient.listObjectsV2(bucketName, objectName, true); - for await (const obj of objectsStream) { - if (obj.name === objectName) { - return obj; - } - } - // if no object is found, return null - return null; - } catch (err: any) { - console.error("Error retrieving file:", err); - throw err; - } +const getFile = async ( + bucketName: string, + objectName: string, +): Promise => { + try { + const objectsStream = minioClient.listObjectsV2( + bucketName, + objectName, + true, + ); + for await (const obj of objectsStream) { + if (obj.name === objectName) { + return obj; + } + } + // if no object is found, return null + return null; + } catch (err: any) { + console.error("Error retrieving file:", err); + throw err; + } }; /** @@ -92,17 +103,17 @@ const getFile = async (bucketName: string, objectName: string): Promise => { - try { - await minioClient.removeObject(bucketName, objectName); - console.info(`File ${objectName} deleted successfully.`); - } catch (err: any) { - console.error("Error deleting file:", err); - throw err; - } -}; - -export { - deleteFile, getFile, setUpMinioBucket, uploadFile +const deleteFile = async ( + bucketName: string, + objectName: string, +): Promise => { + try { + await minioClient.removeObject(bucketName, objectName); + console.info(`File ${objectName} deleted successfully.`); + } catch (err: any) { + console.error("Error deleting file:", err); + throw err; + } }; +export { deleteFile, getFile, setUpMinioBucket, uploadFile }; diff --git a/src/api/swagger.js b/src/api/swagger.js index 5daabd3..da4ead9 100644 --- a/src/api/swagger.js +++ b/src/api/swagger.js @@ -1,9 +1,9 @@ const swaggerDocument = { - "openapi": "3.1.0", - "info": { - "title": "Gallery API Documentation", - "version": "1.0.0" - } -} + openapi: "3.1.0", + info: { + title: "Gallery API Documentation", + version: "1.0.0", + }, +}; -export default swaggerDocument; \ No newline at end of file +export default swaggerDocument; diff --git a/src/api/swagger/auth.js b/src/api/swagger/auth.js index fe15c22..bb26503 100644 --- a/src/api/swagger/auth.js +++ b/src/api/swagger/auth.js @@ -1,149 +1,153 @@ const authPaths = { - "/api/v1/auth/callback": { - "get": { - "tags": [ - "Authentication Module" - ], - "summary": "Handles the callback from OAuth provider.", - "description": "Processes the callback from the OAuth provider, handling authorization and redirection.", - "parameters": [ - { - "in": "query", - "name": "code", - "schema": { - "type": "string" - }, - "required": false, - "description": "The authorization code returned by the OAuth provider." - }, - { - "in": "query", - "name": "error", - "schema": { - "type": "string" - }, - "required": false, - "description": "Error message returned by the OAuth provider if the authorization fails." - } - ], - "responses": { - "302": { - "description": "Redirects user to either the login process page or an error page.", - "headers": { - "Location": { - "description": "The URL to which the user is redirected.", - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/v1/auth/login/process": { - "get": { - "tags": [ - "Authentication Module" - ], - "summary": "Handles the login process after OAuth callback.", - "description": "Uses the authorization code to fetch tokens, retrieve user data, and store them in a cache.", - "parameters": [ - { - "in": "query", - "name": "provider", - "schema": { - "type": "string" - }, - "required": true, - "description": "The OAuth provider (e.g. github)." - }, - { - "in": "query", - "name": "key", - "schema": { - "type": "string" - }, - "required": true, - "description": "The encrypted key generated from the authorization code." - } - ], - "responses": { - "200": { - "description": "Returns user data on successful login.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The user's unique ID." - }, - "role": { - "type": "string", - "description": "The user's role." - }, - "name": { - "type": "string", - "description": "The user's name." - }, - "email": { - "type": "string", - "description": "The user's email address." - }, - "handle": { - "type": "string", - "description": "The user's handle or username on the provider platform." - }, - "avatar_url": { - "type": "string", - "description": "The URL of the user's avatar image." - }, - "status": { - "type": "string", - "description": "The user's status, if any." - }, - "location": { - "type": "string", - "description": "The user's location." - }, - "profile_url": { - "type": "string", - "description": "The URL of the user's profile." - }, - "provider": { - "type": "string", - "description": "The OAuth provider used (e.g. github)." - }, - "provider_user_id": { - "type": "string", - "description": "The user's ID as provided by the OAuth provider." - } - } - } - } - } - }, - "401": { - "description": "Unauthorized due to invalid login credentials or missing provider.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "A message describing the error." - } - } - } - } - } - } - } - } - } -} + "/api/v1/auth/callback": { + get: { + tags: ["Authentication Module"], + summary: "Handles the callback from OAuth provider.", + description: + "Processes the callback from the OAuth provider, handling authorization and redirection.", + parameters: [ + { + in: "query", + name: "code", + schema: { + type: "string", + }, + required: false, + description: "The authorization code returned by the OAuth provider.", + }, + { + in: "query", + name: "error", + schema: { + type: "string", + }, + required: false, + description: + "Error message returned by the OAuth provider if the authorization fails.", + }, + ], + responses: { + 302: { + description: + "Redirects user to either the login process page or an error page.", + headers: { + Location: { + description: "The URL to which the user is redirected.", + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + "/api/v1/auth/login/process": { + get: { + tags: ["Authentication Module"], + summary: "Handles the login process after OAuth callback.", + description: + "Uses the authorization code to fetch tokens, retrieve user data, and store them in a cache.", + parameters: [ + { + in: "query", + name: "provider", + schema: { + type: "string", + }, + required: true, + description: "The OAuth provider (e.g. github).", + }, + { + in: "query", + name: "key", + schema: { + type: "string", + }, + required: true, + description: + "The encrypted key generated from the authorization code.", + }, + ], + responses: { + 200: { + description: "Returns user data on successful login.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + id: { + type: "string", + description: "The user's unique ID.", + }, + role: { + type: "string", + description: "The user's role.", + }, + name: { + type: "string", + description: "The user's name.", + }, + email: { + type: "string", + description: "The user's email address.", + }, + handle: { + type: "string", + description: + "The user's handle or username on the provider platform.", + }, + avatar_url: { + type: "string", + description: "The URL of the user's avatar image.", + }, + status: { + type: "string", + description: "The user's status, if any.", + }, + location: { + type: "string", + description: "The user's location.", + }, + profile_url: { + type: "string", + description: "The URL of the user's profile.", + }, + provider: { + type: "string", + description: "The OAuth provider used (e.g. github).", + }, + provider_user_id: { + type: "string", + description: + "The user's ID as provided by the OAuth provider.", + }, + }, + }, + }, + }, + }, + 401: { + description: + "Unauthorized due to invalid login credentials or missing provider.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: "A message describing the error.", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; -export default authPaths; \ No newline at end of file +export default authPaths; diff --git a/src/api/swagger/theme.js b/src/api/swagger/theme.js index 59a2e03..ac7af39 100644 --- a/src/api/swagger/theme.js +++ b/src/api/swagger/theme.js @@ -1,353 +1,355 @@ const themePaths = { - "/api/v1/themes/": { - "get": { - "tags": [ - "Themes Module" - ], - "summary": "Retrieves a list of themes.", - "description": "Fetches a paginated list of themes with optional search query.", - "parameters": [ - { - "in": "query", - "name": "pageSize", - "schema": { - "type": "integer", - "default": 30 - }, - "required": false, - "description": "The number of themes to retrieve per page." - }, - { - "in": "query", - "name": "pageNum", - "schema": { - "type": "integer", - "default": 1 - }, - "required": false, - "description": "The page number to retrieve." - }, - { - "in": "query", - "name": "searchQuery", - "schema": { - "type": "string" - }, - "required": false, - "description": "A search query to filter themes by name or description." - } - ], - "responses": { - "200": { - "description": "A list of themes retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the theme." - }, - "description": { - "type": "string", - "description": "A brief description of the theme." - }, - "author": { - "type": "string", - "description": "The author of the theme." - }, - "github": { - "type": "string", - "description": "The GitHub repository link for the theme." - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Tags associated with the theme." - }, - "version": { - "type": "string", - "description": "The current version of the theme." - } - } - } - } - } - } - }, - "500": { - "description": "Internal server error occurred while fetching themes.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message" - } - } - } - } - } - } - } - } - }, - "/api/v1/themes/versions": { - "get": { - "tags": [ - "Themes Module" - ], - "summary": "Retrieves theme versions.", - "description": "Fetches all published versions for a specific theme.", - "parameters": [ - { - "in": "query", - "name": "themeId", - "schema": { - "type": "string" - }, - "required": true, - "description": "The ID of the theme for which versions are to be retrieved." - } - ], - "responses": { - "200": { - "description": "A list of theme versions retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The unique identifier for this theme version." - }, - "theme_id": { - "type": "string", - "description": "The ID of the theme this version belongs to." - }, - "version": { - "type": "string", - "description": "The version number of the theme." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "The date when this version was released." - } - } - } - } - } - } - }, - "500": { - "description": "Internal server error occurred while fetching theme versions.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message" - } - } - } - } - } - } - } - } - }, - "/api/v1/themes/publish": { - "post": { - "tags": [ - "Themes Module" - ], - "summary": "Publishes a new theme.", - "description": "Publishes a new theme or updates an existing one. Handles versioning and validation.", - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "styles": { - "type": "string", - "format": "binary", - "description": "CSS file for the theme." - }, - "options": { - "type": "string", - "format": "binary", - "description": "JSON file containing theme options." - }, - "display": { - "type": "string", - "format": "binary", - "description": "PNG file for the theme display image." - } - } - }, - "application/json": { - "schema": { - "type": "object", - "properties": { - "theme_id": { - "type": "string", - "description": "The ID of the theme being published." - }, - "name": { - "type": "string", - "description": "The name of the theme." - }, - "description": { - "type": "string", - "description": "A brief description of the theme." - }, - "version": { - "type": "string", - "description": "The version of the theme being published." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Theme published successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Success message." - } - } - } - } - } - }, - "400": { - "description": "Bad request due to validation failure.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Validation error message." - } - } - } - } - } - }, - "500": { - "description": "Internal server error occurred while publishing the theme.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message" - } - } - } - } - } - } - } - } - } - }, - "/api/v1/themes/unpublish": { - "delete": { - "tags": [ - "Themes Module" - ], - "summary": "Unpublishes an existing theme.", - "description": "Removes a theme from publication.", - "parameters": [ - { - "in": "query", - "name": "theme_id", - "schema": { - "type": "string" - }, - "required": true, - "description": "The ID of the theme to unpublish." - } - ], - "responses": { - "200": { - "description": "Theme unpublished successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Success message." - } - } - } - } - }, - "400": { - "description": "Feature not allowed or bad request.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message describing why the operation failed." - } - } - } - } - } - }, - "500": { - "description": "Internal server error occurred while unpublishing the theme.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message" - } - } - } - } - } - } - } - } - } - } -} + "/api/v1/themes/": { + get: { + tags: ["Themes Module"], + summary: "Retrieves a list of themes.", + description: + "Fetches a paginated list of themes with optional search query.", + parameters: [ + { + in: "query", + name: "pageSize", + schema: { + type: "integer", + default: 30, + }, + required: false, + description: "The number of themes to retrieve per page.", + }, + { + in: "query", + name: "pageNum", + schema: { + type: "integer", + default: 1, + }, + required: false, + description: "The page number to retrieve.", + }, + { + in: "query", + name: "searchQuery", + schema: { + type: "string", + }, + required: false, + description: + "A search query to filter themes by name or description.", + }, + ], + responses: { + 200: { + description: "A list of themes retrieved successfully.", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + description: "The name of the theme.", + }, + description: { + type: "string", + description: "A brief description of the theme.", + }, + author: { + type: "string", + description: "The author of the theme.", + }, + github: { + type: "string", + description: "The GitHub repository link for the theme.", + }, + tags: { + type: "array", + items: { + type: "string", + }, + description: "Tags associated with the theme.", + }, + version: { + type: "string", + description: "The current version of the theme.", + }, + }, + }, + }, + }, + }, + }, + 500: { + description: "Internal server error occurred while fetching themes.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: "Error message", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "/api/v1/themes/versions": { + get: { + tags: ["Themes Module"], + summary: "Retrieves theme versions.", + description: "Fetches all published versions for a specific theme.", + parameters: [ + { + in: "query", + name: "themeId", + schema: { + type: "string", + }, + required: true, + description: + "The ID of the theme for which versions are to be retrieved.", + }, + ], + responses: { + 200: { + description: "A list of theme versions retrieved successfully.", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + description: + "The unique identifier for this theme version.", + }, + theme_id: { + type: "string", + description: + "The ID of the theme this version belongs to.", + }, + version: { + type: "string", + description: "The version number of the theme.", + }, + created_at: { + type: "string", + format: "date-time", + description: "The date when this version was released.", + }, + }, + }, + }, + }, + }, + }, + 500: { + description: + "Internal server error occurred while fetching theme versions.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: "Error message", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "/api/v1/themes/publish": { + post: { + tags: ["Themes Module"], + summary: "Publishes a new theme.", + description: + "Publishes a new theme or updates an existing one. Handles versioning and validation.", + requestBody: { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + styles: { + type: "string", + format: "binary", + description: "CSS file for the theme.", + }, + options: { + type: "string", + format: "binary", + description: "JSON file containing theme options.", + }, + display: { + type: "string", + format: "binary", + description: "PNG file for the theme display image.", + }, + }, + }, + "application/json": { + schema: { + type: "object", + properties: { + theme_id: { + type: "string", + description: "The ID of the theme being published.", + }, + name: { + type: "string", + description: "The name of the theme.", + }, + description: { + type: "string", + description: "A brief description of the theme.", + }, + version: { + type: "string", + description: "The version of the theme being published.", + }, + }, + }, + }, + }, + }, + responses: { + 201: { + description: "Theme published successfully.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + description: "Success message.", + }, + }, + }, + }, + }, + }, + 400: { + description: "Bad request due to validation failure.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: "Validation error message.", + }, + }, + }, + }, + }, + }, + 500: { + description: + "Internal server error occurred while publishing the theme.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: "Error message", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "/api/v1/themes/unpublish": { + delete: { + tags: ["Themes Module"], + summary: "Unpublishes an existing theme.", + description: "Removes a theme from publication.", + parameters: [ + { + in: "query", + name: "theme_id", + schema: { + type: "string", + }, + required: true, + description: "The ID of the theme to unpublish.", + }, + ], + responses: { + 200: { + description: "Theme unpublished successfully.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + description: "Success message.", + }, + }, + }, + }, + }, + 400: { + description: "Feature not allowed or bad request.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message describing why the operation failed.", + }, + }, + }, + }, + }, + }, + 500: { + description: + "Internal server error occurred while unpublishing the theme.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: "Error message", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; -export default themePaths; \ No newline at end of file +export default themePaths; diff --git a/src/api/swagger/user.js b/src/api/swagger/user.js index cbe6edd..c375624 100644 --- a/src/api/swagger/user.js +++ b/src/api/swagger/user.js @@ -1,393 +1,402 @@ const userPaths = { - "/api/v1/users/profile": { - "get": { - "tags": [ - "Users Module" - ], - "summary": "Retrieves the user profile information.", - "description": "Fetches the user's profile data if the user is authorized.", - "parameters": [ - { - "in": "query", - "name": "userId", - "schema": { - "type": "string" - }, - "required": false, - "description": "The ID of the user whose profile is being requested." - } - ], - "responses": { - "200": { - "description": "User profile data retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The user's unique ID." - }, - "role": { - "type": "string", - "description": "The user's role." - }, - "name": { - "type": "string", - "description": "The user's name." - }, - "email": { - "type": "string", - "description": "The user's email address." - }, - "handle": { - "type": "string", - "description": "The user's handle or username on the provider platform." - }, - "avatar_url": { - "type": "string", - "description": "The URL of the user's avatar image." - }, - "status": { - "type": "string", - "description": "The user's status, if any." - }, - "location": { - "type": "string", - "description": "The user's location." - }, - "profile_url": { - "type": "string", - "description": "The URL of the user's profile." - }, - "provider": { - "type": "string", - "description": "The OAuth provider used (e.g., GitHub)." - }, - "provider_user_id": { - "type": "string", - "description": "The user's ID as provided by the OAuth provider." - } - } - } - } - } - }, - "403": { - "description": "Unauthorized access.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating unauthorized access." - } - } - } - } - } - } - } - } - }, - "/api/v1/users/themes": { - "get": { - "tags": [ - "Users Module" - ], - "summary": "Retrieves themes belonging to the user.", - "description": "Fetches the list of themes that belong to the user if the user is authorized.", - "parameters": [ - { - "in": "query", - "name": "userId", - "schema": { - "type": "string" - }, - "required": false, - "description": "The ID of the user whose themes are being requested." - } - ], - "responses": { - "200": { - "description": "User's themes retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The unique ID of the theme." - }, - "name": { - "type": "string", - "description": "The name of the theme." - }, - "description": { - "type": "string", - "description": "A brief description of the theme." - }, - "version": { - "type": "string", - "description": "The current version of the theme." - } - } - } - } - } - } - }, - "403": { - "description": "Unauthorized access.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating unauthorized access." - } - } - } - } - } - } - } - } - }, - "/api/v1/users/themes/favorited": { - "get": { - "tags": [ - "Users Module" - ], - "summary": "Retrieves themes favorited by the user.", - "description": "Fetches the list of themes favorited by the user if the user is authorized.", - "parameters": [ - { - "in": "query", - "name": "userId", - "schema": { - "type": "string" - }, - "required": false, - "description": "The ID of the user whose favorited themes are being requested." - } - ], - "responses": { - "200": { - "description": "User's favorited themes retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The unique ID of the favorited theme." - }, - "name": { - "type": "string", - "description": "The name of the favorited theme." - }, - "description": { - "type": "string", - "description": "A brief description of the favorited theme." - }, - "version": { - "type": "string", - "description": "The current version of the favorited theme." - } - } - } - } - } - } - }, - "403": { - "description": "Unauthorized access.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating unauthorized access." - } - } - } - } - } - } - } - }, - "post": { - "tags": [ - "Users Module" - ], - "summary": "Adds a theme to user's favorites.", - "description": "Adds the specified theme to the user's list of favorited themes.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "theme_id": { - "type": "string", - "description": "The ID of the theme to be added to favorites." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Theme added to favorites successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Success message." - } - } - } - } - } - }, - "404": { - "description": "Theme not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating the theme was not found." - } - } - } - } - } - }, - "400": { - "description": "Theme already favorited.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating the theme is already in the user's favorites." - } - } - } - } - } - }, - "500": { - "description": "Internal server error occurred while adding theme to favorites.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating a server error occurred." - } - } - } - } - } - } - } - }, - "delete": { - "tags": [ - "Users Module" - ], - "summary": "Removes a theme from user's favorites.", - "description": "Removes the specified theme from the user's list of favorited themes.", - "parameters": [ - { - "in": "query", - "name": "theme_id", - "schema": { - "type": "string" - }, - "required": true, - "description": "The ID of the theme to be removed from favorites." - } - ], - "responses": { - "200": { - "description": "Theme removed from favorites successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Success message." - } - } - } - } - } - }, - "404": { - "description": "Favorite theme not found.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating the favorite theme was not found." - } - } - } - } - } - }, - "500": { - "description": "Internal server error occurred while removing theme from favorites.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message indicating a server error occurred." - } - } - } - } - } - } - } - } - } -} + "/api/v1/users/profile": { + get: { + tags: ["Users Module"], + summary: "Retrieves the user profile information.", + description: "Fetches the user's profile data if the user is authorized.", + parameters: [ + { + in: "query", + name: "userId", + schema: { + type: "string", + }, + required: false, + description: "The ID of the user whose profile is being requested.", + }, + ], + responses: { + 200: { + description: "User profile data retrieved successfully.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + id: { + type: "string", + description: "The user's unique ID.", + }, + role: { + type: "string", + description: "The user's role.", + }, + name: { + type: "string", + description: "The user's name.", + }, + email: { + type: "string", + description: "The user's email address.", + }, + handle: { + type: "string", + description: + "The user's handle or username on the provider platform.", + }, + avatar_url: { + type: "string", + description: "The URL of the user's avatar image.", + }, + status: { + type: "string", + description: "The user's status, if any.", + }, + location: { + type: "string", + description: "The user's location.", + }, + profile_url: { + type: "string", + description: "The URL of the user's profile.", + }, + provider: { + type: "string", + description: "The OAuth provider used (e.g., GitHub).", + }, + provider_user_id: { + type: "string", + description: + "The user's ID as provided by the OAuth provider.", + }, + }, + }, + }, + }, + }, + 403: { + description: "Unauthorized access.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating unauthorized access.", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "/api/v1/users/themes": { + get: { + tags: ["Users Module"], + summary: "Retrieves themes belonging to the user.", + description: + "Fetches the list of themes that belong to the user if the user is authorized.", + parameters: [ + { + in: "query", + name: "userId", + schema: { + type: "string", + }, + required: false, + description: "The ID of the user whose themes are being requested.", + }, + ], + responses: { + 200: { + description: "User's themes retrieved successfully.", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + description: "The unique ID of the theme.", + }, + name: { + type: "string", + description: "The name of the theme.", + }, + description: { + type: "string", + description: "A brief description of the theme.", + }, + version: { + type: "string", + description: "The current version of the theme.", + }, + }, + }, + }, + }, + }, + }, + 403: { + description: "Unauthorized access.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating unauthorized access.", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "/api/v1/users/themes/favorited": { + get: { + tags: ["Users Module"], + summary: "Retrieves themes favorited by the user.", + description: + "Fetches the list of themes favorited by the user if the user is authorized.", + parameters: [ + { + in: "query", + name: "userId", + schema: { + type: "string", + }, + required: false, + description: + "The ID of the user whose favorited themes are being requested.", + }, + ], + responses: { + 200: { + description: "User's favorited themes retrieved successfully.", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + description: "The unique ID of the favorited theme.", + }, + name: { + type: "string", + description: "The name of the favorited theme.", + }, + description: { + type: "string", + description: + "A brief description of the favorited theme.", + }, + version: { + type: "string", + description: + "The current version of the favorited theme.", + }, + }, + }, + }, + }, + }, + }, + 403: { + description: "Unauthorized access.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating unauthorized access.", + }, + }, + }, + }, + }, + }, + }, + }, + post: { + tags: ["Users Module"], + summary: "Adds a theme to user's favorites.", + description: + "Adds the specified theme to the user's list of favorited themes.", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + theme_id: { + type: "string", + description: "The ID of the theme to be added to favorites.", + }, + }, + }, + }, + }, + }, + responses: { + 201: { + description: "Theme added to favorites successfully.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + description: "Success message.", + }, + }, + }, + }, + }, + }, + 404: { + description: "Theme not found.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating the theme was not found.", + }, + }, + }, + }, + }, + }, + 400: { + description: "Theme already favorited.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating the theme is already in the user's favorites.", + }, + }, + }, + }, + }, + }, + 500: { + description: + "Internal server error occurred while adding theme to favorites.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating a server error occurred.", + }, + }, + }, + }, + }, + }, + }, + }, + delete: { + tags: ["Users Module"], + summary: "Removes a theme from user's favorites.", + description: + "Removes the specified theme from the user's list of favorited themes.", + parameters: [ + { + in: "query", + name: "theme_id", + schema: { + type: "string", + }, + required: true, + description: "The ID of the theme to be removed from favorites.", + }, + ], + responses: { + 200: { + description: "Theme removed from favorites successfully.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + description: "Success message.", + }, + }, + }, + }, + }, + }, + 404: { + description: "Favorite theme not found.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating the favorite theme was not found.", + }, + }, + }, + }, + }, + }, + 500: { + description: + "Internal server error occurred while removing theme from favorites.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + description: + "Error message indicating a server error occurred.", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; -export default userPaths; \ No newline at end of file +export default userPaths; diff --git a/src/jobs/index.ts b/src/jobs/index.ts index 25e7ee3..d30958f 100644 --- a/src/jobs/index.ts +++ b/src/jobs/index.ts @@ -7,4 +7,4 @@ runSyncThemesFromGitHub(); // todo: ideally a cronjob should be used to only spin up job pod when required // but nvm this is good enough for now setInterval(runProcessThemeQueue, 900000); -setInterval(runSyncThemesFromGitHub, 86400000); \ No newline at end of file +setInterval(runSyncThemesFromGitHub, 86400000); diff --git a/src/jobs/processQueuedThemes.ts b/src/jobs/processQueuedThemes.ts index 853390c..cac2c85 100644 --- a/src/jobs/processQueuedThemes.ts +++ b/src/jobs/processQueuedThemes.ts @@ -2,25 +2,25 @@ * Runs the job for processing theme queue. */ const runProcessThemeQueue = async () => { - // todo: grab all themes from ThemeJob table - const themeJobs = []; + // todo: grab all themes from ThemeJob table + const themeJobs = []; - // todo: de-dupe (i.e. if the same theme is both created and deleted, keep the latest one by timestamp) + // todo: de-dupe (i.e. if the same theme is both created and deleted, keep the latest one by timestamp) - // todo: filter for themes to create - const themesToCreate: string[] = []; - // todo: filter for themes to delete - const themesToDelete: string[] = []; + // todo: filter for themes to create + const themesToCreate: string[] = []; + // todo: filter for themes to delete + const themesToDelete: string[] = []; - const toAdd = await fetchFilesToAdd(themesToCreate); - const toRemove = await fetchFilesToDelete(themesToDelete); + const toAdd = await fetchFilesToAdd(themesToCreate); + const toRemove = await fetchFilesToDelete(themesToDelete); - // todo: use a github application to create pull requests to the themes repository to add/remove files - console.info(toAdd); - console.info(toRemove); + // todo: use a github application to create pull requests to the themes repository to add/remove files + console.info(toAdd); + console.info(toRemove); - // todo: upon successful update of thenes, update their updated_at field within the theme table -} + // todo: upon successful update of thenes, update their updated_at field within the theme table +}; /** * Fetches the files to add. @@ -28,11 +28,11 @@ const runProcessThemeQueue = async () => { * @param themes themes to determine files to add */ const fetchFilesToAdd = async (themes: string[]) => { - // todo: grab all entries from ThemeJob table - // todo: fetch files from minio for each theme in the table - // todo: generate meta.json from name, description etc - // todo: return a list of folders/files to add to the github themes repository (including generated meta.json) -} + // todo: grab all entries from ThemeJob table + // todo: fetch files from minio for each theme in the table + // todo: generate meta.json from name, description etc + // todo: return a list of folders/files to add to the github themes repository (including generated meta.json) +}; /** * Fetches the files to delete. @@ -40,11 +40,9 @@ const fetchFilesToAdd = async (themes: string[]) => { * @param themes themes to determine files to delete */ const fetchFilesToDelete = async (themes: string[]) => { - // todo: grab all entries from ThemeJob table - // todo: fetch files from minio for each theme in the table - // todo: return a list of folders/files to delete in the github themes repository -} - -export { - runProcessThemeQueue + // todo: grab all entries from ThemeJob table + // todo: fetch files from minio for each theme in the table + // todo: return a list of folders/files to delete in the github themes repository }; + +export { runProcessThemeQueue }; diff --git a/src/jobs/syncThemesFromGitHub.ts b/src/jobs/syncThemesFromGitHub.ts index aa7c5b6..689f4f9 100644 --- a/src/jobs/syncThemesFromGitHub.ts +++ b/src/jobs/syncThemesFromGitHub.ts @@ -12,20 +12,24 @@ import { ThemeMetaData } from "../api/interfaces/ThemeMetaData"; * @returns list of theme folder names */ const fetchFolders = async (): Promise => { - const repoOwner = "tjtanjin"; - const repoName = "react-chatbotify-themes"; - const path = "themes"; + const repoOwner = "tjtanjin"; + const repoName = "react-chatbotify-themes"; + const path = "themes"; - try { - const response = await axios.get(`https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}`); - const folders = response.data.filter(item => item.type === "dir").map(item => item.name); + try { + const response = await axios.get( + `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}`, + ); + const folders = response.data + .filter((item) => item.type === "dir") + .map((item) => item.name); - console.info("Fetched folders:", folders); - return folders; - } catch (error) { - console.error("Error fetching folders from GitHub:", error); - return []; - } + console.info("Fetched folders:", folders); + return folders; + } catch (error) { + console.error("Error fetching folders from GitHub:", error); + return []; + } }; /** @@ -35,83 +39,93 @@ const fetchFolders = async (): Promise => { * * @returns theme meta data from contents in meta json file */ -const fetchMetaJson = async (themeName: string): Promise => { - const url = `https://raw.githubusercontent.com/tjtanjin/react-chatbotify-themes/main/themes/${themeName}/meta.json`; - try { - const response = await axios.get(url); - return response.data; - } catch (error) { - console.error(`Error fetching meta.json for theme ${themeName}:`, error); - return null; - } +const fetchMetaJson = async ( + themeName: string, +): Promise => { + const url = `https://raw.githubusercontent.com/tjtanjin/react-chatbotify-themes/main/themes/${themeName}/meta.json`; + try { + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(`Error fetching meta.json for theme ${themeName}:`, error); + return null; + } }; /** * Runs job to sync themes from github into the application. */ const runSyncThemesFromGitHub = async () => { - try { - // fetch all themes in database - const databaseThemes = await Theme.findAll({ - attributes: ["id"] - }); - const databaseThemeIds = databaseThemes.map(theme => theme.dataValues.id); + try { + // fetch all themes in database + const databaseThemes = await Theme.findAll({ + attributes: ["id"], + }); + const databaseThemeIds = databaseThemes.map((theme) => theme.dataValues.id); - // fetch all themes from github - const gitHubThemes = await fetchFolders(); + // fetch all themes from github + const gitHubThemes = await fetchFolders(); - // fetch theme ids that are in theme job to be created - const themeJobs = await ThemeJobQueue.findAll({ - attributes: ["id"] - }); - const themeJobIds = themeJobs.map(job => job.dataValues.id); + // fetch theme ids that are in theme job to be created + const themeJobs = await ThemeJobQueue.findAll({ + attributes: ["id"], + }); + const themeJobIds = themeJobs.map((job) => job.dataValues.id); - // delete themes no longer found on github, but exclude those in theme job - const themesToDelete = databaseThemeIds.filter(id => !gitHubThemes.includes(id) && !themeJobIds.includes(id)); - if (themesToDelete.length > 0) { - await Theme.destroy({ - where: { - id: themesToDelete - } - }); - console.info(`Deleted themes from database: ${themesToDelete}`); - } + // delete themes no longer found on github, but exclude those in theme job + const themesToDelete = databaseThemeIds.filter( + (id) => !gitHubThemes.includes(id) && !themeJobIds.includes(id), + ); + if (themesToDelete.length > 0) { + await Theme.destroy({ + where: { + id: themesToDelete, + }, + }); + console.info(`Deleted themes from database: ${themesToDelete}`); + } - // create new themes found on github, and update versioning table as well - const themesToCreate = gitHubThemes.filter(name => !databaseThemeIds.includes(name)); - for (const themeId of themesToCreate) { - const transaction = await sequelize.transaction(); - try { - const metaData = await fetchMetaJson(themeId); - if (metaData) { - const newTheme = await Theme.create({ - id: themeId, - name: metaData.name, - description: metaData.description, - }, { transaction }); + // create new themes found on github, and update versioning table as well + const themesToCreate = gitHubThemes.filter( + (name) => !databaseThemeIds.includes(name), + ); + for (const themeId of themesToCreate) { + const transaction = await sequelize.transaction(); + try { + const metaData = await fetchMetaJson(themeId); + if (metaData) { + const newTheme = await Theme.create( + { + id: themeId, + name: metaData.name, + description: metaData.description, + }, + { transaction }, + ); - await ThemeVersion.create({ - theme_id: themeId, - version: metaData.version, - created_at: sequelize.literal("NOW()") - }, { transaction }); + await ThemeVersion.create( + { + theme_id: themeId, + version: metaData.version, + created_at: sequelize.literal("NOW()"), + }, + { transaction }, + ); - await transaction.commit(); - console.info(`Created theme and version in database: ${themeId}`); - } else { - throw new Error(`Missing meta.json data for theme: ${themeId}`); - } - } catch (error) { - await transaction.rollback(); - console.error(`Failed to create theme ${themeId}: ${error}`); - } - } - } catch (error) { - console.error("Error fetching themes:", error); - // todo: send an alert on failure since this is critical? - } -} - -export { - runSyncThemesFromGitHub + await transaction.commit(); + console.info(`Created theme and version in database: ${themeId}`); + } else { + throw new Error(`Missing meta.json data for theme: ${themeId}`); + } + } catch (error) { + await transaction.rollback(); + console.error(`Failed to create theme ${themeId}: ${error}`); + } + } + } catch (error) { + console.error("Error fetching themes:", error); + // todo: send an alert on failure since this is critical? + } }; + +export { runSyncThemesFromGitHub }; diff --git a/tsconfig.json b/tsconfig.json index 99729e2..94b12e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,22 +9,12 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "types": [ - "node" - ], - "typeRoots": [ - "./node_modules/@types", - "./types" - ] + "types": ["node"], + "typeRoots": ["./node_modules/@types", "./types"] }, - "include": [ - "src/**/*", - "types" - ], - "exclude": [ - "node_modules" - ], + "include": ["src/**/*", "types"], + "exclude": ["node_modules"], "ts-node": { "files": true - }, -} \ No newline at end of file + } +} diff --git a/types/session.d.ts b/types/session.d.ts index 19f317a..1b54386 100644 --- a/types/session.d.ts +++ b/types/session.d.ts @@ -1,14 +1,14 @@ -import 'express-session'; +import "express-session"; -declare module 'express-session' { - export interface SessionData { - userId: string; - provider: string; - } +declare module "express-session" { + export interface SessionData { + userId: string; + provider: string; + } } -declare module 'express-serve-static-core' { - interface Request { - userData: UserData; - } -} \ No newline at end of file +declare module "express-serve-static-core" { + interface Request { + userData: UserData; + } +} From fe34441ccb79f1908a9e3d07024f6d393971a4f1 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:47:24 +0300 Subject: [PATCH 10/14] patch: prettier fix 2 --- src/api/controllers/themeController.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/controllers/themeController.ts b/src/api/controllers/themeController.ts index 725f607..f1b13d0 100644 --- a/src/api/controllers/themeController.ts +++ b/src/api/controllers/themeController.ts @@ -134,11 +134,9 @@ const unpublishTheme = async (req: Request, res: Response) => { // if theme does not exist, cannot delete if (!theme) { - return res - .status(404) - .json({ - error: "Failed to unpublish theme, the theme does not exist.", - }); + return res.status(404).json({ + error: "Failed to unpublish theme, the theme does not exist.", + }); } // if theme exist and user is admin, can delete From 9b46efff02f2be1737fc433fb7b44297555e137f Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 13:58:27 +0300 Subject: [PATCH 11/14] patch: eslint rules --- eslint.config.mjs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index f75ba7b..ee6aa4a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,11 +1,30 @@ import globals from "globals"; import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; export default [ - { files: ["**/*.{js,mjs,cjs,ts}"] }, - { files: ["**/*.js"], languageOptions: { sourceType: "script" } }, - { languageOptions: { globals: globals.browser } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, -]; + { + files: ["**/*.{js,mjs,cjs,ts}"], + languageOptions: { + parser: tsParser, + sourceType: "module", + globals: globals.browser, + }, + // Include the recommended configurations directly + plugins: { + "@typescript-eslint": tseslint, + }, + rules: { + ...pluginJs.configs.recommended.rules, + ...tseslint.configs.recommended.rules, + "@typescript-eslint/no-explicit-any": "off", // Disable the no-explicit-any rule + }, + }, + { + files: ["**/*.js"], + languageOptions: { + sourceType: "script", + }, + }, +]; \ No newline at end of file From 5cfca06ca3622ade25e2e1af53429569ebca32c6 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 14:00:18 +0300 Subject: [PATCH 12/14] patch: Eslint + Node compatibility fix --- eslint.config.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index ee6aa4a..742091e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,16 +9,18 @@ export default [ languageOptions: { parser: tsParser, sourceType: "module", - globals: globals.browser, + globals: { + ...globals.browser, + ...globals.node, // Include Node.js globals + }, }, - // Include the recommended configurations directly plugins: { "@typescript-eslint": tseslint, }, rules: { ...pluginJs.configs.recommended.rules, ...tseslint.configs.recommended.rules, - "@typescript-eslint/no-explicit-any": "off", // Disable the no-explicit-any rule + "@typescript-eslint/no-explicit-any": "off", }, }, { From aca5562e09c53277769bc82430ab66f5c8c52967 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 14:10:52 +0300 Subject: [PATCH 13/14] patch: fix liter errors --- src/api/controllers/themeController.ts | 3 ++- src/api/controllers/userController.ts | 10 ++++++++-- .../services/authentication/authentication.ts | 3 ++- src/api/services/github/pullRequest.ts | 17 ++++++++++------- src/jobs/processQueuedThemes.ts | 4 +++- src/jobs/syncThemesFromGitHub.ts | 4 ++-- types/session.d.ts | 1 + 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/api/controllers/themeController.ts b/src/api/controllers/themeController.ts index f1b13d0..1ffa709 100644 --- a/src/api/controllers/themeController.ts +++ b/src/api/controllers/themeController.ts @@ -41,6 +41,7 @@ const getThemes = async (req: Request, res: Response) => { res.json(themes); } catch (error) { res.status(500).json({ error: "Failed to fetch themes" }); + console.log(error) } }; @@ -75,7 +76,7 @@ const getThemeVersions = async (req: Request, res: Response) => { */ const publishTheme = async (req: Request, res: Response) => { const userData = req.userData; - const { theme_id, name, description, version } = req.body; + const { theme_id, name, description } = req.body; // todo: perform checks in the following steps: // 1) if theme_id already exist and user is not author, 403 diff --git a/src/api/controllers/userController.ts b/src/api/controllers/userController.ts index 2351fa4..c91a97e 100644 --- a/src/api/controllers/userController.ts +++ b/src/api/controllers/userController.ts @@ -56,7 +56,10 @@ const getUserThemes = async (req: Request, res: Response) => { }, }); return res.json(themes); - } catch {} + } catch (error) { + res.status(500).json({ error: "Failed to fetch themes" }); + console.log(error) + } } // all other cases unauthorized @@ -90,7 +93,10 @@ const getUserFavoriteThemes = async (req: Request, res: Response) => { include: [Theme], }); res.json(userFavoriteThemes); - } catch {} + } catch (error) { + res.status(500).json({ error: "Failed to fetch user favorite themes" }); + console.log(error) + } } // all other cases unauthorized diff --git a/src/api/services/authentication/authentication.ts b/src/api/services/authentication/authentication.ts index d69a714..66d1ff7 100644 --- a/src/api/services/authentication/authentication.ts +++ b/src/api/services/authentication/authentication.ts @@ -41,7 +41,7 @@ const fetchTokensWithCode = async ( encrypt(tokenResponse.access_token), { EX: 27900 }, ); - } catch (error) {} + } catch (error) {console.log(error)} return tokenResponse; }; @@ -170,6 +170,7 @@ const getOrCreateUser = async ( return newUser; } catch (error) { + console.log(error) return null; } }; diff --git a/src/api/services/github/pullRequest.ts b/src/api/services/github/pullRequest.ts index 1f224fc..48e0093 100644 --- a/src/api/services/github/pullRequest.ts +++ b/src/api/services/github/pullRequest.ts @@ -1,3 +1,4 @@ +/* // ignore this entire file for now, unused but will re-look later import axios, { AxiosRequestConfig } from "axios"; import { readFileSync } from "fs"; @@ -81,10 +82,12 @@ async function createPullRequest() { } } -// const branchCreated = await createBranch(); -// if (branchCreated) { -// const fileAdded = await addFile(); -// if (fileAdded) { -// await createPullRequest(); -// } -// } +const branchCreated = await createBranch(); +if (branchCreated) { + const fileAdded = await addFile(); + if (fileAdded) { + await createPullRequest(); + } +} + +*/ \ No newline at end of file diff --git a/src/jobs/processQueuedThemes.ts b/src/jobs/processQueuedThemes.ts index cac2c85..bd2765a 100644 --- a/src/jobs/processQueuedThemes.ts +++ b/src/jobs/processQueuedThemes.ts @@ -3,7 +3,7 @@ */ const runProcessThemeQueue = async () => { // todo: grab all themes from ThemeJob table - const themeJobs = []; + //const themeJobs = []; // todo: de-dupe (i.e. if the same theme is both created and deleted, keep the latest one by timestamp) @@ -28,6 +28,7 @@ const runProcessThemeQueue = async () => { * @param themes themes to determine files to add */ const fetchFilesToAdd = async (themes: string[]) => { + console.log(themes) // todo: grab all entries from ThemeJob table // todo: fetch files from minio for each theme in the table // todo: generate meta.json from name, description etc @@ -40,6 +41,7 @@ const fetchFilesToAdd = async (themes: string[]) => { * @param themes themes to determine files to delete */ const fetchFilesToDelete = async (themes: string[]) => { + console.log(themes) // todo: grab all entries from ThemeJob table // todo: fetch files from minio for each theme in the table // todo: return a list of folders/files to delete in the github themes repository diff --git a/src/jobs/syncThemesFromGitHub.ts b/src/jobs/syncThemesFromGitHub.ts index 689f4f9..29ecee3 100644 --- a/src/jobs/syncThemesFromGitHub.ts +++ b/src/jobs/syncThemesFromGitHub.ts @@ -94,14 +94,14 @@ const runSyncThemesFromGitHub = async () => { try { const metaData = await fetchMetaJson(themeId); if (metaData) { - const newTheme = await Theme.create( + /*const newTheme = await Theme.create( { id: themeId, name: metaData.name, description: metaData.description, }, { transaction }, - ); + );*/ await ThemeVersion.create( { diff --git a/types/session.d.ts b/types/session.d.ts index 1b54386..fcc1976 100644 --- a/types/session.d.ts +++ b/types/session.d.ts @@ -9,6 +9,7 @@ declare module "express-session" { declare module "express-serve-static-core" { interface Request { + // eslint-disable-next-line no-undef userData: UserData; } } From cc54336be343d1bf088fc087c7638c432784caa6 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Sun, 25 Aug 2024 14:12:32 +0300 Subject: [PATCH 14/14] patch: format fix --- eslint.config.mjs | 2 +- src/api/controllers/themeController.ts | 2 +- src/api/controllers/userController.ts | 4 ++-- src/api/services/authentication/authentication.ts | 6 ++++-- src/api/services/github/pullRequest.ts | 2 +- src/jobs/processQueuedThemes.ts | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 742091e..1002f70 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,4 +29,4 @@ export default [ sourceType: "script", }, }, -]; \ No newline at end of file +]; diff --git a/src/api/controllers/themeController.ts b/src/api/controllers/themeController.ts index 1ffa709..ad95af2 100644 --- a/src/api/controllers/themeController.ts +++ b/src/api/controllers/themeController.ts @@ -41,7 +41,7 @@ const getThemes = async (req: Request, res: Response) => { res.json(themes); } catch (error) { res.status(500).json({ error: "Failed to fetch themes" }); - console.log(error) + console.log(error); } }; diff --git a/src/api/controllers/userController.ts b/src/api/controllers/userController.ts index c91a97e..71e9f4e 100644 --- a/src/api/controllers/userController.ts +++ b/src/api/controllers/userController.ts @@ -58,7 +58,7 @@ const getUserThemes = async (req: Request, res: Response) => { return res.json(themes); } catch (error) { res.status(500).json({ error: "Failed to fetch themes" }); - console.log(error) + console.log(error); } } @@ -95,7 +95,7 @@ const getUserFavoriteThemes = async (req: Request, res: Response) => { res.json(userFavoriteThemes); } catch (error) { res.status(500).json({ error: "Failed to fetch user favorite themes" }); - console.log(error) + console.log(error); } } diff --git a/src/api/services/authentication/authentication.ts b/src/api/services/authentication/authentication.ts index 66d1ff7..9c05a01 100644 --- a/src/api/services/authentication/authentication.ts +++ b/src/api/services/authentication/authentication.ts @@ -41,7 +41,9 @@ const fetchTokensWithCode = async ( encrypt(tokenResponse.access_token), { EX: 27900 }, ); - } catch (error) {console.log(error)} + } catch (error) { + console.log(error); + } return tokenResponse; }; @@ -170,7 +172,7 @@ const getOrCreateUser = async ( return newUser; } catch (error) { - console.log(error) + console.log(error); return null; } }; diff --git a/src/api/services/github/pullRequest.ts b/src/api/services/github/pullRequest.ts index 48e0093..98877ab 100644 --- a/src/api/services/github/pullRequest.ts +++ b/src/api/services/github/pullRequest.ts @@ -90,4 +90,4 @@ if (branchCreated) { } } -*/ \ No newline at end of file +*/ diff --git a/src/jobs/processQueuedThemes.ts b/src/jobs/processQueuedThemes.ts index bd2765a..d709c9a 100644 --- a/src/jobs/processQueuedThemes.ts +++ b/src/jobs/processQueuedThemes.ts @@ -28,7 +28,7 @@ const runProcessThemeQueue = async () => { * @param themes themes to determine files to add */ const fetchFilesToAdd = async (themes: string[]) => { - console.log(themes) + console.log(themes); // todo: grab all entries from ThemeJob table // todo: fetch files from minio for each theme in the table // todo: generate meta.json from name, description etc @@ -41,7 +41,7 @@ const fetchFilesToAdd = async (themes: string[]) => { * @param themes themes to determine files to delete */ const fetchFilesToDelete = async (themes: string[]) => { - console.log(themes) + console.log(themes); // todo: grab all entries from ThemeJob table // todo: fetch files from minio for each theme in the table // todo: return a list of folders/files to delete in the github themes repository