diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 10fd9fd62f..273ad1d0f9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -91,6 +91,7 @@ jobs: exit 1 Check-Unauthorized-Changes: + if: ${{ github.actor != 'dependabot[bot]' }} name: Checks if no unauthorized files are changed runs-on: ubuntu-latest steps: @@ -138,6 +139,7 @@ jobs: exit 1 File-count-check: + if: ${{ github.actor != 'dependabot[bot]' }} name: Checks if number of files changed is acceptable runs-on: ubuntu-latest steps: @@ -288,6 +290,7 @@ jobs: min_coverage: 95.0 JSDocs: + if: ${{ github.actor != 'dependabot[bot]' }} name: 'JSDocs comments and pipeline' runs-on: ubuntu-latest needs: Test-Application @@ -303,6 +306,7 @@ jobs: run: echo "Run JSdocs :${{ env.RUN_JSDOCS }}" Branch-check: + if: ${{ github.actor != 'dependabot[bot]' }} name: "Base branch check" runs-on: ubuntu-latest steps: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 90c6dfbcf7..d94f50a3d8 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -26,6 +26,7 @@ jobs: ############################################################################## Push-Workflow: + if: ${{ github.actor != 'dependabot[bot]' }} name: Testing Application runs-on: ubuntu-latest strategy: @@ -54,8 +55,6 @@ jobs: MONGO_DB_URL: mongodb://localhost:27017/talawa-test-db REDIS_HOST: localhost REDIS_PORT: 6379 -# ACCESS_TOKEN_SECRET: ${{ secrets.ACCESS_TOKEN_SECRET }} -# REFRESH_TOKEN_SECRET: ${{ secrets.REFRESH_TOKEN_SECRET }} # We checkout the content of the Talawa-API repository in a directory called `api` steps: @@ -95,6 +94,7 @@ jobs: # You can find the deployment instructions in the scripts/cloud-api-demo/README.md file Deploy-Workflow: + if: ${{ github.actor != 'dependabot[bot]' }} name: Deploying Application to Cloud VPS needs: Push-Workflow runs-on: ubuntu-latest @@ -130,6 +130,7 @@ jobs: python3 /usr/local/bin/scripts/deploy.py --path ~/develop --branch develop Check-Schema: + if: ${{ github.actor != 'dependabot[bot]' }} name: Check Schema runs-on: ubuntu-latest diff --git a/.github/workflows/md_mdx_format_adjuster.py b/.github/workflows/talawa_api_md_mdx_format_adjuster.py similarity index 100% rename from .github/workflows/md_mdx_format_adjuster.py rename to .github/workflows/talawa_api_md_mdx_format_adjuster.py diff --git a/README.md b/README.md index bdf939c79f..ddf41d33ec 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Core features include: `talawa` is based on the original `quito` code created by the [Palisadoes Foundation](http://www.palisadoes.org) as part of its annual Calico Challenge program. Calico provides paid summer internships for Jamaican university students to work on selected open source projects. They are mentored by software professionals and receive stipends based on the completion of predefined milestones. Calico was started in 2015. Visit [The Palisadoes Foundation's website](http://www.palisadoes.org/) for more details on its origin and activities. -## Table of Contents +## Table of Contents diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..14cfc0893f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,139 @@ +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import tsdoc from "eslint-plugin-tsdoc"; +import _import from "eslint-plugin-import"; +import { fixupPluginRules } from "@eslint/compat"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import parser from "@graphql-eslint/eslint-plugin"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; +import graphqlEslint from "@graphql-eslint/eslint-plugin"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +export default [{ + ignores: [ + "**/.github", + "**/.vscode", + "**/build", + "**/coverage", + "**/node_modules", + "src/types", + "docs/Schema.md", + ], +}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"), { + plugins: { + "@typescript-eslint": typescriptEslint, + tsdoc, + "import": fixupPluginRules(_import), + }, + + languageOptions: { + env: { + node: true, + }, + parser: tsParser, +} +, + + rules: { + "no-restricted-imports": ["error", { + patterns: ["**/src/**"], + }], + + "import/no-duplicates": "error", + "tsdoc/syntax": "error", + "@typescript-eslint/ban-ts-comment": "error", + "@typescript-eslint/ban-types": "error", + "@typescript-eslint/no-duplicate-enum-values": "error", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-asserted-optional-chain": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-var-requires": "error", + }, +}, { + files: ["**/*.ts"], + + languageOptions: { + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: ".", + }, + }, + + rules: { + "@typescript-eslint/array-type": "error", + "@typescript-eslint/consistent-type-assertions": "error", + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/explicit-function-return-type": "error", + + "@typescript-eslint/naming-convention": ["error", { + selector: "interface", + format: ["PascalCase"], + prefix: ["Interface", "TestInterface"], + }, { + selector: ["typeAlias", "typeLike", "enum"], + format: ["PascalCase"], + }, { + selector: "typeParameter", + format: ["PascalCase"], + prefix: ["T"], + }, { + selector: "variable", + format: ["camelCase", "UPPER_CASE"], + leadingUnderscore: "allow", + }, { + selector: "parameter", + format: ["camelCase"], + leadingUnderscore: "allow", + }, { + selector: "function", + format: ["camelCase"], + }, { + selector: "memberLike", + modifiers: ["private"], + format: ["camelCase"], + leadingUnderscore: "require", + }, { + selector: "variable", + modifiers: ["exported"], + format: null, + }], + }, +}, { + files: ["./src/typeDefs/**/*.ts"], + processor: "@graphql-eslint/graphql", +}, { + files: ["./src/typeDefs/**/*.graphql"], + + plugins: { + "@graphql-eslint": graphqlEslint, + }, + + languageOptions: { + parser: parser, + }, +}, { + files: ["tests/**/*"], + + rules: { + "no-restricted-imports": "off", + }, +}, { + files: ["./src/index.ts", "./src/utilities/copyToClipboard.ts"], + + rules: { + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-empty-function": "off", + }, +}]; \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index 1769c40537..d989fa282c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -29,6 +29,7 @@ "invalid.fileType": "Invalid file type", "invalid.refreshToken": "Invalid refresh token", "invalid.credentials": "Invalid credentials", + "invalid.timeoutRange": "Invalid timeout range", "registrant.alreadyExist": "Already registered for the event", "member.notFound": "Member not found", "registrant.alreadyUnregistered": "Already unregistered for the event", diff --git a/locales/fr.json b/locales/fr.json index 39fea9082c..4b299485c5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -28,6 +28,7 @@ "invalid.fileType": "Type de fichier non valide", "invalid.refreshToken": "Jeton d'actualisation non valide", "invalid.credentials": "Informations d'identification non valides", + "invalid.timeoutRange": "Plage de temps d'attente non valide", "registrant.alreadyExist": "Déjà inscrit à l'événement", "member.notFound": "Membre introuvable", "registrant.alreadyUnregistered": "Déjà non inscrit à l'événement", diff --git a/locales/hi.json b/locales/hi.json index 74d3372fb0..b21fb5775b 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -29,6 +29,7 @@ "invalid.fileType": "अमान्य फ़ाइल प्रकार", "invalid.refreshToken": "अमान्य रीफ़्रेश टोकन", "invalid.credentials": "अवैध प्रत्यय पत्र", + "invalid.timeoutRange": "अमान्य टाइमआउट रेंज", "registrant.alreadyExist": "घटना के लिए पहले से पंजीकृत", "member.notFound": "सदस्य अनुपस्थित", "registrant.alreadyUnregistered": "घटना के लिए पहले से ही अपंजीकृत", diff --git a/locales/sp.json b/locales/sp.json index cec263813a..e468585977 100644 --- a/locales/sp.json +++ b/locales/sp.json @@ -28,6 +28,7 @@ "invalid.fileType": "Tipo de archivo no válido", "invalid.refreshToken": "Token de actualización no válido", "invalid.credentials": "Credenciales no válidas", + "invalid.timeoutRange": "Rango de tiempo de espera no válido", "registrant.alreadyExist": "Ya inscrito para el evento", "member.notFound": "Miembro no encontrado", "registrant.alreadyUnregistered": "Ya no está registrado para el evento", diff --git a/locales/zh.json b/locales/zh.json index 16453c5236..511a75784a 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -28,6 +28,7 @@ "invalid.fileType": "無效的文件類型", "invalid.refreshToken": "無效的刷新令牌", "invalid.credentials": "無效的憑據", + "invalid.timeoutRange": "无效的超时范围", "registrant.alreadyExist": "已经报名参加活动", "member.notFound": "未找到成员", "registrant.alreadyUnregistered": "已取消注册该活动", diff --git a/package-lock.json b/package-lock.json index f30fe8deee..9882cd9f78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,9 @@ "zod-error": "^1.5.0" }, "devDependencies": { + "@eslint/compat": "^1.1.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "^8.57.0", "@graphql-codegen/cli": "^5.0.2", "@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript-resolvers": "^4.2.1", @@ -1118,6 +1121,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", @@ -1415,6 +1427,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", @@ -2036,15 +2056,25 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/compat": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.1.1.tgz", + "integrity": "sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -2052,7 +2082,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2062,20 +2092,51 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "type-fest": "^0.20.2" + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2085,6 +2146,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2092,23 +2154,13 @@ "node": "*" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@faker-js/faker": { @@ -6594,9 +6646,10 @@ } }, "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==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -8256,7 +8309,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -9041,6 +9094,37 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -10072,14 +10156,6 @@ "node": "*" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -11870,9 +11946,9 @@ } }, "node_modules/lint-staged/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -11937,9 +12013,9 @@ } }, "node_modules/lint-staged/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "node_modules/lint-staged/node_modules/is-fullwidth-code-point": { @@ -12902,11 +12978,6 @@ } } }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -12971,9 +13042,9 @@ } }, "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==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mustache": { "version": "4.2.0", @@ -14943,11 +15014,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/sentence-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", diff --git a/package.json b/package.json index 67e0b99804..70eded2fa8 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,9 @@ "zod-error": "^1.5.0" }, "devDependencies": { + "@eslint/compat": "^1.1.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "^8.57.0", "@graphql-codegen/cli": "^5.0.2", "@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript-resolvers": "^4.2.1", diff --git a/schema.graphql b/schema.graphql index c5fb18a0dd..76c86b291f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -239,6 +239,7 @@ type Community { logoUrl: String name: String! socialMediaUrls: SocialMediaUrls + timeout: Int websiteLink: String } @@ -1192,6 +1193,7 @@ type Mutation { updateOrganization(data: UpdateOrganizationInput, file: String, id: ID!): Organization! updatePluginStatus(id: ID!, orgId: ID!): Plugin! updatePost(data: PostUpdateInput, id: ID!): Post! + updateSessionTimeout(timeout: Int!): Boolean! updateUserPassword(data: UpdateUserPasswordInput!): UserData! updateUserProfile(data: UpdateUserInput, file: String): User! updateUserRoleInOrganization(organizationId: ID!, role: String!, userId: ID!): Organization! diff --git a/src/constants.ts b/src/constants.ts index fab3f2b12d..dcb04c3a95 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -43,6 +43,13 @@ export const AGENDA_CATEGORY_NOT_FOUND_ERROR = Object.freeze({ PARAM: "agendaCategory", }); +export const APP_USER_PROFILE_NOT_FOUND_ERROR = Object.freeze({ + DESC: "appUserProfile not found", + CODE: "appUserProfile.notFound", + MESSAGE: "appUserProfile.notFound", + PARAM: "appUserProfile", +}); + export const BASE_RECURRING_EVENT_NOT_FOUND = Object.freeze({ DESC: "Base Recurring Event not found", CODE: "baseRecurringEvent.notFound", @@ -57,6 +64,13 @@ export const CHAT_NOT_FOUND_ERROR = Object.freeze({ PARAM: "chat", }); +export const COMMUNITY_NOT_FOUND_ERROR = Object.freeze({ + DESC: "Community not found", + CODE: "community.notFound", + MESSAGE: "community.notFound", + PARAM: "community", +}); + export const VENUE_ALREADY_EXISTS_ERROR = Object.freeze({ DESC: "Venue already exists", CODE: "venue.alreadyExists", @@ -144,6 +158,14 @@ export const FUND_NOT_FOUND_ERROR = Object.freeze({ export const INVALID_OTP = "Invalid OTP"; export const IN_PRODUCTION = process.env.NODE_ENV === "production"; + +export const INVALID_TIMEOUT_RANGE = Object.freeze({ + DESC: "Timeout should be in the range of 15 to 60 minutes.", + CODE: "invalid.timeoutRange", + MESSAGE: "invalid.timeoutRange", + PARAM: "timeout", +}); + export const MEMBER_NOT_FOUND_ERROR = Object.freeze({ DESC: "Member not found", CODE: "member.notFound", @@ -696,6 +718,9 @@ export const PRELOGIN_IMAGERY_FIELD_EMPTY = Object.freeze({ PARAM: "preLoginImagery.empty", }); +export const MINIMUM_TIMEOUT_MINUTES = 15; +export const MAXIMUM_TIMEOUT_MINUTES = 60; + export const MAXIMUM_FETCH_LIMIT = 100; export const MAXIMUM_IMAGE_SIZE_LIMIT_KB = 20000; diff --git a/src/models/Community.ts b/src/models/Community.ts index 329254aae7..2301ed2e2a 100644 --- a/src/models/Community.ts +++ b/src/models/Community.ts @@ -19,6 +19,7 @@ export interface InterfaceCommunity { slack: string; reddit: string; }; // Object containing various social media URLs for the community. + timeout: number; } /** @@ -35,6 +36,9 @@ export interface InterfaceCommunity { * @param youTube - YouTube URL. * @param slack - Slack URL. * @param reddit - Reddit URL. + * @param websiteLink - Community website URL. + * @param name - Community name. + * @param timeout - Timeout duration in minutes (default is 30 minutes). */ const communitySchema = new Schema({ name: { @@ -73,6 +77,12 @@ const communitySchema = new Schema({ type: String, }, }, + timeout: { + type: Number, + default: 30, + min: [15, "Timeout should be at least 15 minutes."], + max: [60, "Timeout should not exceed 60 minutes."], + }, }); /** diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 390dea99dd..353670e350 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -114,6 +114,7 @@ import { updateLanguage } from "./updateLanguage"; import { updateOrganization } from "./updateOrganization"; import { updatePluginStatus } from "./updatePluginStatus"; import { updatePost } from "./updatePost"; +import { updateSessionTimeout } from "./updateSessionTimeout"; import { updateUserPassword } from "./updateUserPassword"; import { updateUserProfile } from "./updateUserProfile"; import { updateUserRoleInOrganization } from "./updateUserRoleInOrganization"; @@ -236,6 +237,7 @@ export const Mutation: MutationResolvers = { updateLanguage, updateOrganization, updatePluginStatus, + updateSessionTimeout, updateUserProfile, updateUserPassword, updateUserTag, diff --git a/src/resolvers/Mutation/login.ts b/src/resolvers/Mutation/login.ts index cb825c961d..22a7d9ecc3 100644 --- a/src/resolvers/Mutation/login.ts +++ b/src/resolvers/Mutation/login.ts @@ -14,7 +14,7 @@ import { createRefreshToken, } from "../../utilities"; /** - * This function enables login. + * This function enables login. (note: only works when using the last resort SuperAdmin credentials) * @param _parent - parent of current request * @param args - payload provided with the request * @remarks The following checks are done: @@ -55,29 +55,43 @@ export const login: MutationResolvers["login"] = async (_parent, args) => { } let appUserProfile: InterfaceAppUserProfile | null = await AppUserProfile.findOne({ - userId: user._id.toString(), + userId: user._id, appLanguageCode: "en", tokenVersion: 0, }).lean(); if (!appUserProfile) { appUserProfile = await AppUserProfile.create({ - userId: user._id.toString(), + userId: user._id, appLanguageCode: "en", tokenVersion: 0, isSuperAdmin: false, }); - await User.updateOne( + + user = await User.findOneAndUpdate( { - _id: user._id.toString(), + _id: user._id, }, { - appUserProfileId: appUserProfile?._id?.toString(), + appUserProfileId: appUserProfile?._id, }, + { new: true, lean: true }, ); + + // user = await User.findOne({ + // email: args.data.email.toLowerCase(), + // }).lean(); + + if (!user) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } } - const accessToken = createAccessToken( + const accessToken = await createAccessToken( user, appUserProfile as InterfaceAppUserProfile, ); @@ -104,7 +118,7 @@ export const login: MutationResolvers["login"] = async (_parent, args) => { // ); await AppUserProfile.findOneAndUpdate( { - user: user._id, + _id: user.appUserProfileId, }, { isSuperAdmin: true, diff --git a/src/resolvers/Mutation/updateSessionTimeout.ts b/src/resolvers/Mutation/updateSessionTimeout.ts new file mode 100644 index 0000000000..cf3a840011 --- /dev/null +++ b/src/resolvers/Mutation/updateSessionTimeout.ts @@ -0,0 +1,88 @@ +import type { InterfaceAppUserProfile } from "../../models"; +import { User, AppUserProfile } from "../../models"; +import { + COMMUNITY_NOT_FOUND_ERROR, + INVALID_TIMEOUT_RANGE, + USER_NOT_FOUND_ERROR, + APP_USER_PROFILE_NOT_FOUND_ERROR, + MINIMUM_TIMEOUT_MINUTES, + MAXIMUM_TIMEOUT_MINUTES, +} from "../../constants"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import { superAdminCheck } from "../../utilities"; +import { Community } from "../../models/Community"; + +/** + * This function updates the session timeout and can only be performed by superadmin users. + * @param _parent - parent of the current request + * @param args - payload provided with the request, including organizationId and timeout + * @param context - context of the entire application, containing user information + * @returns - A message true if the organization timeout is updated successfully + * @throws - NotFoundError: If the user, appuserprofile or organization is not found + * @throws - ValidationError: If the user is not an admin or superadmin, or if the timeout is outside the valid range + * @throws - InternalServerError: If there is an error updating the organization timeout + */ + +export const updateSessionTimeout: MutationResolvers["updateSessionTimeout"] = + async (_parent, args, context) => { + const userId = context.userId; + const user = await User.findById(userId).lean(); + + if (!user) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + + //const appuserprofile: InterfaceAppUserProfile | null = await AppUserProfile.findOne({userId: userId}).lean(); + const appuserprofile: InterfaceAppUserProfile | null = + await AppUserProfile.findById(user.appUserProfileId).lean(); //more appropriate since it shows the link between the user and the userprofile + + if (!appuserprofile) { + throw new errors.NotFoundError( + requestContext.translate(APP_USER_PROFILE_NOT_FOUND_ERROR.MESSAGE), + APP_USER_PROFILE_NOT_FOUND_ERROR.CODE, + APP_USER_PROFILE_NOT_FOUND_ERROR.PARAM, + ); + } + + superAdminCheck(appuserprofile); + + const community = await Community.findOne().lean(); + + if (!community) { + throw new errors.NotFoundError( + requestContext.translate(COMMUNITY_NOT_FOUND_ERROR.MESSAGE), + COMMUNITY_NOT_FOUND_ERROR.CODE, + COMMUNITY_NOT_FOUND_ERROR.PARAM, + ); + } + + if ( + args.timeout < MINIMUM_TIMEOUT_MINUTES || + args.timeout > MAXIMUM_TIMEOUT_MINUTES || + args.timeout % 5 !== 0 + ) { + throw new errors.ValidationError( + [ + { + message: requestContext.translate(INVALID_TIMEOUT_RANGE.MESSAGE), + code: INVALID_TIMEOUT_RANGE.CODE, + param: INVALID_TIMEOUT_RANGE.PARAM, + }, + ], + INVALID_TIMEOUT_RANGE.MESSAGE, + ); + } + + await Community.findByIdAndUpdate( + community._id, + { timeout: args.timeout }, + { new: true }, + ); + + return true; + }; diff --git a/src/setup/superAdmin.ts b/src/setup/superAdmin.ts index aeef732b1f..2d62240aba 100644 --- a/src/setup/superAdmin.ts +++ b/src/setup/superAdmin.ts @@ -17,7 +17,7 @@ export async function askForSuperAdminEmail(): Promise { name: "email", message: "Enter the email which you wish to assign as the Super Admin of last resort :", - validate: (input: string) => + validate: (input: string): boolean | string => isValidEmail(input) || "Invalid email. Please try again.", }, ]); diff --git a/src/typeDefs/mutations.ts b/src/typeDefs/mutations.ts index bca74770e5..6bb217447f 100644 --- a/src/typeDefs/mutations.ts +++ b/src/typeDefs/mutations.ts @@ -337,6 +337,8 @@ export const mutations = gql` updatePluginStatus(id: ID!, orgId: ID!): Plugin! + updateSessionTimeout(timeout: Int!): Boolean! @auth + updateUserTag(input: UpdateUserTagInput!): UserTag @auth updateUserProfile(data: UpdateUserInput, file: String): User! @auth diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index 8faf1a6521..c0be08e183 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -130,6 +130,7 @@ export const types = gql` logoUrl: String websiteLink: String socialMediaUrls: SocialMediaUrls + timeout: Int } type CreateAdminPayload { user: AppUserProfile diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index 0f4178071b..2e83523f49 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -318,6 +318,7 @@ export type Community = { logoUrl?: Maybe; name: Scalars['String']['output']; socialMediaUrls?: Maybe; + timeout?: Maybe; websiteLink?: Maybe; }; @@ -1283,6 +1284,7 @@ export type Mutation = { updateOrganization: Organization; updatePluginStatus: Plugin; updatePost: Post; + updateSessionTimeout: Scalars['Boolean']['output']; updateUserPassword: UserData; updateUserProfile: User; updateUserRoleInOrganization: Organization; @@ -1918,6 +1920,11 @@ export type MutationUpdatePostArgs = { }; +export type MutationUpdateSessionTimeoutArgs = { + timeout: Scalars['Int']['input']; +}; + + export type MutationUpdateUserPasswordArgs = { data: UpdateUserPasswordInput; }; @@ -3911,6 +3918,7 @@ export type CommunityResolvers, ParentType, ContextType>; name?: Resolver; socialMediaUrls?: Resolver, ParentType, ContextType>; + timeout?: Resolver, ParentType, ContextType>; websiteLink?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4428,6 +4436,7 @@ export type MutationResolvers>; updatePluginStatus?: Resolver>; updatePost?: Resolver>; + updateSessionTimeout?: Resolver>; updateUserPassword?: Resolver>; updateUserProfile?: Resolver>; updateUserRoleInOrganization?: Resolver>; diff --git a/src/utilities/auth.ts b/src/utilities/auth.ts index 84389ac1f4..9df4712c76 100644 --- a/src/utilities/auth.ts +++ b/src/utilities/auth.ts @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; import { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } from "../constants"; import type { InterfaceAppUserProfile, InterfaceUser } from "../models"; -import { User } from "../models"; +import { Community, User } from "../models"; /** * Interface representing the payload of a JWT token. @@ -22,10 +22,17 @@ export interface InterfaceJwtTokenPayload { * @param appUserProfile - Application user profile data * @returns JSON Web Token string payload */ -export const createAccessToken = ( +export const createAccessToken = async ( user: InterfaceUser, appUserProfile: InterfaceAppUserProfile, -): string => { +): Promise => { + let timeout = 30; //in minutes + const community = await Community.findOne().lean(); + + if (community) { + timeout = community.timeout; + } + return jwt.sign( { tokenVersion: appUserProfile.tokenVersion, @@ -33,6 +40,7 @@ export const createAccessToken = ( firstName: user.firstName, lastName: user.lastName, email: user.email, + timeout: timeout, }, ACCESS_TOKEN_SECRET as string, { diff --git a/tests/resolvers/Mutation/UpdateSessionTimeout.spec.ts b/tests/resolvers/Mutation/UpdateSessionTimeout.spec.ts new file mode 100644 index 0000000000..ab3aead5bb --- /dev/null +++ b/tests/resolvers/Mutation/UpdateSessionTimeout.spec.ts @@ -0,0 +1,232 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; + +import { User, AppUserProfile, Community } from "../../../src/models"; +import type { MutationUpdateSessionTimeoutArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; + +import { nanoid } from "nanoid"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { + COMMUNITY_NOT_FOUND_ERROR, + INVALID_TIMEOUT_RANGE, + USER_NOT_FOUND_ERROR, + APP_USER_PROFILE_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_SUPERADMIN, +} from "../../../src/constants"; +import { updateSessionTimeout as updateSessionTimeoutResolver } from "../../../src/resolvers/Mutation/updateSessionTimeout"; +import type { + TestAppUserProfileType, + TestUserType, +} from "../../helpers/userAndOrg"; + +import { requestContext } from "../../../src/libraries"; + +import bcrypt from "bcryptjs"; +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testAppUserProfile: TestAppUserProfileType; + +vi.mock("../../utilities/uploadEncodedImage", () => ({ + uploadEncodedImage: vi.fn(), +})); + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +beforeEach(async () => { + const hashedPassword = await bcrypt.hash("password", 12); + + testUser = await User.create({ + email: `email${nanoid().toLowerCase()}@gmail.com`, + password: hashedPassword, + firstName: "firstName", + lastName: "lastName", + }); + + testAppUserProfile = await AppUserProfile.create({ + userId: testUser._id, + appLanguageCode: "en", + tokenVersion: 0, + isSuperAdmin: true, + }); + + await User.updateOne( + { + _id: testUser._id.toString(), + }, + { + appUserProfileId: testAppUserProfile?._id?.toString(), + }, + ); + + await Community.create({ + name: "test community", + timeout: 25, + }); +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.doUnmock("../../../src/constants"); + vi.resetModules(); +}); + +describe("resolvers -> Mutation -> updateSessionTimeout", () => { + it("throws NotFoundError if community does not exist", async () => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + const args: MutationUpdateSessionTimeoutArgs = { + timeout: 15, + }; + + const context = { + userId: testUser?._id, + }; + + await Community.deleteMany({}); + + try { + await updateSessionTimeoutResolver?.({}, args, context); + } catch (error: unknown) { + expect(spy).toHaveBeenCalledWith(COMMUNITY_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${COMMUNITY_NOT_FOUND_ERROR.MESSAGE}`, + ); + } + }); + + it("throws NotFoundError if user does not exist", async () => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + try { + const args: MutationUpdateSessionTimeoutArgs = { + timeout: 15, + }; + + const context = { + userId: new Types.ObjectId().toString(), + }; + + await updateSessionTimeoutResolver?.({}, args, context); + } catch (error: unknown) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, + ); + } + }); + + it("throws NotFoundError if appUserProfile does not exist", async () => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + const args: MutationUpdateSessionTimeoutArgs = { + timeout: 15, + }; + + const context = { + userId: testUser?._id, + }; + + await AppUserProfile.deleteOne({ userId: testUser?._id }); + + try { + await updateSessionTimeoutResolver?.({}, args, context); + } catch (error: unknown) { + expect(spy).toHaveBeenCalledWith( + APP_USER_PROFILE_NOT_FOUND_ERROR.MESSAGE, + ); + expect((error as Error).message).toEqual( + `Translated ${APP_USER_PROFILE_NOT_FOUND_ERROR.MESSAGE}`, + ); + } + }); + + it("throws ValidationError if timeout is out of range", async () => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + + const args: MutationUpdateSessionTimeoutArgs = { + timeout: 3, + }; + + const context = { + userId: testUser?._id, + }; + + try { + await updateSessionTimeoutResolver?.({}, args, context); + } catch (error: unknown) { + expect(spy).toHaveBeenCalledWith(INVALID_TIMEOUT_RANGE.MESSAGE); + expect((error as Error).message).toEqual(INVALID_TIMEOUT_RANGE.MESSAGE); + } + }); + + it("throws UnauthorizedError if superAdmin is false", async () => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + + const args: MutationUpdateSessionTimeoutArgs = { + timeout: 15, + }; + + const context = { + userId: testUser?._id, + }; + + AppUserProfile.findByIdAndUpdate( + { + _id: testUser?.appUserProfileId, + }, + { + isSuperAdmin: false, + }, + ); + + try { + await updateSessionTimeoutResolver?.({}, args, context); + } catch (error: unknown) { + expect(spy).toHaveBeenCalledWith(USER_NOT_AUTHORIZED_SUPERADMIN.MESSAGE); + expect((error as Error).message).toEqual( + USER_NOT_AUTHORIZED_SUPERADMIN.MESSAGE, + ); + } + }); + + it("updates session timeout successfully", async () => { + const args: MutationUpdateSessionTimeoutArgs = { + timeout: 15, + }; + + const context = { + userId: testUser?._id, + }; + + const result = await updateSessionTimeoutResolver?.({}, args, context); + + expect(result).toEqual(true); + }); +}); diff --git a/tests/utilities/auth.spec.ts b/tests/utilities/auth.spec.ts index b3c4d84a47..b00263e238 100644 --- a/tests/utilities/auth.spec.ts +++ b/tests/utilities/auth.spec.ts @@ -38,7 +38,7 @@ afterAll(async () => { describe("createAccessToken", () => { it("should create a JWT token with the correct payload", async () => { - const token = createAccessToken( + const token = await createAccessToken( user ? user.toObject() : ({} as InterfaceUser), appUserProfile ? appUserProfile.toObject()