diff --git a/.DS_Store b/.DS_Store index 1ad1b253c..8d8d400ad 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/development_hgn-rest-dev.yml b/.github/workflows/development_hgn-rest-dev.yml index 7d822c869..05030cf70 100644 --- a/.github/workflows/development_hgn-rest-dev.yml +++ b/.github/workflows/development_hgn-rest-dev.yml @@ -1,20 +1,6 @@ # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy # More GitHub Actions for Azure: https://github.com/Azure/actions -# CONFIGURATION -# For help, go to https://github.com/Azure/Actions -# -# 1. Set up the following secrets in your repository: -# AZURE_WEBAPP_PUBLISH_PROFILE -# -# 2. Change these variables for your configuration: - - - - -# For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions -# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples - name: Build and deploy Node.js app to Azure Web App - hgn-rest-dev on: @@ -23,33 +9,53 @@ on: - development workflow_dispatch: -env: - AZURE_WEBAPP_NAME: hgn-rest-dev # set this to your application's name - AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - NODE_VERSION: '14.x' # set this to the node version to use - jobs: - build-and-deploy: - name: Build and Deploy + build: runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build environment: name: 'Production' url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + steps: - - uses: actions/checkout@master - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: npm install, build, and test - run: | - # Build and test the project, then - # deploy to Azure Web App. - npm install - npm run build --if-present - - name: 'Deploy to Azure WebApp' - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D3183F132BC14D79A19DC24953125EA4 }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest-dev' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_37BDFCF15560478F9069CEF1EA105BF0 }} diff --git a/.github/workflows/main_hgn-rest-beta.yml b/.github/workflows/main_hgn-rest-beta.yml index 5db4b0e97..24d316a97 100644 --- a/.github/workflows/main_hgn-rest-beta.yml +++ b/.github/workflows/main_hgn-rest-beta.yml @@ -1,20 +1,6 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy # More GitHub Actions for Azure: https://github.com/Azure/actions -# CONFIGURATION -# For help, go to https://github.com/Azure/Actions -# -# 1. Set up the following secrets in your repository: -# AZURE_WEBAPP_PUBLISH_PROFILE -# -# 2. Change these variables for your configuration: - - - - -# For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions -# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples - name: Build and deploy Node.js app to Azure Web App - hgn-rest-beta on: @@ -23,35 +9,53 @@ on: - main workflow_dispatch: -env: - AZURE_WEBAPP_NAME: hgn-rest-beta # set this to your application's name - AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - NODE_VERSION: '14.x' # set this to the node version to use - jobs: - build-and-deploy: - name: Build and Deploy + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: runs-on: ubuntu-latest + needs: build environment: name: 'Production' url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + steps: - - uses: actions/checkout@master - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: npm install, build, and test - run: | - # Build and test the project, then - # deploy to Azure Web App. - npm install - npm run build --if-present - - name: 'Deploy to Azure WebApp' - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_47800FE52B59410A903D5C41C2F9C10F }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest-beta' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_DB4151C3B2C645B88AD84509DA4DD53D }} diff --git a/.github/workflows/main_hgn-rest.yml b/.github/workflows/main_hgn-rest.yml new file mode 100644 index 000000000..8b6a4298f --- /dev/null +++ b/.github/workflows/main_hgn-rest.yml @@ -0,0 +1,61 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - hgn-rest + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D375D726FC884C3C919531718B394AF9 }} diff --git a/package-lock.json b/package-lock.json index 349c724d0..427a02b40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4797,6 +4797,11 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4923,6 +4928,33 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5076,7 +5108,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colorette": { "version": "2.0.19", @@ -5321,6 +5353,23 @@ } } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5788,7 +5837,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "8.47.0", @@ -7460,7 +7509,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-property-descriptors": { "version": "1.0.0", @@ -10799,7 +10848,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.merge": { "version": "4.6.2", @@ -11511,6 +11560,14 @@ } } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12330,6 +12387,23 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12338,7 +12412,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", diff --git a/package.json b/package.json index 8b88744dc..91e73f39a 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "babel-plugin-module-resolver": "^5.0.0", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", + "cheerio": "^1.0.0-rc.12", "cors": "^2.8.4", "cron": "^1.8.2", "dotenv": "^5.0.1", diff --git a/requirements/emailController/addNonHgnEmailSubscription.md b/requirements/emailController/addNonHgnEmailSubscription.md new file mode 100644 index 000000000..f5748f142 --- /dev/null +++ b/requirements/emailController/addNonHgnEmailSubscription.md @@ -0,0 +1,23 @@ +# Add Non-HGN Email Subscription Function + +## Negative Cases + +1. ❌ **Returns error 400 if `email` field is missing from the request** + - Ensures that the function checks for the presence of the `email` field in the request body and responds with a `400` status code if it's missing. + +2. ❌ **Returns error 400 if the provided `email` already exists in the subscription list** + - This case checks that the function responds with a `400` status code and a message indicating that the email is already subscribed. + +3. ❌ **Returns error 500 if there is an internal error while checking the subscription list** + - Covers scenarios where there's an issue querying the `EmailSubscriptionList` collection for the provided email (e.g., database connection issues). + +4. ❌ **Returns error 500 if there is an error sending the confirmation email** + - This case handles any issues that occur while calling the `emailSender` function, such as network errors or service unavailability. + +## Positive Cases + +1. ❌ **Returns status 200 when a new email is successfully subscribed** + - Ensures that the function successfully creates a JWT token, constructs the email, and sends the subscription confirmation email to the user. + +2. ❌ **Successfully sends a confirmation email containing the correct link** + - Verifies that the generated JWT token is correctly included in the confirmation link sent to the user in the email body. diff --git a/requirements/emailController/confirmNonHgnEmailSubscription.md b/requirements/emailController/confirmNonHgnEmailSubscription.md new file mode 100644 index 000000000..d5e1367af --- /dev/null +++ b/requirements/emailController/confirmNonHgnEmailSubscription.md @@ -0,0 +1,18 @@ +# Confirm Non-HGN Email Subscription Function Tests + +## Negative Cases +1. ✅ **Returns error 400 if `token` field is missing from the request** + - (Test: `should return 400 if token is not provided`) + +2. ✅ **Returns error 401 if the provided `token` is invalid or expired** + - (Test: `should return 401 if token is invalid`) + +3. ✅ **Returns error 400 if the decoded `token` does not contain a valid `email` field** + - (Test: `should return 400 if email is missing from payload`) + +4. ❌ **Returns error 500 if there is an internal error while saving the new email subscription** + +## Positive Cases +1. ❌ **Returns status 200 when a new email is successfully subscribed** + +2. ❌ **Returns status 200 if the email is already subscribed (duplicate email)** diff --git a/requirements/emailController/removeNonHgnEmailSubscription.md b/requirements/emailController/removeNonHgnEmailSubscription.md new file mode 100644 index 000000000..af793e2a9 --- /dev/null +++ b/requirements/emailController/removeNonHgnEmailSubscription.md @@ -0,0 +1,10 @@ +# Remove Non-HGN Email Subscription Function Tests + +## Negative Cases +1. ✅ **Returns error 400 if `email` field is missing from the request** + - (Test: `should return 400 if email is missing`) + +2. ❌ **Returns error 500 if there is an internal error while deleting the email subscription** + +## Positive Cases +1. ❌ **Returns status 200 when an email is successfully unsubscribed** diff --git a/requirements/emailController/sendEmail.md b/requirements/emailController/sendEmail.md new file mode 100644 index 000000000..7ca9a482c --- /dev/null +++ b/requirements/emailController/sendEmail.md @@ -0,0 +1,10 @@ +# Send Email Function + +## Negative Cases + +1. ❌ **Returns error 400 if `to`, `subject`, or `html` fields are missing from the request** +2. ❌ **Returns error 500 if there is an internal error while sending the email** + +## Positive Cases + +1. ✅ **Returns status 200 when email is successfully sent with `to`, `subject`, and `html` fields provided** diff --git a/requirements/emailController/sendEmailToAll.md b/requirements/emailController/sendEmailToAll.md new file mode 100644 index 000000000..32a09fed6 --- /dev/null +++ b/requirements/emailController/sendEmailToAll.md @@ -0,0 +1,26 @@ +# Send Email to All Function + +## Negative Cases + +1. ❌ **Returns error 400 if `subject` or `html` fields are missing from the request** + - The request should be rejected if either the `subject` or `html` content is not provided in the request body. + +2. ❌ **Returns error 500 if there is an internal error while fetching users** + - This case covers scenarios where there's an error fetching users from the `userProfile` collection (e.g., database connection issues). + +3. ❌ **Returns error 500 if there is an internal error while fetching the subscription list** + - This case covers scenarios where there's an error fetching emails from the `EmailSubcriptionList` collection. + +4. ❌ **Returns error 500 if there is an error sending emails** + - This case handles any issues that occur while calling the `emailSender` function, such as network errors or service unavailability. + +## Positive Cases + +1. ❌ **Returns status 200 when emails are successfully sent to all active users** + - Ensures that the function sends emails correctly to all users meeting the criteria (`isActive` and `EmailSubcriptionList`). + +2. ❌ **Returns status 200 when emails are successfully sent to all users in the subscription list** + - Verifies that the function sends emails to all users in the `EmailSubcriptionList`, including the unsubscribe link in the email body. + +3. ❌ **Combines user and subscription list emails successfully** + - Ensures that the function correctly sends emails to both active users and the subscription list without issues. diff --git a/requirements/emailController/updateEmailSubscription.md b/requirements/emailController/updateEmailSubscription.md new file mode 100644 index 000000000..bcafa5a28 --- /dev/null +++ b/requirements/emailController/updateEmailSubscription.md @@ -0,0 +1,20 @@ +# Update Email Subscriptions Function + +## Negative Cases + +1. ❌ **Returns error 400 if `emailSubscriptions` field is missing from the request** + - This ensures that the function checks for the presence of the `emailSubscriptions` field in the request body and responds with a `400` status code if it's missing. + +2. ❌ **Returns error 400 if `email` field is missing from the requestor object** + - Ensures that the function requires an `email` field within the `requestor` object in the request body and returns `400` if it's absent. + +3. ❌ **Returns error 404 if the user with the provided `email` is not found** + - This checks that the function correctly handles cases where no user exists with the given `email` and responds with a `404` status code. + +4. ✅ **Returns error 500 if there is an internal error while updating the user profile** + - Covers scenarios where there's a database error while updating the user's email subscriptions. + +## Positive Cases + +1. ❌ **Returns status 200 and the updated user when email subscriptions are successfully updated** + - Ensures that the function updates the `emailSubscriptions` field for the user and returns the updated user document along with a `200` status code. diff --git a/requirements/popUpEditorController/createPopPopupEditor.md b/requirements/popUpEditorController/createPopPopupEditor.md new file mode 100644 index 000000000..0afcaa740 --- /dev/null +++ b/requirements/popUpEditorController/createPopPopupEditor.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# createPopPopupEditor Function + +> ### Positive case + +> 1. ✅ Should return 201 and the new pop-up editor on success + +> ### Negative case + +> 1. ✅ Should return 403 if user does not have permission to create a pop-up editor +> 2. ✅ Should return 400 if the request body is missing required fields +> 3. ✅ Should return 500 if there is an error saving the new pop-up editor to the database diff --git a/requirements/popUpEditorController/getAllPopupEditors.md b/requirements/popUpEditorController/getAllPopupEditors.md new file mode 100644 index 000000000..f42f93c1a --- /dev/null +++ b/requirements/popUpEditorController/getAllPopupEditors.md @@ -0,0 +1,10 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getAllPopupEditors Function + +> ## Positive case +> 1. ✅ Should return 200 and all pop-up editors on success + +> ## Negative case +> 1. ✅ Should return 404 if there is an error retrieving the pop-up editors from the database diff --git a/requirements/popUpEditorController/getPopupEditorById.md b/requirements/popUpEditorController/getPopupEditorById.md new file mode 100644 index 000000000..013096ed9 --- /dev/null +++ b/requirements/popUpEditorController/getPopupEditorById.md @@ -0,0 +1,10 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getPopupEditorById Function + +> ## Positive case +> 1. ✅ Should return 200 and the pop-up editor on success + +> ## Negative case +> 1. ✅ Should return 404 if the pop-up editor is not found diff --git a/requirements/popUpEditorController/updatePopupEditor.md b/requirements/popUpEditorController/updatePopupEditor.md new file mode 100644 index 000000000..c4e5f5904 --- /dev/null +++ b/requirements/popUpEditorController/updatePopupEditor.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updatePopupEditor Function + +> ## Positive case +> 1. ✅ Should return 200 and the updated pop-up editor on success + + +> ## Negative case +> 1. ✅ Should return 404 if the pop-up editor is not found diff --git a/requirements/teamController/teamController.md b/requirements/teamController/teamController.md new file mode 100644 index 000000000..ad5dd3281 --- /dev/null +++ b/requirements/teamController/teamController.md @@ -0,0 +1,36 @@ + +Check mark: ✅ +Cross Mark: ❌ + +# Team Controller Test Documentation + +## GetAllTeams + +> ### Negative Cases +1. ✅ **Returns 404 - an error occurs during team retrieval.** + +> ### Positive Cases +1. ✅ **Returns 200 - should return all teams sorted by name.** + + +## GetTeamById + +> ### Negative Cases +1. ✅ **Returns 404 - the specified team ID does not exist.** + +> ### Positive Cases +1. ✅ **Returns 200 - all is successful, return a team by ID.** + + +## PostTeam + +> ### Negative Cases +1. ❌ **Returns 403 - the requestor lacks `postTeam` permission.** +2. ❌ **Returns 403 - a team with the same name already exists.** + +> ### Positive Cases +1. ❌ **Returns 200 - a new team is successfully created.** + + + + diff --git a/src/controllers/actionItemController.js b/src/controllers/actionItemController.js index 6d5864213..390794b31 100644 --- a/src/controllers/actionItemController.js +++ b/src/controllers/actionItemController.js @@ -1,128 +1,123 @@ -/** - * Unused legacy code. Commented out to avoid confusion. Will delete in the next cycle. - * Commented by: Shengwei Peng - * Date: 2024-03-22 - */ - -// const mongoose = require('mongoose'); -// const notificationhelper = require('../helpers/notificationhelper')(); - -// const actionItemController = function (ActionItem) { -// const getactionItem = function (req, res) { -// const userid = req.params.userId; -// ActionItem.find({ -// assignedTo: userid, -// }, ('-createdDateTime -__v')) -// .populate('createdBy', 'firstName lastName') -// .then((results) => { -// const actionitems = []; - -// results.forEach((element) => { -// const actionitem = {}; - -// actionitem._id = element._id; -// actionitem.description = element.description; -// actionitem.createdBy = `${element.createdBy.firstName} ${element.createdBy.lastName}`; -// actionitem.assignedTo = element.assignedTo; - -// actionitems.push(actionitem); -// }); - -// res.status(200).send(actionitems); -// }) -// .catch((error) => { -// res.status(400).send(error); -// }); -// }; -// const postactionItem = function (req, res) { -// const { requestorId, assignedTo } = req.body.requestor; -// const _actionItem = new ActionItem(); - -// _actionItem.description = req.body.description; -// _actionItem.assignedTo = req.body.assignedTo; -// _actionItem.createdBy = req.body.requestor.requestorId; - -// _actionItem.save() -// .then((result) => { -// notificationhelper.notificationcreated(requestorId, assignedTo, _actionItem.description); - -// const actionitem = {}; - -// actionitem.createdBy = 'You'; -// actionitem.description = _actionItem.description; -// actionitem._id = result._id; -// actionitem.assignedTo = _actionItem.assignedTo; - -// res.status(200).send(actionitem); -// }) -// .catch((error) => { -// res.status(400).send(error); -// }); -// }; - -// const deleteactionItem = async function (req, res) { -// const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); - -// const _actionItem = await ActionItem.findById(actionItemId) -// .catch((error) => { -// res.status(400).send(error); -// }); - -// if (!_actionItem) { -// res.status(400).send({ -// message: 'No valid records found', -// }); -// return; -// } - -// const { requestorId, assignedTo } = req.body.requestor; - -// notificationhelper.notificationdeleted(requestorId, assignedTo, _actionItem.description); - -// _actionItem.remove() -// .then(() => { -// res.status(200).send({ -// message: 'removed', -// }); -// }) -// .catch((error) => { -// res.status(400).send(error); -// }); -// }; - -// const editactionItem = async function (req, res) { -// const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); - -// const { requestorId, assignedTo } = req.body.requestor; - -// const _actionItem = await ActionItem.findById(actionItemId) -// .catch((error) => { -// res.status(400).send(error); -// }); - -// if (!_actionItem) { -// res.status(400).send({ -// message: 'No valid records found', -// }); -// return; -// } -// notificationhelper.notificationedited(requestorId, assignedTo, _actionItem.description, req.body.description); - -// _actionItem.description = req.body.description; -// _actionItem.assignedTo = req.body.assignedTo; - -// _actionItem.save() -// .then(res.status(200).send('Saved')) -// .catch((error) => res.status(400).send(error)); -// }; - -// return { -// getactionItem, -// postactionItem, -// deleteactionItem, -// editactionItem, - -// }; -// }; - -// module.exports = actionItemController; +const mongoose = require('mongoose'); +const notificationhelper = require('../helpers/notificationhelper')(); + +const actionItemController = function (ActionItem) { + const getactionItem = function (req, res) { + const userid = req.params.userId; + ActionItem.find({ + assignedTo: userid, + }, ('-createdDateTime -__v')) + .populate('createdBy', 'firstName lastName') + .then((results) => { + const actionitems = []; + + results.forEach((element) => { + const actionitem = {}; + + actionitem._id = element._id; + actionitem.description = element.description; + actionitem.createdBy = `${element.createdBy.firstName} ${element.createdBy.lastName}`; + actionitem.assignedTo = element.assignedTo; + + actionitems.push(actionitem); + }); + + res.status(200).send(actionitems); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const postactionItem = function (req, res) { + const { requestorId, assignedTo } = req.body.requestor; + const _actionItem = new ActionItem(); + + _actionItem.description = req.body.description; + _actionItem.assignedTo = req.body.assignedTo; + _actionItem.createdBy = req.body.requestor.requestorId; + + _actionItem.save() + .then((result) => { + notificationhelper.notificationcreated(requestorId, assignedTo, _actionItem.description); + + const actionitem = {}; + + actionitem.createdBy = 'You'; + actionitem.description = _actionItem.description; + actionitem._id = result._id; + actionitem.assignedTo = _actionItem.assignedTo; + + res.status(200).send(actionitem); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const deleteactionItem = async function (req, res) { + const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); + + const _actionItem = await ActionItem.findById(actionItemId) + .catch((error) => { + res.status(400).send(error); + }); + + if (!_actionItem) { + res.status(400).send({ + message: 'No valid records found', + }); + return; + } + + const { requestorId, assignedTo } = req.body.requestor; + + notificationhelper.notificationdeleted(requestorId, assignedTo, _actionItem.description); + + _actionItem.remove() + .then(() => { + res.status(200).send({ + message: 'removed', + }); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const editactionItem = async function (req, res) { + const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); + + const { requestorId, assignedTo } = req.body.requestor; + + const _actionItem = await ActionItem.findById(actionItemId) + .catch((error) => { + res.status(400).send(error); + }); + + if (!_actionItem) { + res.status(400).send({ + message: 'No valid records found', + }); + return; + } + + notificationhelper.notificationedited(requestorId, assignedTo, _actionItem.description, req.body.description); + + _actionItem.description = req.body.description; + _actionItem.assignedTo = req.body.assignedTo; + + _actionItem.save() + .then(res.status(200).send('Saved')) + .catch((error) => res.status(400).send(error)); + }; + + return { + getactionItem, + postactionItem, + deleteactionItem, + editactionItem, + }; +}; + +module.exports = actionItemController; diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 55c661b2c..84d9bccaf 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -19,7 +19,7 @@ const badgeController = function (Badge) { // }; const getAllBadges = async function (req, res) { - console.log(req.body.requestor); // Retain logging from development branch for debugging + // console.log(req.body.requestor); // Retain logging from development branch for debugging // Check if the user has any of the following permissions if ( @@ -29,7 +29,7 @@ const badgeController = function (Badge) { !(await helper.hasPermission(req.body.requestor, 'updateBadges')) && !(await helper.hasPermission(req.body.requestor, 'deleteBadges')) ) { - console.log('in if statement'); // Retain logging from development branch for debugging + // console.log('in if statement'); // Retain logging from development branch for debugging res.status(403).send('You are not authorized to view all badge data.'); return; } @@ -129,12 +129,10 @@ const badgeController = function (Badge) { const combinedEarnedDate = [...grouped[badge].earnedDate, ...item.earnedDate]; const timestampArray = combinedEarnedDate.map((date) => new Date(date).getTime()); timestampArray.sort((a, b) => a - b); - grouped[badge].earnedDate = timestampArray.map((timestamp) => - new Date(timestamp) + grouped[badge].earnedDate = timestampArray.map((timestamp) => new Date(timestamp) .toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' }) .replace(/ /g, '-') - .replace(',', ''), - ); + .replace(',', '')); } } if (existingBadges[badge]) { diff --git a/src/controllers/bmdashboard/bmConsumableController.js b/src/controllers/bmdashboard/bmConsumableController.js index d501c5a47..ee44513ce 100644 --- a/src/controllers/bmdashboard/bmConsumableController.js +++ b/src/controllers/bmdashboard/bmConsumableController.js @@ -80,7 +80,12 @@ const bmConsumableController = function (BuildingConsumable) { const bmPostConsumableUpdateRecord = function (req, res) { const { - quantityUsed, quantityWasted, qtyUsedLogUnit, qtyWastedLogUnit, stockAvailable, consumable, + quantityUsed, + quantityWasted, + qtyUsedLogUnit, + qtyWastedLogUnit, + stockAvailable, + consumable, } = req.body; let unitsUsed = quantityUsed; let unitsWasted = quantityWasted; @@ -91,36 +96,48 @@ const bmConsumableController = function (BuildingConsumable) { if (quantityWasted >= 0 && qtyWastedLogUnit === 'percent') { unitsWasted = (stockAvailable / 100) * quantityWasted; } - if (unitsUsed > stockAvailable || unitsWasted > stockAvailable || (unitsUsed + unitsWasted) > stockAvailable) { - return res.status(500).send({ message: 'Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available.' }); - } if (unitsUsed < 0 || unitsWasted < 0) { - return res.status(500).send({ message: 'Please check the used and wasted stock values. Negative numbers are invalid.' }); + if ( + unitsUsed > stockAvailable || + unitsWasted > stockAvailable || + unitsUsed + unitsWasted > stockAvailable + ) { + return res + .status(500) + .send({ + message: + 'Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available.', + }); + } + if (unitsUsed < 0 || unitsWasted < 0) { + return res + .status(500) + .send({ + message: 'Please check the used and wasted stock values. Negative numbers are invalid.', + }); } + const newStockUsed = parseFloat((consumable.stockUsed + unitsUsed).toFixed(4)); + const newStockWasted = parseFloat((consumable.stockWasted + unitsWasted).toFixed(4)); + const newAvailable = parseFloat((stockAvailable - (unitsUsed + unitsWasted)).toFixed(4)); - const newStockUsed = parseFloat((consumable.stockUsed + unitsUsed).toFixed(4)); - const newStockWasted = parseFloat((consumable.stockWasted + unitsWasted).toFixed(4)); - const newAvailable = parseFloat((stockAvailable - (unitsUsed + unitsWasted)).toFixed(4)); - - BuildingConsumable.updateOne( - { _id: consumable._id }, - { - $set: { - stockUsed: newStockUsed, - stockWasted: newStockWasted, - stockAvailable: newAvailable, - }, - $push: { - updateRecord: { - date: req.body.date, - createdBy: req.body.requestor.requestorId, - quantityUsed: unitsUsed, - quantityWasted: unitsWasted, - }, + BuildingConsumable.updateOne( + { _id: consumable._id }, + { + $set: { + stockUsed: newStockUsed, + stockWasted: newStockWasted, + stockAvailable: newAvailable, + }, + $push: { + updateRecord: { + date: req.body.date, + createdBy: req.body.requestor.requestorId, + quantityUsed: unitsUsed, + quantityWasted: unitsWasted, }, - }, - ) + }, + ) .then((results) => { res.status(200).send(results); }) @@ -128,13 +145,13 @@ const bmConsumableController = function (BuildingConsumable) { console.log('error: ', error); res.status(500).send({ message: error }); }); -}; + }; return { fetchBMConsumables, bmPurchaseConsumables, - bmPostConsumableUpdateRecord + bmPostConsumableUpdateRecord, }; }; -module.exports = bmConsumableController; \ No newline at end of file +module.exports = bmConsumableController; diff --git a/src/controllers/bmdashboard/bmInventoryTypeController.js b/src/controllers/bmdashboard/bmInventoryTypeController.js index 175d948b4..d52ce6c77 100644 --- a/src/controllers/bmdashboard/bmInventoryTypeController.js +++ b/src/controllers/bmdashboard/bmInventoryTypeController.js @@ -32,39 +32,36 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp } const fetchToolTypes = async (req, res) => { - try { - ToolType - .find() + ToolType.find() .populate([ - { - path: 'available', - select: '_id code project', - populate: { - path: 'project', - select: '_id name' - } - }, - { - path: 'using', - select: '_id code project', - populate: { - path: 'project', - select: '_id name' - } - } + { + path: 'available', + select: '_id code project', + populate: { + path: 'project', + select: '_id name', + }, + }, + { + path: 'using', + select: '_id code project', + populate: { + path: 'project', + select: '_id name', + }, + }, ]) .exec() - .then(result => { + .then((result) => { res.status(200).send(result); }) - .catch(error => { - console.error("fetchToolTypes error: ", error); + .catch((error) => { + console.error('fetchToolTypes error: ', error); res.status(500).send(error); }); - } catch (err) { - console.log("error: ", err) + console.log('error: ', err); res.json(err); } }; @@ -269,7 +266,6 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp } } - async function fetchInventoryByType(req, res) { const { type } = req.params; let SelectedType = InvType; @@ -409,4 +405,4 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp }; } -module.exports = bmInventoryTypeController; \ No newline at end of file +module.exports = bmInventoryTypeController; diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js new file mode 100644 index 000000000..ec76d3326 --- /dev/null +++ b/src/controllers/currentWarningsController.js @@ -0,0 +1,153 @@ +/* eslint-disable */ +const mongoose = require('mongoose'); +const userProfile = require('../models/userProfile'); + +const currentWarningsController = function (currentWarnings) { + const checkForDuplicates = (currentWarning, warnings) => { + const duplicateFound = warnings.some( + (warning) => warning.warningTitle.toLowerCase() === currentWarning, + ); + + return duplicateFound; + }; + + const checkIfSpecialCharacter = (warning) => { + return !/^[a-zA-Z][a-zA-Z0-9]*(?: [a-zA-Z0-9]+)*$/.test(warning); + }; + const getCurrentWarnings = async (req, res) => { + try { + const response = await currentWarnings.find({}); + + if (response.length === 0) { + return res.status(400).send({ message: 'no valid records' }); + } + return res.status(200).send({ currentWarningDescriptions: response }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + const postNewWarningDescription = async (req, res) => { + try { + const { newWarning, activeWarning, isPermanent } = req.body; + + const warnings = await currentWarnings.find({}); + + if (warnings.length === 0) { + return res.status(400).send({ error: 'no valid records' }); + } + + const testWarning = checkIfSpecialCharacter(newWarning); + if (testWarning) { + return res.status(200).send({ + error: 'Warning cannot have special characters as the first letter', + }); + } + + if (checkForDuplicates(newWarning, warnings)) { + return res.status(200).send({ error: 'warning already exists' }); + } + + const newWarningDescription = new currentWarnings(); + newWarningDescription.warningTitle = newWarning; + newWarningDescription.activeWarning = activeWarning; + newWarningDescription.isPermanent = isPermanent; + + warnings.push(newWarningDescription); + await newWarningDescription.save(); + + return res.status(201).send({ newWarnings: warnings }); + } catch (error) { + return res.status(401).send({ message: error.message }); + } + }; + + const editWarningDescription = async (req, res) => { + try { + const { editedWarning } = req.body; + + const id = editedWarning._id; + + const warnings = await currentWarnings.find({}); + + if (warnings.length === 0) { + return res.status(400).send({ message: 'no valid records' }); + } + + const lowerCaseWarning = editedWarning.warningTitle.toLowerCase(); + const testWarning = checkIfSpecialCharacter(lowerCaseWarning); + + if (testWarning) { + return res.status(200).send({ + error: 'Warning cannot have special characters as the first letter', + }); + } + + if (checkForDuplicates(lowerCaseWarning, warnings)) { + return res.status(200).send({ error: 'warning already exists try a different name' }); + } + + await currentWarnings.findOneAndUpdate( + { _id: id }, + [{ $set: { warningTitle: lowerCaseWarning.trim() } }], + { new: true }, + ); + + res.status(201).send({ message: 'warning description was updated' }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + const updateWarningDescription = async (req, res) => { + try { + const { warningDescriptionId } = req.params; + + await currentWarnings.findOneAndUpdate( + { _id: warningDescriptionId }, + [{ $set: { activeWarning: { $not: '$activeWarning' } } }], + { new: true }, + ); + + res.status(201).send({ message: 'warning description was updated' }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + const deleteWarningDescription = async (req, res) => { + try { + const { warningDescriptionId } = req.params; + const documentToDelete = await currentWarnings.findById(warningDescriptionId); + + await currentWarnings.deleteOne({ + _id: mongoose.Types.ObjectId(warningDescriptionId), + }); + + const deletedDescription = documentToDelete.warningTitle; + + await userProfile.updateMany( + { + 'warnings.description': deletedDescription, + }, + { + $pull: { + warnings: { description: deletedDescription }, + }, + }, + ); + + return res.status(200); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + return { + getCurrentWarnings, + postNewWarningDescription, + updateWarningDescription, + deleteWarningDescription, + editWarningDescription, + }; +}; +module.exports = currentWarningsController; diff --git a/src/controllers/emailController.js b/src/controllers/emailController.js index ce6b3990d..15243ef5b 100644 --- a/src/controllers/emailController.js +++ b/src/controllers/emailController.js @@ -1,13 +1,61 @@ // emailController.js -const nodemailer = require('nodemailer'); const jwt = require('jsonwebtoken'); +const cheerio = require('cheerio'); const emailSender = require('../utilities/emailSender'); +const { hasPermission } = require('../utilities/permissions'); const EmailSubcriptionList = require('../models/emailSubcriptionList'); const userProfile = require('../models/userProfile'); const frontEndUrl = process.env.FRONT_END_URL || 'http://localhost:3000'; const jwtSecret = process.env.JWT_SECRET || 'EmailSecret'; +const handleContentToOC = (htmlContent) => + ` + +
+ + + + ${htmlContent} + + `; + +const handleContentToNonOC = (htmlContent, email) => + ` + + + + + + ${htmlContent} +Thank you for subscribing to our email updates!
+If you would like to unsubscribe, please click here
+ + `; + +function extractImagesAndCreateAttachments(html) { + const $ = cheerio.load(html); + const attachments = []; + + $('img').each((i, img) => { + const src = $(img).attr('src'); + if (src.startsWith('data:image')) { + const base64Data = src.split(',')[1]; + const _cid = `image-${i}`; + attachments.push({ + filename: `image-${i}.png`, + content: Buffer.from(base64Data, 'base64'), + cid: _cid, + }); + $(img).attr('src', `cid:${_cid}`); + } + }); + return { + html: $.html(), + attachments, + }; +} + const sendEmail = async (req, res) => { const canSendEmail = await hasPermission(req.body.requestor, 'sendEmails'); if (!canSendEmail) { @@ -16,11 +64,36 @@ const sendEmail = async (req, res) => { } try { const { to, subject, html } = req.body; + // Validate required fields + if (!subject || !html || !to) { + const missingFields = []; + if (!subject) missingFields.push('Subject'); + if (!html) missingFields.push('HTML content'); + if (!to) missingFields.push('Recipient email'); + console.log('missingFields', missingFields); + return res + .status(400) + .send(`${missingFields.join(' and ')} ${missingFields.length > 1 ? 'are' : 'is'} required`); + } + + // Extract images and create attachments + const { html: processedHtml, attachments } = extractImagesAndCreateAttachments(html); + + // Log recipient for debugging + console.log('Recipient:', to); + + + await emailSender(to, subject, html) + .then(result => { + console.log('Email sent successfully:', result); + res.status(200).send(`Email sent successfully to ${to}`); + }) + .catch(error => { + console.error('Error sending email:', error); + res.status(500).send('Error sending email'); + }); - console.log('to', to); - emailSender(to, subject, html); - return res.status(200).send('Email sent successfully'); } catch (error) { console.error('Error sending email:', error); return res.status(500).send('Error sending email'); @@ -35,46 +108,44 @@ const sendEmailToAll = async (req, res) => { } try { const { subject, html } = req.body; + if (!subject || !html) { + return res.status(400).send('Subject and HTML content are required'); + } + + const { html: processedHtml, attachments } = extractImagesAndCreateAttachments(html); + const users = await userProfile.find({ - firstName: 'Haoji', + firstName: '', email: { $ne: null }, isActive: true, emailSubscriptions: true, }); - let to = ''; - const emailContent = ` - - - - + if (users.length === 0) { + return res.status(404).send('No users found'); + } + const recipientEmails = users.map((user) => user.email); + console.log('# sendEmailToAll to', recipientEmails.join(',')); + if (recipientEmails.length === 0) { + throw new Error('No recipients defined'); + } - - ${html} - - `; - users.forEach((user) => { - to += `${user.email},`; - }); - emailSender(to, subject, emailContent); - const emailList = await EmailSubcriptionList.find({ email: { $ne: null } }); - emailList.forEach((emailObject) => { - const { email } = emailObject; - const emailContent = ` - - - - + const emailContentToOCmembers = handleContentToOC(processedHtml); + await Promise.all( + recipientEmails.map((email) => + emailSender(email, subject, emailContentToOCmembers, attachments), + ), + ); + const emailSubscribers = await EmailSubcriptionList.find({ email: { $exists: true, $ne: '' } }); + console.log('# sendEmailToAll emailSubscribers', emailSubscribers.length); + await Promise.all( + emailSubscribers.map(({ email }) => { + const emailContentToNonOCmembers = handleContentToNonOC(processedHtml, email); + return emailSender(email, subject, emailContentToNonOCmembers, attachments); + }), + ); - - ${html} -Thank you for subscribing to our email updates!
-If you would like to unsubscribe, please click here
- - `; - emailSender(email, subject, emailContent); - }); return res.status(200).send('Email sent successfully'); } catch (error) { console.error('Error sending email:', error); @@ -112,13 +183,9 @@ const addNonHgnEmailSubscription = async (req, res) => { } const payload = { email }; - const token = jwt.sign( - payload, - jwtSecret, - { - expiresIn: 360, - }, - ); + const token = jwt.sign(payload, jwtSecret, { + expiresIn: 360, + }); const emailContent = ` diff --git a/src/controllers/emailController.spec.js b/src/controllers/emailController.spec.js new file mode 100644 index 000000000..f5327a328 --- /dev/null +++ b/src/controllers/emailController.spec.js @@ -0,0 +1,146 @@ +const { mockReq, mockRes, assertResMock } = require('../test'); +const emailController = require('./emailController'); +const jwt = require('jsonwebtoken'); +const userProfile = require('../models/userProfile'); + + +jest.mock('jsonwebtoken'); +jest.mock('../models/userProfile'); +jest.mock('../utilities/emailSender'); + + + + +const makeSut = () => { + const { + sendEmail, + sendEmailToAll, + updateEmailSubscriptions, + addNonHgnEmailSubscription, + removeNonHgnEmailSubscription, + confirmNonHgnEmailSubscription, + } = emailController; + return { + sendEmail, + sendEmailToAll, + updateEmailSubscriptions, + addNonHgnEmailSubscription, + removeNonHgnEmailSubscription, + confirmNonHgnEmailSubscription, + }; +}; +describe('emailController Controller Unit tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('sendEmail function', () => { + test('should send email successfully', async () => { + const { sendEmail } = makeSut(); + const mockReq = { + body: { + to: 'recipient@example.com', + subject: 'Test Subject', + html: 'Test Body
', + }, + }; + const response = await sendEmail(mockReq, mockRes); + assertResMock(200, 'Email sent successfully', response, mockRes); + }); +}); + + describe('updateEmailSubscriptions function', () => { + test('should handle error when updating email subscriptions', async () => { + const { updateEmailSubscriptions } = makeSut(); + + + userProfile.findOneAndUpdate = jest.fn(); + + userProfile.findOneAndUpdate.mockRejectedValue(new Error('Update failed')); + + const mockReq = { + body: { + emailSubscriptions: ['subscription1', 'subscription2'], + requestor: { + email: 'test@example.com', + }, + }, + }; + + const response = await updateEmailSubscriptions(mockReq, mockRes); + + assertResMock(500, 'Error updating email subscriptions', response, mockRes); + }); + }); + + + describe('confirmNonHgnEmailSubscription function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(() => { + jwt.verify = jest.fn(); + }); + + test('should return 400 if token is not provided', async () => { + const { confirmNonHgnEmailSubscription } = makeSut(); + + const mockReq = { body: {} }; + const response = await confirmNonHgnEmailSubscription(mockReq, mockRes); + + assertResMock(400, 'Invalid token', response, mockRes); + }); + + test('should return 401 if token is invalid', async () => { + const { confirmNonHgnEmailSubscription } = makeSut(); + const mockReq = { body: { token: 'invalidToken' } }; + + jwt.verify.mockImplementation(() => { + throw new Error('Token is not valid'); + }); + + await confirmNonHgnEmailSubscription(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + errors: [ + { msg: 'Token is not valid' }, + ], + }); + }); + + + test('should return 400 if email is missing from payload', async () => { + const { confirmNonHgnEmailSubscription } = makeSut(); + const mockReq = { body: { token: 'validToken' } }; + + // Mocking jwt.verify to return a payload without email + jwt.verify.mockReturnValue({}); + + const response = await confirmNonHgnEmailSubscription(mockReq, mockRes); + + assertResMock(400, 'Invalid token', response, mockRes); + }); + + + + + + }); + describe('removeNonHgnEmailSubscription function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return 400 if email is missing', async () => { + const { removeNonHgnEmailSubscription } = makeSut(); + const mockReq = { body: {} }; + + const response = await removeNonHgnEmailSubscription(mockReq, mockRes); + + assertResMock(400, 'Email is required', response, mockRes); + }); + }); + + }); diff --git a/src/controllers/jobsController.js b/src/controllers/jobsController.js new file mode 100644 index 000000000..81e7c3cb3 --- /dev/null +++ b/src/controllers/jobsController.js @@ -0,0 +1,231 @@ +const Job = require('../models/jobs'); // Import the Job model + +// Controller to fetch all jobs with pagination, search, and filtering +const getJobs = async (req, res) => { + const { page = 1, limit = 18, search = '', category = '' } = req.query; + + try { + // Validate query parameters + const pageNumber = Math.max(1, parseInt(page, 10)); // Ensure page is at least 1 + const limitNumber = Math.max(1, parseInt(limit, 10)); // Ensure limit is at least 1 + + // Build query object + const query = {}; + if (search) query.title = { $regex: search, $options: 'i' }; // Case-insensitive search + if (category) query.category = category; + + // Fetch total count for pagination metadata + const totalJobs = await Job.countDocuments(query); + + // Fetch paginated results + const jobs = await Job.find(query) + .skip((pageNumber - 1) * limitNumber) + .limit(limitNumber); + + // Prepare response + res.json({ + jobs, + pagination: { + totalJobs, + totalPages: Math.ceil(totalJobs / limitNumber), + currentPage: pageNumber, + limit: limitNumber, + hasNextPage: pageNumber < Math.ceil(totalJobs / limitNumber), + hasPreviousPage: pageNumber > 1, + }, + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch jobs', details: error.message }); + } +}; + +// Controller to fetch job summaries with pagination, search, filtering, and sorting +const getJobSummaries = async (req, res) => { + const { search = '', page = 1, limit = 18, category = '' } = req.query; + + try { + const pageNumber = Math.max(1, parseInt(page, 10)); + const limitNumber = Math.max(1, parseInt(limit, 10)); + + // Construct the query object + const query = {}; + if (search) query.title = { $regex: search, $options: 'i' }; + if (category) query.category = category; + + // Sorting logic + const sortCriteria = { + title: 1, + datePosted: -1, + featured: -1 + }; + + // Fetch the total number of jobs matching the query for pagination + const totalJobs = await Job.countDocuments(query); + const jobs = await Job.find(query) + .select('title category location description datePosted featured') + .sort(sortCriteria) + .skip((pageNumber - 1) * limitNumber) + .limit(limitNumber); + + res.json({ + jobs, + pagination: { + totalJobs, + totalPages: Math.ceil(totalJobs / limitNumber), // Calculate total number of pages + currentPage: pageNumber, + limit: limitNumber, + hasNextPage: pageNumber < Math.ceil(totalJobs / limitNumber), + hasPreviousPage: pageNumber > 1, + }, + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch job summaries', details: error.message }); + } +}; + +// Controller to fetch job title suggestions for a dropdown +const getJobTitleSuggestions = async (req, res) => { + const { query = '' } = req.query; + + try { + const suggestions = await Job.find({ title: { $regex: query, $options: 'i' } }) + .distinct('title'); + + res.json({ suggestions }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch job title suggestions', details: error.message }); + } +}; + +const resetJobsFilters = async (req, res) => { + const { page = 1, limit = 18 } = req.query; + + try { + // Validate pagination parameters + const pageNumber = Math.max(1, parseInt(page, 10)); + const limitNumber = Math.max(1, parseInt(limit, 10)); + + // Sorting logic + const sortCriteria = { + title: 1, + datePosted: -1, + featured: -1 + }; + // Fetch all jobs without filtering + const totalJobs = await Job.countDocuments({}); + const jobs = await Job.find({}) + .sort(sortCriteria) + .skip((pageNumber - 1) * limitNumber) + .limit(limitNumber); + + // Respond with all jobs and pagination metadata + res.json({ + jobs, + pagination: { + totalJobs, + totalPages: Math.ceil(totalJobs / limitNumber), + currentPage: pageNumber, + limit: limitNumber, + hasNextPage: pageNumber < Math.ceil(totalJobs / limitNumber), + hasPreviousPage: pageNumber > 1, + }, + }); + } catch (error) { + res.status(500).json({ error: 'Failed to reset filters or reload jobs', details: error.message }); + } +}; + +const getCategories = async (req, res) => { + try { + const categories = await Job.distinct('category', {}); + + // Sort categories alphabetically + categories.sort((a, b) => a.localeCompare(b)); + + res.status(200).json({ categories }); + } catch (error) { + console.error('Error fetching categories:', error); + res.status(500).json({ message: 'Failed to fetch categories' }); + } +}; +// Controller to fetch job details by ID +const getJobById = async (req, res) => { + const { id } = req.params; + + try { + const job = await Job.findById(id); + if (!job) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(job); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch job', details: error.message }); + } +}; + +// Controller to create a new job +const createJob = async (req, res) => { + const { title, category, description, imageUrl, location, applyLink, jobDetailsLink } = req.body; + + try { + const newJob = new Job({ + title, + category, + description, + imageUrl, + location, + applyLink, + jobDetailsLink, + }); + + const savedJob = await newJob.save(); + res.status(201).json(savedJob); + } catch (error) { + res.status(500).json({ error: 'Failed to create job', details: error.message }); + } +}; + +// Controller to update an existing job by ID +const updateJob = async (req, res) => { + const { id } = req.params; + + try { + const updatedJob = await Job.findByIdAndUpdate(id, req.body, { new: true }); + if (!updatedJob) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(updatedJob); + } catch (error) { + res.status(500).json({ error: 'Failed to update job', details: error.message }); + } +}; + +// Controller to delete a job by ID +const deleteJob = async (req, res) => { + const { id } = req.params; + + try { + const deletedJob = await Job.findByIdAndDelete(id); + if (!deletedJob) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json({ message: 'Job deleted successfully' }); + } catch (error) { + res.status(500).json({ error: 'Failed to delete job', details: error.message }); + } +}; + + + +// Export controllers as a plain object +module.exports = { + getJobs, + getJobTitleSuggestions, + getJobById, + createJob, + updateJob, + deleteJob, + getJobSummaries, + resetJobsFilters, + getCategories +}; diff --git a/src/controllers/permissionChangeLogsController.js b/src/controllers/permissionChangeLogsController.js index 33ddb5d72..36e8486d4 100644 --- a/src/controllers/permissionChangeLogsController.js +++ b/src/controllers/permissionChangeLogsController.js @@ -1,6 +1,6 @@ const UserProfile = require('../models/userProfile'); -const permissionChangeLogController = function (PermissionChangeLog) { +const permissionChangeLogController = function (PermissionChangeLog,userPermissionChangeLog) { const getPermissionChangeLogs = async function (req, res) { try { const userProfile = await UserProfile.findOne({ _id: req.params.userId }).exec(); @@ -9,8 +9,24 @@ const permissionChangeLogController = function (PermissionChangeLog) { if (userProfile.role !== 'Owner') { res.status(204).send([]); } else { - const changeLogs = await PermissionChangeLog.find({}); - res.status(200).send(changeLogs); + const userChangeLogs = await userPermissionChangeLog.find(); + const rolePermissionChangeLogs = await PermissionChangeLog.find(); + + const formattedUserChangeLogs = userChangeLogs.map(log => ({ + ...log.toObject(), + name: log.individualName, + })); + + const formattedRolePermissionChangeLogs = rolePermissionChangeLogs.map(log => ({ + ...log.toObject(), + name: log.roleName, + })); + + const mergedLogs = [...formattedUserChangeLogs, ...formattedRolePermissionChangeLogs].sort( + (a, b) => new Date(b.logDateTime) - new Date(a.logDateTime) + ); + + res.status(200).json(mergedLogs); } } else { res.status(403).send(`User (${req.params.userId}) not found.`); diff --git a/src/controllers/popupEditorController.spec.js b/src/controllers/popupEditorController.spec.js new file mode 100644 index 000000000..e70b05553 --- /dev/null +++ b/src/controllers/popupEditorController.spec.js @@ -0,0 +1,163 @@ +const PopUpEditor = require('../models/popupEditor'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +jest.mock('../utilities/permissions'); + +const helper = require('../utilities/permissions'); +const popupEditorController = require('./popupEditorController'); + +const flushPromises = () => new Promise(setImmediate); + +const mockHasPermission = (value) => + jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); + +const makeSut = () => { + const { getAllPopupEditors, getPopupEditorById, createPopupEditor, updatePopupEditor } = + popupEditorController(PopUpEditor); + return { getAllPopupEditors, getPopupEditorById, createPopupEditor, updatePopupEditor }; +}; + +describe('popupEditorController Controller Unit tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe(`getAllPopupEditors function`, () => { + test(`Should return 200 and popup editors on success`, async () => { + const { getAllPopupEditors } = makeSut(); + const mockPopupEditors = [{ popupName: 'popup', popupContent: 'content' }]; + jest.spyOn(PopUpEditor, 'find').mockResolvedValue(mockPopupEditors); + const response = await getAllPopupEditors(mockReq, mockRes); + assertResMock(200, mockPopupEditors, response, mockRes); + }); + + test(`Should return 404 on error`, async () => { + const { getAllPopupEditors } = makeSut(); + const error = new Error('Test Error'); + + jest.spyOn(PopUpEditor, 'find').mockRejectedValue(error); + const response = await getAllPopupEditors(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + }); + }); + + describe(`getPopupEditorById function`, () => { + test(`Should return 200 and popup editor on success`, async () => { + const { getPopupEditorById } = makeSut(); + const mockPopupEditor = { popupName: 'popup', popupContent: 'content' }; + jest.spyOn(PopUpEditor, 'findById').mockResolvedValue(mockPopupEditor); + const response = await getPopupEditorById(mockReq, mockRes); + assertResMock(200, mockPopupEditor, response, mockRes); + }); + + test(`Should return 404 on error`, async () => { + const { getPopupEditorById } = makeSut(); + const error = new Error('Test Error'); + + jest.spyOn(PopUpEditor, 'findById').mockRejectedValue(error); + const response = await getPopupEditorById(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + }); + }); + + describe(`createPopupEditor function`, () => { + test(`Should return 403 if user is not authorized`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(false); + const response = await createPopupEditor(mockReq, mockRes); + assertResMock( + 403, + { error: 'You are not authorized to create new popup' }, + response, + mockRes, + ); + }); + + test(`Should return 400 if popupName or popupContent is missing`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(true); + const response = await createPopupEditor(mockReq, mockRes); + assertResMock( + 400, + { error: 'popupName , popupContent are mandatory fields' }, + response, + mockRes, + ); + }); + + test(`Should return 201 and popup editor on success`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(true); + mockReq.body = { popupName: 'popup', popupContent: 'content' }; + const mockPopupEditor = { save: jest.fn().mockResolvedValue(mockReq.body) }; + jest.spyOn(PopUpEditor.prototype, 'save').mockImplementationOnce(mockPopupEditor.save); + const response = await createPopupEditor(mockReq, mockRes); + expect(mockPopupEditor.save).toHaveBeenCalled(); + assertResMock(201, mockReq.body, response, mockRes); + }); + + test(`Should return 500 on error`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(true); + const error = new Error('Test Error'); + + jest.spyOn(PopUpEditor.prototype, 'save').mockRejectedValue(error); + const response = await createPopupEditor(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, { error }, response, mockRes); + }); + }); + describe(`updatePopupEditor function`, () => { + test(`Should return 403 if user is not authorized`, async () => { + const { updatePopupEditor } = makeSut(); + mockHasPermission(false); + const response = await updatePopupEditor(mockReq, mockRes); + assertResMock( + 403, + { error: 'You are not authorized to create new popup' }, + response, + mockRes, + ); + }); + + test(`Should return 400 if popupContent is missing`, async () => { + const { updatePopupEditor } = makeSut(); + mockReq.body = {}; + mockHasPermission(true); + const response = await updatePopupEditor(mockReq, mockRes); + assertResMock(400, { error: 'popupContent is mandatory field' }, response, mockRes); + }); + + test(`Should return 201 and popup editor on success`, async () => { + const { updatePopupEditor } = makeSut(); + mockHasPermission(true); + mockReq.body = { popupContent: 'content' }; + const mockPopupEditor = { save: jest.fn().mockResolvedValue(mockReq.body) }; + jest.spyOn(PopUpEditor, 'findById').mockImplementationOnce((mockReq, callback) => callback(null, mockPopupEditor)); + jest.spyOn(PopUpEditor.prototype, 'save').mockImplementationOnce(mockPopupEditor.save); + const response = await updatePopupEditor(mockReq, mockRes); + expect(mockPopupEditor.save).toHaveBeenCalled(); + assertResMock(201, mockReq.body, response, mockRes); + }); + + test('Should return 500 on popupEditor save error', async () => { + const { updatePopupEditor } = makeSut(); + mockHasPermission(true); + const err = new Error('Test Error'); + mockReq.body = { popupContent: 'content' }; + const mockPopupEditor = { save: jest.fn().mockRejectedValue(err)}; + jest + .spyOn(PopUpEditor, 'findById') + .mockImplementation((mockReq, callback) => callback(null, mockPopupEditor)); + jest.spyOn(PopUpEditor.prototype, 'save').mockImplementationOnce(mockPopupEditor.save); + const response = await updatePopupEditor(mockReq, mockRes); + await flushPromises(); + assertResMock(500, {err}, response, mockRes); + }); + }); +}); diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index 610d627a1..e56e7e406 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -172,13 +172,22 @@ const profileInitialSetupController = function ( const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; await session.commitTransaction(); - const acknowledgment = await sendEmailWithAcknowledgment( - email, - 'NEEDED: Complete your One Community profile setup', - sendLinkMessage(link), - ); - - return res.status(200).send(acknowledgment); + // Send response immediately without waiting for email acknowledgment + res.status(200).send({ message: 'Token created successfully, email is being sent.' }); + + // Asynchronously send the email acknowledgment + setImmediate(async () => { + try { + await sendEmailWithAcknowledgment( + email, + 'NEEDED: Complete your One Community profile setup', + sendLinkMessage(link), + ); + } catch (emailError) { + // Log email sending failure + LOGGER.logException(emailError, 'sendEmailWithAcknowledgment', JSON.stringify({ email, link }), null); + } + }); } catch (error) { await session.abortTransaction(); LOGGER.logException(error, 'getSetupToken', JSON.stringify(req.body), null); diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index 6d3cfc37b..3e48b3f42 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -300,12 +300,12 @@ const projectController = function (Project) { const updateTask = await hasPermission(req.body.requestor, 'updateTask'); const suggestTask = await hasPermission(req.body.requestor, 'suggestTask'); - const getId = (getProjMembers || postTask || updateTask || suggestTask); + const canGetId = (getProjMembers || postTask || updateTask || suggestTask); userProfile .find( { projects: projectId }, - { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: getId }, + { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: canGetId }, ) .sort({ firstName: 1, lastName: 1 }) .then((results) => { diff --git a/src/controllers/reasonSchedulingController.js b/src/controllers/reasonSchedulingController.js index 67162abe1..a227e7f3e 100644 --- a/src/controllers/reasonSchedulingController.js +++ b/src/controllers/reasonSchedulingController.js @@ -395,4 +395,4 @@ module.exports = { getSingleReason, patchReason, deleteReason, -}; \ No newline at end of file +}; diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index 1e019ffa1..b4d71a5dd 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -683,7 +683,10 @@ const taskController = function (Task) { }; const updateTask = async (req, res) => { - if (!(await hasPermission(req.body.requestor, 'updateTask'))) { + if ( + !(await hasPermission(req.body.requestor, 'updateTask')) && + !(await hasPermission(req.body.requestor, 'removeUserFromTask')) + ) { res.status(403).send({ error: 'You are not authorized to update Task.' }); return; } diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index f1fd2241d..9ce719a8c 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -6,8 +6,66 @@ const Logger = require('../startup/logger'); const teamcontroller = function (Team) { const getAllTeams = function (req, res) { - Team.find({}) - .sort({ teamName: 1 }) + Team.aggregate([ + { + $unwind: '$members', + }, + { + $lookup: { + from: 'userProfiles', + localField: 'members.userId', + foreignField: '_id', + as: 'userProfile', + }, + }, + { + $unwind: '$userProfile', + }, + { + $match: { + isActive: true, + } + }, + { + $group: { + _id: { + teamId: '$_id', + teamCode: '$userProfile.teamCode', + }, + count: { $sum: 1 }, + teamName: { $first: '$teamName' }, + members: { + $push: { + _id: '$userProfile._id', + name: '$userProfile.name', + email: '$userProfile.email', + teamCode: '$userProfile.teamCode', + addDateTime: '$members.addDateTime', + }, + }, + createdDatetime: { $first: '$createdDatetime' }, + modifiedDatetime: { $first: '$modifiedDatetime' }, + isActive: { $first: '$isActive' }, + }, + }, + { + $sort: { count: -1 }, // Sort by the most frequent teamCode + }, + { + $group: { + _id: '$_id.teamId', + teamCode: { $first: '$_id.teamCode' }, // Get the most frequent teamCode + teamName: { $first: '$teamName' }, + members: { $first: '$members' }, + createdDatetime: { $first: '$createdDatetime' }, + modifiedDatetime: { $first: '$modifiedDatetime' }, + isActive: { $first: '$isActive' }, + }, + }, + { + $sort: { teamName: 1 }, // Sort teams by name + }, + ]) .then((results) => res.status(200).send(results)) .catch((error) => { Logger.logException(error); @@ -223,15 +281,15 @@ const teamcontroller = function (Team) { }, }, ]) - .then((result) => res.status(200).send(result)) + .then((result) => { + res.status(200).send(result) + }) .catch((error) => { Logger.logException(error, null, `TeamId: ${teamId} Request:${req.body}`); - res.status(500).send(error); + return res.status(500).send(error); }); }; const updateTeamVisibility = async (req, res) => { - console.log('==============> 9 '); - const { visibility, teamId, userId } = req.body; try { @@ -310,6 +368,47 @@ const teamcontroller = function (Team) { }); }; + const getAllTeamMembers = async function (req,res) { + try{ + const teamIds = req.body; + const cacheKey='teamMembersCache' + if(cache.hasCache(cacheKey)){ + let data=cache.getCache('teamMembersCache') + return res.status(200).send(data); + } + if (!Array.isArray(teamIds) || teamIds.length === 0 || !teamIds.every(team => mongoose.Types.ObjectId.isValid(team._id))) { + return res.status(400).send({ error: 'Invalid request: teamIds must be a non-empty array of valid ObjectId strings.' }); + } + let data = await Team.aggregate([ + { + $match: { _id: { $in: teamIds.map(team => mongoose.Types.ObjectId(team._id)) } } + }, + { $unwind: '$members' }, + { + $lookup: { + from: 'userProfiles', + localField: 'members.userId', + foreignField: '_id', + as: 'userProfile', + }, + }, + { $unwind: { path: '$userProfile', preserveNullAndEmptyArrays: true } }, + { + $group: { + _id: '$_id', // Group by team ID + teamName: { $first: '$teamName' }, // Use $first to keep the team name + createdDatetime: { $first: '$createdDatetime' }, + members: { $push: '$members' }, // Rebuild the members array + }, + }, + ]) + cache.setCache(cacheKey,data) + res.status(200).send(data); + }catch(error){ + console.log(error) + res.status(500).send({'message':"Fetching team members failed"}); + } + } return { getAllTeams, getAllTeamCode, @@ -320,6 +419,7 @@ const teamcontroller = function (Team) { assignTeamToUsers, getTeamMembership, updateTeamVisibility, + getAllTeamMembers }; }; diff --git a/src/controllers/teamController.spec.js b/src/controllers/teamController.spec.js new file mode 100644 index 000000000..bf6c1b51d --- /dev/null +++ b/src/controllers/teamController.spec.js @@ -0,0 +1,81 @@ +const Team = require('../models/team'); +const teamController = require('./teamController'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +const makeSut = () => { + const { getAllTeams, getTeamById } = teamController(Team); + return { + getAllTeams, + getTeamById, + }; +}; + +const flushPromises = () => new Promise(setImmediate); + +describe('teamController', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + const sortObject = { + sort: () => {}, + }; + + const error = new Error('any error'); + + describe('getAllTeams', () => { + test('Returns 404 - an error occurs during team retrieval.', async () => { + const { getAllTeams } = makeSut(); + + const mockSort = jest.spyOn(sortObject, 'sort').mockRejectedValueOnce(error); + const findSpy = jest.spyOn(Team, 'find').mockReturnValue(sortObject); + const response = getAllTeams(mockReq, mockRes); + await flushPromises(); + + expect(findSpy).toHaveBeenCalledWith({}); + expect(mockSort).toHaveBeenCalledWith({ teamName: 1 }); + assertResMock(404, error, response, mockRes); + }); + + test('Returns 200 - should return all teams sorted by name.', async () => { + const team1 = { teamName: 'Team A' }; + const team2 = { teamName: 'Team B' }; + const sortedTeams = [team1, team2]; + + const mockSortResovledValue = [team1, team2]; + const mockSort = jest.spyOn(sortObject, 'sort').mockResolvedValue(mockSortResovledValue); + const findSpy = jest.spyOn(Team, 'find').mockReturnValue(sortObject); + const { getAllTeams } = makeSut(); + const response = getAllTeams(mockReq, mockRes); + await flushPromises(); + + expect(findSpy).toHaveBeenCalledWith({}); + expect(mockSort).toHaveBeenCalledWith({ teamName: 1 }); + assertResMock(200, sortedTeams, response, mockRes); + }); + }); + + describe('getTeamById', () => { + test('Returns 404 - the specified team ID does not exist.', async () => { + const { getTeamById } = makeSut(); + const req = { params: { teamId: 'nonExistentTeamId' } }; + const findByIdSpy = jest.spyOn(Team, 'findById').mockRejectedValue(error); + const response = getTeamById(req, mockRes); + await flushPromises(); + + expect(findByIdSpy).toHaveBeenCalledWith(req.params.teamId); + assertResMock(404, error, response, mockRes); + }); + + test('Returns 200 - all is successful, return a team by ID.', async () => { + const { getTeamById } = makeSut(); + const teamId = '5a8e21f00317bc'; + const findByIdSpy = jest.spyOn(Team, 'findById').mockResolvedValue({ teamId }); + const response = getTeamById(mockReq, mockRes); + await flushPromises(); + + expect(findByIdSpy).toHaveBeenCalledWith(teamId); + assertResMock(200, { teamId }, response, mockRes); + }); + }); +}); diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 4cf5190db..44a50fcfb 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -420,7 +420,7 @@ const addEditHistory = async (One Community
-ADMINISTRATIVE DETAILS:
Start Date: ${moment(userprofile.startDate).utc().format('M-D-YYYY')}
Role: ${userprofile.role}
@@ -594,7 +594,7 @@ const timeEntrycontroller = function (TimeEntry) { await timeEntry.save({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); } @@ -867,7 +867,7 @@ const timeEntrycontroller = function (TimeEntry) { } await timeEntry.save({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); @@ -940,7 +940,7 @@ const timeEntrycontroller = function (TimeEntry) { await timeEntry.remove({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); @@ -1063,38 +1063,47 @@ const timeEntrycontroller = function (TimeEntry) { }); }; - const getTimeEntriesForReports = function (req, res) { + const getTimeEntriesForReports =async function (req, res) { const { users, fromDate, toDate } = req.body; - - TimeEntry.find( - { - personId: { $in: users }, - dateOfWork: { $gte: fromDate, $lte: toDate }, - }, - ' -createdDateTime', - ) - .populate('projectId') - - .then((results) => { - const data = []; - - results.forEach((element) => { - const record = {}; - record._id = element._id; - record.isTangible = element.isTangible; - record.personId = element.personId._id; - record.dateOfWork = element.dateOfWork; - [record.hours, record.minutes] = formatSeconds(element.totalSeconds); - record.projectId = element.projectId ? element.projectId._id : ''; - record.projectName = element.projectId ? element.projectId.projectName : ''; - data.push(record); - }); - - res.status(200).send(data); - }) - .catch((error) => { - res.status(400).send(error); + const cacheKey = `timeEntry_${fromDate}_${toDate}`; + const timeentryCache=cacheClosure(); + const cacheData=timeentryCache.hasCache(cacheKey) + if(cacheData){ + const data = timeentryCache.getCache(cacheKey); + return res.status(200).send(data); + } + try { + const results = await TimeEntry.find( + { + personId: { $in: users }, + dateOfWork: { $gte: fromDate, $lte: toDate }, + }, + '-createdDateTime' // Exclude unnecessary fields + ) + .lean() // Returns plain JavaScript objects, not Mongoose documents + .populate({ + path: 'projectId', + select: '_id projectName', // Only return necessary fields from the project + }) + .exec(); // Executes the query + const data = results.map(element => { + const record = { + _id: element._id, + isTangible: element.isTangible, + personId: element.personId, + dateOfWork: element.dateOfWork, + hours: formatSeconds(element.totalSeconds)[0], + minutes: formatSeconds(element.totalSeconds)[1], + projectId: element.projectId?._id || '', + projectName: element.projectId?.projectName || '', + }; + return record; }); + timeentryCache.setCache(cacheKey,data); + return res.status(200).send(data); + } catch (error) { + res.status(400).send(error); + } }; const getTimeEntriesForProjectReports = function (req, res) { @@ -1275,7 +1284,12 @@ const timeEntrycontroller = function (TimeEntry) { */ const getLostTimeEntriesForTeamList = function (req, res) { const { teams, fromDate, toDate } = req.body; - + const lostteamentryCache=cacheClosure() + const cacheKey = `LostTeamEntry_${fromDate}_${toDate}`; + const cacheData=lostteamentryCache.getCache(cacheKey) + if(cacheData){ + return res.status(200).send(cacheData) + } TimeEntry.find( { entryType: 'team', @@ -1284,7 +1298,7 @@ const timeEntrycontroller = function (TimeEntry) { isActive: { $ne: false }, }, ' -createdDateTime', - ) + ).lean() .populate('teamId') .sort({ lastModifiedDateTime: -1 }) .then((results) => { @@ -1301,7 +1315,8 @@ const timeEntrycontroller = function (TimeEntry) { [record.hours, record.minutes] = formatSeconds(element.totalSeconds); data.push(record); }); - res.status(200).send(data); + lostteamentryCache.setCache(cacheKey,data); + return res.status(200).send(data); }) .catch((error) => { res.status(400).send(error); diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 1240d3373..277842c43 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,4 +1,3 @@ -const Team = require('../models/team'); const Project = require('../models/project'); const cacheClosure = require('../utilities/nodeCache'); const userProfileController = require("./userProfileController"); @@ -6,198 +5,202 @@ const userProfile = require('../models/userProfile'); const project = require('../models/project'); const controller = userProfileController(userProfile, project); -const getAllTeamCodeHelper = controller.getAllTeamCodeHelper; +const { getAllTeamCodeHelper } = controller; const titlecontroller = function (Title) { const cache = cacheClosure(); - const getAllTitles = function (req, res) { - Title.find({}) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. + async function checkTeamCodeExists(teamCode) { + try { + if (cache.getCache('teamCodes')) { + const teamCodes = JSON.parse(cache.getCache('teamCodes')); + return teamCodes.includes(teamCode); + } + const teamCodes = await getAllTeamCodeHelper(); + return teamCodes.includes(teamCode); + } catch (error) { + console.error('Error checking if team code exists:', error); + throw error; + } + } + + async function checkProjectExists(projectID) { + try { + const proj = await Project.findOne({ _id: projectID }).exec(); + return !!proj; + } catch (error) { + console.error('Error checking if project exists:', error); + throw error; + } + } + + const getAllTitles = function (req, res) { + Title.find({}) + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); }; - const getTitleById = function (req, res) { - const { titleId } = req.params; + const getTitleById = function (req, res) { + const { titleId } = req.params; + + Title.findById(titleId) + .then((results) => res.send(results)) + .catch((error) => res.send(error)); + }; + + const postTitle = async function (req, res) { + const title = new Title(); + title.titleName = req.body.titleName; + title.titleCode = req.body.titleCode; + title.teamCode = req.body.teamCode; + title.projectAssigned = req.body.projectAssigned; + title.mediaFolder = req.body.mediaFolder; + title.teamAssiged = req.body.teamAssiged; + + const titleCodeRegex = /^[A-Za-z]+$/; + if (!title.titleCode || !title.titleCode.trim()) { + return res.status(400).send({ message: 'Title code cannot be empty.' }); + } + + if (!titleCodeRegex.test(title.titleCode)) { + return res.status(400).send({ message: 'Title Code must contain only upper or lower case letters.' }); + } + + // valid title name + if (!title.titleName.trim()) { + res.status(400).send({ message: 'Title cannot be empty.' }); + return; + } + + // if media is empty + if (!title.mediaFolder.trim()) { + res.status(400).send({ message: 'Media folder cannot be empty.' }); + return; + } - Title.findById(titleId) - .then((results) => res.send(results)) - .catch((error) => res.send(error)); - }; + if (!title.teamCode) { + res.status(400).send({ message: 'Please provide a team code.' }); + return; + } - const postTitle = async function (req, res) { - const title = new Title(); - title.titleName = req.body.titleName; - title.teamCode = req.body.teamCode; - title.projectAssigned = req.body.projectAssigned; - title.mediaFolder = req.body.mediaFolder; - title.teamAssiged = req.body.teamAssiged; + const teamCodeExists = await checkTeamCodeExists(title.teamCode); + if (!teamCodeExists) { + res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); + return; + } + + // validate if project exist + const projectExist = await checkProjectExists(title.projectAssigned._id); + if (!projectExist) { + res.status(400).send({ message: 'Project lalala is empty or not exist!!!' }); + return; + } + + // validate if team exist + if (title.teamAssiged && title.teamAssiged._id === 'N/A') { + res.status(400).send({ message: 'Team not exists.' }); + return; + } + + title + .save() + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)) + }; + + // update title function. + const updateTitle = async function (req, res) { + try { + + const filter = req.body.id; // valid title name - if (!title.titleName.trim()) { + if (!req.body.titleName.trim()) { res.status(400).send({ message: 'Title cannot be empty.' }); return; } - // if media is empty - if (!title.mediaFolder.trim()) { - res.status(400).send({ message: 'Media folder cannot be empty.' }); + if (!req.body.titleCode.trim()) { + res.status(400).send({ message: 'Title code cannot be empty.' }); return; } - const shortnames = title.titleName.trim().split(' '); - let shortname; - if (shortnames.length > 1) { - shortname = (shortnames[0][0] + shortnames[1][0]).toUpperCase(); - } else if (shortnames.length === 1) { - shortname = shortnames[0][0].toUpperCase(); + const titleCodeRegex = /^[A-Za-z]+$/; + if (!titleCodeRegex.test(req.body.titleCode)) { + return res.status(400).send({ message: 'Title Code must contain only upper or lower case letters.' }); } - title.shortName = shortname; - // Validate team code by checking if it exists in the database - if (!title.teamCode) { + // if media is empty + if (!req.body.mediaFolder.trim()) { + res.status(400).send({ message: 'Media folder cannot be empty.' }); + return; + } + + if (!req.body.teamCode) { res.status(400).send({ message: 'Please provide a team code.' }); return; } - const teamCodeExists = await checkTeamCodeExists(title.teamCode); + const teamCodeExists = await checkTeamCodeExists(req.body.teamCode); if (!teamCodeExists) { res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); return; } // validate if project exist - const projectExist = await checkProjectExists(title.projectAssigned._id); + const projectExist = await checkProjectExists(req.body.projectAssigned._id); if (!projectExist) { - res.status(400).send({ message: 'Project is empty or not exist.' }); + res.status(400).send({ message: 'Project is empty or not exist~~~' }); return; } // validate if team exist - if (title.teamAssiged && title.teamAssiged._id === 'N/A') { + if (req.body.teamAssiged && req.body.teamAssiged._id === 'N/A') { res.status(400).send({ message: 'Team not exists.' }); return; } + const result = await Title.findById(filter); + result.titleName = req.body.titleName; + result.titleCode = req.body.titleCode; + result.teamCode = req.body.teamCode; + result.projectAssigned = req.body.projectAssigned; + result.mediaFolder = req.body.mediaFolder; + result.teamAssiged = req.body.teamAssiged; + const updatedTitle = await result.save(); + res.status(200).send({ message: 'Update successful', updatedTitle }); + + } catch (error) { + console.log(error); + res.status(500).send({ message: 'An error occurred', error }); + } - title - .save() - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); - }; - - // update title function. - const updateTitle = async function (req, res) { - try{ - - const filter=req.body.id; - - // valid title name - if (!req.body.titleName.trim()) { - res.status(400).send({ message: 'Title cannot be empty.' }); - return; - } - - // if media is empty - if (!req.body.mediaFolder.trim()) { - res.status(400).send({ message: 'Media folder cannot be empty.' }); - return; - } - const shortnames = req.body.titleName.trim().split(' '); - let shortname; - if (shortnames.length > 1) { - shortname = (shortnames[0][0] + shortnames[1][0]).toUpperCase(); - } else if (shortnames.length === 1) { - shortname = shortnames[0][0].toUpperCase(); - } - req.body.shortName = shortname; - - // Validate team code by checking if it exists in the database - if (!req.body.teamCode) { - res.status(400).send({ message: 'Please provide a team code.' }); - return; - } - - const teamCodeExists = await checkTeamCodeExists(req.body.teamCode); - if (!teamCodeExists) { - res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); - return; - } - - // validate if project exist - const projectExist = await checkProjectExists(req.body.projectAssigned._id); - if (!projectExist) { - res.status(400).send({ message: 'Project is empty or not exist.' }); - return; - } - - // validate if team exist - if (req.body.teamAssiged && req.body.teamAssiged._id === 'N/A') { - res.status(400).send({ message: 'Team not exists.' }); - return; - } - const result = await Title.findById(filter); - result.titleName = req.body.titleName; - result.teamCode = req.body.teamCode; - result.projectAssigned = req.body.projectAssigned; - result.mediaFolder = req.body.mediaFolder; - result.teamAssiged = req.body.teamAssiged; - const updatedTitle = await result.save(); - res.status(200).send({ message: 'Update successful', updatedTitle }); - - }catch(error){ - console.log(error); - res.status(500).send({ message: 'An error occurred', error }); - } - - }; - - const deleteTitleById = async function (req, res) { - const { titleId } = req.params; - Title.deleteOne({ _id: titleId }) - .then((result) => res.send(result)) - .catch((error) => res.send(error)); - }; - - const deleteAllTitles = async function (req, res) { - Title.deleteMany({}) - .then((result) => { - if (result.deletedCount === 0) { - res.send({ message: 'No titles found to delete.' }); - } else { - res.send({ message: `${result.deletedCount} titles were deleted successfully.` }); - } - }) - .catch((error) => { - console.log(error) - res.status(500).send(error); - }); - }; - // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. - async function checkTeamCodeExists(teamCode) { - try { - if (cache.getCache('teamCodes')) { - const teamCodes = JSON.parse(cache.getCache('teamCodes')); - return teamCodes.includes(teamCode); + }; + + const deleteTitleById = async function (req, res) { + const { titleId } = req.params; + Title.deleteOne({ _id: titleId }) + .then((result) => res.send(result)) + .catch((error) => res.send(error)); + }; + + const deleteAllTitles = async function (req, res) { + Title.deleteMany({}) + .then((result) => { + if (result.deletedCount === 0) { + res.send({ message: 'No titles found to delete.' }); + } else { + res.send({ message: `${result.deletedCount} titles were deleted successfully.` }); } - const teamCodes = await getAllTeamCodeHelper(); - return teamCodes.includes(teamCode); - } catch (error) { - console.error('Error checking if team code exists:', error); - throw error; - } - } + }) + .catch((error) => { + console.log(error) + res.status(500).send(error); + }); + }; + - async function checkProjectExists(projectID) { - try { - const project = await Project.findOne({ _id: projectID }).exec(); - return !!project; - } catch (error) { - console.error('Error checking if project exists:', error); - throw error; - } - } - return { getAllTitles, @@ -209,5 +212,4 @@ const titlecontroller = function (Title) { }; }; - module.exports = titlecontroller; - \ No newline at end of file +module.exports = titlecontroller; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 31f297872..04805be15 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -17,6 +17,7 @@ const userService = require('../services/userService'); // const { authorizedUserSara, authorizedUserJae } = process.env; const authorizedUserSara = `nathaliaowner@gmail.com`; // To test this code please include your email here const authorizedUserJae = `jae@onecommunityglobal.org`; +const logUserPermissionChangeByAccount = require('../utilities/logUserPermissionChangeByAccount'); const { hasPermission, canRequestorUpdateUser } = require('../utilities/permissions'); const helper = require('../utilities/permissions'); @@ -483,7 +484,6 @@ const userProfileController = function (UserProfile, Project) { let originalRecord = {}; if (PROTECTED_EMAIL_ACCOUNT.includes(record.email)) { originalRecord = objectUtils.deepCopyMongooseObjectWithLodash(record); - // console.log('originalRecord', originalRecord); } // validate userprofile pic @@ -696,6 +696,7 @@ const userProfileController = function (UserProfile, Project) { (await hasPermission(req.body.requestor, 'putUserProfilePermissions')) ) { record.permissions = req.body.permissions; + await logUserPermissionChangeByAccount(req); } if (req.body.endDate !== undefined) { @@ -1493,26 +1494,7 @@ const userProfileController = function (UserProfile, Project) { res.status(200).send({ refreshToken: currentRefreshToken }); }; - // Search for user by first name - // const getUserBySingleName = (req, res) => { - // const pattern = new RegExp(`^${ req.params.singleName}`, 'i'); - - // // Searches for first or last name - // UserProfile.find({ - // $or: [ - // { firstName: { $regex: pattern } }, - // { lastName: { $regex: pattern } }, - // ], - // }) - // .select('firstName lastName') - // .then((users) => { - // if (users.length === 0) { - // return res.status(404).send({ error: 'Users Not Found' }); - // } - // res.status(200).send(users); - // }) - // .catch((error) => res.status(500).send(error)); - // }; + const getUserBySingleName = (req, res) => { const pattern = new RegExp(`^${req.params.singleName}`, 'i'); @@ -1557,13 +1539,7 @@ const userProfileController = function (UserProfile, Project) { .catch((error) => res.status(500).send(error)); }; - // function escapeRegExp(string) { - // return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // } - /** - * Authorizes user to be able to add Weekly Report Recipients - * - */ + const authorizeUser = async (req, res) => { try { let authorizedUser; @@ -1600,6 +1576,43 @@ const userProfileController = function (UserProfile, Project) { } }; + const toggleInvisibility = async function (req, res) { + const { userId } = req.params; + const { isVisible } = req.body; + + if (!mongoose.Types.ObjectId.isValid(userId)) { + res.status(400).send({ + error: 'Bad Request', + }); + return; + } + if (!(await hasPermission(req.body.requestor, 'toggleInvisibility'))) { + res.status(403).send('You are not authorized to change user visibility'); + return; + } + + cache.removeCache(`user-${userId}`); + UserProfile.findByIdAndUpdate(userId, { $set: { isVisible } }, (err, _) => { + if (err) { + return res.status(500).send(`Could not Find user with id ${userId}`); + } + // Check if there's a cache for all users and update it accordingly + const isUserInCache = cache.hasCache('allusers'); + if (isUserInCache) { + const allUserData = JSON.parse(cache.getCache('allusers')); + const userIdx = allUserData.findIndex((users) => users._id === userId); + const userData = allUserData[userIdx]; + userData.isVisible = isVisible; + allUserData.splice(userIdx, 1, userData); + cache.setCache('allusers', JSON.stringify(allUserData)); + } + + return res.status(200).send({ + message: 'User visibility updated successfully', + isVisible, + }); + })} + const addInfringements = async function (req, res) { if (!(await hasPermission(req.body.requestor, 'addInfringements'))) { res.status(403).send('You are not authorized to add blue square'); @@ -1636,7 +1649,17 @@ const userProfileController = function (UserProfile, Project) { record .save() .then((results) => { - userHelper.notifyInfringements(originalinfringements, results.infringements); + userHelper.notifyInfringements( + originalinfringements, + results.infringements, + results.firstName, + results.lastName, + results.email, + results.role, + results.startDate, + results.jobTitle[0], + results.weeklycommittedHours, + ); res.status(200).json({ _id: record._id, }); @@ -1678,7 +1701,17 @@ const userProfileController = function (UserProfile, Project) { record .save() .then((results) => { - userHelper.notifyInfringements(originalinfringements, results.infringements); + userHelper.notifyInfringements( + originalinfringements, + results.infringements, + results.firstName, + results.lastName, + results.email, + results.role, + results.startDate, + results.jobTitle[0], + results.weeklycommittedHours, + ); res.status(200).json({ _id: record._id, }); @@ -1693,7 +1726,6 @@ const userProfileController = function (UserProfile, Project) { return; } const { userId, blueSquareId } = req.params; - // console.log(userId, blueSquareId); UserProfile.findById(userId, async (err, record) => { if (err || !record) { @@ -1710,7 +1742,17 @@ const userProfileController = function (UserProfile, Project) { record .save() .then((results) => { - userHelper.notifyInfringements(originalinfringements, results.infringements); + userHelper.notifyInfringements( + originalinfringements, + results.infringements, + results.firstName, + results.lastName, + results.email, + results.role, + results.startDate, + results.jobTitle[0], + results.weeklycommittedHours, + ); res.status(200).json({ _id: record._id, }); @@ -1728,28 +1770,28 @@ const userProfileController = function (UserProfile, Project) { const query = match[1] ? { - $or: [ - { - firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - { - $and: [ - { firstName: { $regex: new RegExp(`${escapeRegExp(firstName)}`, 'i') } }, - { lastName: { $regex: new RegExp(`${escapeRegExp(lastName)}`, 'i') } }, - ], - }, - ], - } + $or: [ + { + firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + { + $and: [ + { firstName: { $regex: new RegExp(`${escapeRegExp(firstName)}`, 'i') } }, + { lastName: { $regex: new RegExp(`${escapeRegExp(lastName)}`, 'i') } }, + ], + }, + ], + } : { - $or: [ - { - firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - { - lastName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - ], - }; + $or: [ + { + firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + { + lastName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + ], + }; const userProfile = await UserProfile.find(query); @@ -1797,20 +1839,56 @@ const userProfileController = function (UserProfile, Project) { } }; + const getUserByAutocomplete = (req, res) => { + const { searchText } = req.params; + + if (!searchText) { + return res.status(400).send({ message: 'Search text is required' }); + } + + const regex = new RegExp(searchText, 'i'); // Case-insensitive regex for partial matching + + UserProfile.find( + { + $or: [ + { firstName: { $regex: regex } }, + { lastName: { $regex: regex } }, + { + $expr: { + $regexMatch: { + input: { $concat: ['$firstName', ' ', '$lastName'] }, + regex: searchText, + options: 'i', + }, + }, + }, + ], + }, + '_id firstName lastName', // Projection to limit fields returned + ) + .limit(10) // Limit results for performance + .then((results) => { + res.status(200).send(results); + }) + .catch(() => { + res.status(500).send({ error: 'Internal Server Error' }); + }); + }; + const updateUserInformation = async function (req,res){ try { const data=req.body; data.map(async (e)=> { - let result = await UserProfile.findById(e.user_id); + const result = await UserProfile.findById(e.user_id); result[e.item]=e.value - let newdata=await result.save() + await result.save(); }) res.status(200).send({ message: 'Update successful'}); } catch (error) { console.log(error) return res.status(500) } - } + }; return { postUserProfile, @@ -1834,14 +1912,16 @@ const userProfileController = function (UserProfile, Project) { getUserByFullName, changeUserRehireableStatus, authorizeUser, + toggleInvisibility, addInfringements, editInfringements, deleteInfringements, getProjectsByPerson, getAllTeamCode, getAllTeamCodeHelper, + getUserByAutocomplete, + getUserProfileBasicInfo, updateUserInformation, - getUserProfileBasicInfo }; }; diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 381844883..08515c207 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -1,22 +1,45 @@ /* eslint-disable */ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); +const currentWarnings = require('../models/currentWarnings'); +const emailSender = require('../utilities/emailSender'); +const userHelper = require('../helpers/userHelper')(); +let currentWarningDescriptions = null; +let currentUserName = null; +const emailTemplate = { + thirdWarning: { + subject: 'Third Warning', + body: `This is the 3rd time the Admin team has requested the same thing from you. Specifically <“tracked area”>. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.
+Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.
+With Gratitude,
+One Community
`, + }, + fourthWarning: { + subject: 'Fourth Warning', + body: `username !
+This is the 3rd time the Admin team has requested the same thing from you. Specifically <“tracked area”>. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.
+Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.
+With Gratitude,
+One Community
`, + }, +}; +async function getWarningDescriptions() { + currentWarningDescriptions = await currentWarnings.find({}, { warningTitle: 1, _id: 0 }); +} -const descriptions = [ - 'Better Descriptions', - 'Log Time to Tasks', - 'Log Time as You Go', - 'Log Time to Action Items', - 'Intangible Time Log w/o Reason', -]; const warningsController = function (UserProfile) { const getWarningsByUserId = async function (req, res) { + currentWarningDescriptions = await currentWarnings.find({ + activeWarning: true, + }); + + currentWarningDescriptions = currentWarningDescriptions.map((a) => a.warningTitle); const { userId } = req.params; try { const { warnings } = await UserProfile.findById(userId); - const completedData = filterWarnings(warnings); + const { completedData } = filterWarnings(currentWarningDescriptions, warnings); if (!warnings) { return res.status(400).send({ message: 'no valiud records' }); @@ -32,13 +55,20 @@ const warningsController = function (UserProfile) { const { userId } = req.params; const { iconId, color, date, description } = req.body; + const { monitorData } = req.body; const record = await UserProfile.findById(userId); if (!record) { return res.status(400).send({ message: 'No valid records found' }); } - const updatedWarnings = await userProfile.findByIdAndUpdate( + const userAssignedWarning = { + firstName: record.firstName, + lastName: record.lastName, + email: record.email, + }; + + const updatedWarnings = await UserProfile.findByIdAndUpdate( { _id: userId, }, @@ -46,7 +76,24 @@ const warningsController = function (UserProfile) { { new: true, upsert: true }, ); - const completedData = filterWarnings(updatedWarnings.warnings); + const { completedData, sendEmail, size } = filterWarnings( + currentWarningDescriptions, + updatedWarnings.warnings, + iconId, + color, + ); + + const adminEmails = await getUserRoleByEmail(record); + if (sendEmail !== null) { + sendEmailToUser( + sendEmail, + description, + userAssignedWarning, + monitorData, + size, + adminEmails, + ); + } res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { @@ -69,8 +116,8 @@ const warningsController = function (UserProfile) { return res.status(400).send({ message: 'no valid records' }); } - const sortedWarnings = filterWarnings(warnings.warnings); - res.status(201).send({ message: 'succesfully deleted', warnings: sortedWarnings }); + const { completedData } = filterWarnings(currentWarningDescriptions, warnings.warnings); + res.status(201).send({ message: 'succesfully deleted', warnings: completedData }); } catch (error) { res.status(401).send({ message: error.message || error }); } @@ -83,19 +130,80 @@ const warningsController = function (UserProfile) { }; }; -// gests the dsecriptions key from the array -const getDescriptionKey = (val) => { - const descriptions = [ - 'Better Descriptions', - 'Log Time to Tasks', - 'Log Time as You Go', - 'Log Time to Action Items', - 'Intangible Time Log w/o Reason', - ]; - - return descriptions.indexOf(val); +//helper to get the team members admin emails +async function getUserRoleByEmail(user) { + //replacement for jae's email + const recipients = ['test@test.com']; + for (const teamId of user.teams) { + const managementEmails = await userHelper.getTeamManagementEmail(teamId); + if (Array.isArray(managementEmails) && managementEmails.length > 0) { + managementEmails.forEach((management) => { + recipients.push(management.email); + }); + } + } + + return [...new Set(recipients)]; +} + +//helper function to get the ordinal +function getOrdinal(n) { + const suffixes = ['th', 'st', 'nd', 'rd']; + const value = n % 100; + return n + (suffixes[(value - 20) % 10] || suffixes[value] || suffixes[0]); +} +const sendEmailToUser = ( + sendEmail, + warningDescription, + userAssignedWarning, + monitorData, + size, + adminEmails, +) => { + const ordinal = getOrdinal(size); + const subjectTitle = ordinal + ' Warning'; + + const currentUserName = `${userAssignedWarning.firstName} ${userAssignedWarning.lastName}`; + const emailTemplate = + sendEmail === 'issue warning' + ? `Hello ${currentUserName},
+This is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}. Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, don’t hesitate to ask questions—the Admin team is here to assist.
+Moving forward, please ensure this issue is resolved. Repeated requests for the same thing require unnecessary administrative attention and may result in a blue square being issued if it happens again.
+The Admin member who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}. Please comment on your Google Doc and tag them using this email if you have any questions.
+With Gratitude,
+One Community
` + : `Hello ${currentUserName},
+A blue square has been issued because this is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}.
+Moving forward, please ensure this is resolved. Repeated requests for the same thing require unnecessary administrative attention, will result in an additional blue square being issued, and could lead to termination.
+Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, feel free to ask questions—the Admin team is here to help.
+The Admin member who issued this blue square is ${monitorData.firstName} ${monitorData.lastName} and can be reached at ${monitorData.email}. If you have any questions, please comment on your Google Doc and tag them using this email.
+With Gratitude,
+One Community
`; + + if (sendEmail === 'issue warning') { + emailSender( + `${userAssignedWarning.email}`, + subjectTitle, + emailTemplate, + null, + adminEmails.toString(), + null, + ); + } else if (sendEmail === 'issue blue square') { + emailSender( + `${userAssignedWarning.email}`, + `Blue Square issued for ${warningDescription}`, + null, + emailTemplate, + adminEmails.toString(), + null, + ); + } }; +// gets the dsecriptions key from the array +const getDescriptionKey = (val) => currentWarningDescriptions.indexOf(val); + const sortKeysAlphabetically = (a, b) => getDescriptionKey(a) - getDescriptionKey(b); // method to see which color is first @@ -118,14 +226,29 @@ const sortByColorAndDate = (a, b) => { return colorComparison; }; -const filterWarnings = (warnings) => { +const filterWarnings = (currentWarningDescriptions, warnings, iconId = null, color = null) => { const warningsObject = {}; + let sendEmail = null; + let size = null; + warnings.forEach((warning) => { if (!warningsObject[warning.description]) { warningsObject[warning.description] = []; } warningsObject[warning.description].push(warning); + + if ( + warningsObject[warning.description].length >= 3 && + warning.iconId === iconId && + color === 'yellow' + ) { + sendEmail = 'issue warning'; + size = warningsObject[warning.description].length; + } else if (warning.iconId === iconId && color === 'red') { + sendEmail = 'issue blue square'; + size = warningsObject[warning.description].length; + } }); const warns = Object.keys(warningsObject) @@ -141,13 +264,13 @@ const filterWarnings = (warnings) => { const completedData = []; - for (const descrip of descriptions) { + for (const descrip of currentWarningDescriptions) { completedData.push({ title: descrip, warnings: warns[descrip] ? warns[descrip] : [], }); } - return completedData; + return { completedData, sendEmail, size }; }; module.exports = warningsController; diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index e7a8662a6..e773847d2 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -11,6 +11,7 @@ const userProfileJobs = () => { async () => { const SUNDAY = 0; // will change back to 0 after fix if (moment().tz('America/Los_Angeles').day() === SUNDAY) { + console.log('Running Cron Jobs'); await userhelper.assignBlueSquareForTimeNotMet(); await userhelper.applyMissedHourForCoreTeam(); await userhelper.emailWeeklySummariesForAllUsers(); diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index 08e0bc907..dddb7cca2 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -181,14 +181,11 @@ const dashboardhelper = function () { _myTeam.members.forEach((teamMember) => { if (teamMember.userId.equals(userid) && teamMember.visible) isUserVisible = true; }); - if(isUserVisible) - { + if (isUserVisible) { _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) - teamMemberIds.push(teamMember.userId); - }); - } - + if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); + }); + } }); teamMembers = await userProfile.find( @@ -204,8 +201,7 @@ const dashboardhelper = function () { timeOffTill: 1, endDate: 1, missedHours: 1, - } - + }, ); } else { // 'Core Team', 'Owner' //All users @@ -270,7 +266,7 @@ const dashboardhelper = function () { ? teamMember.weeklySummaries[0].summary !== '' : false, weeklycommittedHours: teamMember.weeklycommittedHours, - missedHours: (teamMember.missedHours ?? 0), + missedHours: teamMember.missedHours ?? 0, totaltangibletime_hrs: (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds ?? 0) / 3600, totalintangibletime_hrs: @@ -311,255 +307,6 @@ const dashboardhelper = function () { console.log(error); return new Error(error); } - - // return myTeam.aggregate([ - // { - // $match: { - // _id: userid, - // }, - // }, - // { - // $unwind: '$myteam', - // }, - // { - // $project: { - // _id: 0, - // role: 1, - // personId: '$myteam._id', - // name: '$myteam.fullName', - // }, - // }, - // { - // $lookup: { - // from: 'userProfiles', - // localField: 'personId', - // foreignField: '_id', - // as: 'persondata', - // }, - // }, - // { - // $match: { - // // leaderboard user roles hierarchy - // $or: [ - // { - // role: { $in: ['Owner', 'Core Team'] }, - // }, - // { - // $and: [ - // { - // role: 'Administrator', - // }, - // { 'persondata.0.role': { $nin: ['Owner', 'Administrator'] } }, - // ], - // }, - // { - // $and: [ - // { - // role: { $in: ['Manager', 'Mentor'] }, - // }, - // { - // 'persondata.0.role': { - // $nin: ['Manager', 'Mentor', 'Core Team', 'Administrator', 'Owner'], - // }, - // }, - // ], - // }, - // { 'persondata.0._id': userId }, - // { 'persondata.0.role': 'Volunteer' }, - // { 'persondata.0.isVisible': true }, - // ], - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: { - // $arrayElemAt: ['$persondata.role', 0], - // }, - // isVisible: { - // $arrayElemAt: ['$persondata.isVisible', 0], - // }, - // hasSummary: { - // $ne: [ - // { - // $arrayElemAt: [ - // { - // $arrayElemAt: ['$persondata.weeklySummaries.summary', 0], - // }, - // 0, - // ], - // }, - // '', - // ], - // }, - // weeklycommittedHours: { - // $sum: [ - // { - // $arrayElemAt: ['$persondata.weeklycommittedHours', 0], - // }, - // { - // $ifNull: [{ $arrayElemAt: ['$persondata.missedHours', 0] }, 0], - // }, - // ], - // }, - // }, - // }, - // { - // $lookup: { - // from: 'timeEntries', - // localField: 'personId', - // foreignField: 'personId', - // as: 'timeEntryData', - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: 1, - // isVisible: 1, - // hasSummary: 1, - // weeklycommittedHours: 1, - // timeEntryData: { - // $filter: { - // input: '$timeEntryData', - // as: 'timeentry', - // cond: { - // $and: [ - // { - // $gte: ['$$timeentry.dateOfWork', pdtstart], - // }, - // { - // $lte: ['$$timeentry.dateOfWork', pdtend], - // }, - // ], - // }, - // }, - // }, - // }, - // }, - // { - // $unwind: { - // path: '$timeEntryData', - // preserveNullAndEmptyArrays: true, - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: 1, - // isVisible: 1, - // hasSummary: 1, - // weeklycommittedHours: 1, - // totalSeconds: { - // $cond: [ - // { - // $gte: ['$timeEntryData.totalSeconds', 0], - // }, - // '$timeEntryData.totalSeconds', - // 0, - // ], - // }, - // isTangible: { - // $cond: [ - // { - // $gte: ['$timeEntryData.totalSeconds', 0], - // }, - // '$timeEntryData.isTangible', - // false, - // ], - // }, - // }, - // }, - // { - // $addFields: { - // tangibletime: { - // $cond: [ - // { - // $eq: ['$isTangible', true], - // }, - // '$totalSeconds', - // 0, - // ], - // }, - // intangibletime: { - // $cond: [ - // { - // $eq: ['$isTangible', false], - // }, - // '$totalSeconds', - // 0, - // ], - // }, - // }, - // }, - // { - // $group: { - // _id: { - // personId: '$personId', - // weeklycommittedHours: '$weeklycommittedHours', - // name: '$name', - // role: '$role', - // isVisible: '$isVisible', - // hasSummary: '$hasSummary', - // }, - // totalSeconds: { - // $sum: '$totalSeconds', - // }, - // tangibletime: { - // $sum: '$tangibletime', - // }, - // intangibletime: { - // $sum: '$intangibletime', - // }, - // }, - // }, - // { - // $project: { - // _id: 0, - // personId: '$_id.personId', - // name: '$_id.name', - // role: '$_id.role', - // isVisible: '$_id.isVisible', - // hasSummary: '$_id.hasSummary', - // weeklycommittedHours: '$_id.weeklycommittedHours', - // totaltime_hrs: { - // $divide: ['$totalSeconds', 3600], - // }, - // totaltangibletime_hrs: { - // $divide: ['$tangibletime', 3600], - // }, - // totalintangibletime_hrs: { - // $divide: ['$intangibletime', 3600], - // }, - // percentagespentintangible: { - // $cond: [ - // { - // $eq: ['$totalSeconds', 0], - // }, - // 0, - // { - // $multiply: [ - // { - // $divide: ['$tangibletime', '$totalSeconds'], - // }, - // 100, - // ], - // }, - // ], - // }, - // }, - // }, - // { - // $sort: { - // totaltangibletime_hrs: -1, - // name: 1, - // role: 1, - // }, - // }, - // ]); }; /** diff --git a/src/helpers/overviewReportHelper.js b/src/helpers/overviewReportHelper.js index 52d6a2ad0..cb1cd8edb 100644 --- a/src/helpers/overviewReportHelper.js +++ b/src/helpers/overviewReportHelper.js @@ -74,6 +74,8 @@ const overviewReportHelper = function () { _id: 1, firstName: 1, lastName: 1, + email: 1, + profilePic: 1, }, }, ]); diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 9ea062422..168d704b1 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -146,14 +146,14 @@ const userHelper = function () { .localeData() .ordinal( totalInfringements, - )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your - requirement this week. This is in addition to any hours missed for last week: - ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours + )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your + requirement this week. This is in addition to any hours missed for last week: + ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours owed for this being your ${moment .localeData() .ordinal( totalInfringements, - )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. + )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. .`; } // bold description for 'System auto-assigned infringement for two reasons ....' and 'not submitting a weekly summary' and logged hrs @@ -204,7 +204,7 @@ const userHelper = function () {One Community
-ADMINISTRATIVE DETAILS:
Start Date: ${administrativeContent.startDate}
Role: ${administrativeContent.role}
@@ -1049,7 +1049,7 @@ const userHelper = function () { }, ); - logger.logInfo(`Job deleting blue squares older than 1 year finished + logger.logInfo(`Job deleting blue squares older than 1 year finished at ${moment().tz('America/Los_Angeles').format()} \nReulst: ${JSON.stringify(results)}`); } catch (err) { logger.logException(err); @@ -1099,11 +1099,11 @@ const userHelper = function () { const emailBody = `Hi Admin!
This email is to let you know that ${person.firstName} ${person.lastName} has been made active again in the Highest Good Network application after being paused on ${endDate}.
- +If you need to communicate anything with them, this is their email from the system: ${person.email}.
- +Thanks!
- +The HGN A.I. (and One Community)
`; emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null, person.email); @@ -2066,74 +2066,70 @@ const userHelper = function () { sendThreeWeeks, followup, ) { - let subject; - let emailBody; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); + let subject; + let emailBody; + recipients.push('onecommunityglobal@gmail.com'); + recipients = recipients.toString(); if (reactivationDate) { subject = `IMPORTANT: ${firstName} ${lastName} has been PAUSED in the Highest Good Network`; emailBody = `Management,
Please note that ${firstName} ${lastName} has been PAUSED in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part until they return on ${moment(reactivationDate).format('M-D-YYYY')}.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); } else if (endDate && isSet && sendThreeWeeks) { const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; const emailBody = `Management,
- +Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
This is more than 3 weeks from now, but you should still start confirming all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.
An additional reminder email will be sent in their final 2 weeks.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - } else if (endDate && isSet && followup) { subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; emailBody = `Management,
- +Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
This is coming up soon. For a smooth transition, please confirm all your work is wrapped up with this individual and nothing further will be needed on their part after this date.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - - } else if (endDate && isSet ) { + } else if (endDate && isSet) { subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; emailBody = `Management,
- +Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - - } else if(endDate){ + } else if (endDate) { subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; emailBody = `Management,
- +Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - }; - } - + }; + const deActivateUser = async () => { try { const emailReceivers = await userProfile.find( @@ -2149,14 +2145,18 @@ const userHelper = function () { const user = users[i]; const { endDate, finalEmailThreeWeeksSent } = user; endDate.setHours(endDate.getHours() + 7); - // notify reminder set final day before 2 weeks - if(finalEmailThreeWeeksSent && moment().isBefore(moment(endDate).subtract(2, 'weeks')) && moment().isAfter(moment(endDate).subtract(3, 'weeks'))){ + // notify reminder set final day before 2 weeks + if ( + finalEmailThreeWeeksSent && + moment().isBefore(moment(endDate).subtract(2, 'weeks')) && + moment().isAfter(moment(endDate).subtract(3, 'weeks')) + ) { const id = user._id; const person = await userProfile.findById(id); const lastDay = moment(person.endDate).format('YYYY-MM-DD'); logger.logInfo(`User with id: ${user._id}'s final Day is set at ${moment().format()}.`); person.teams.map(async (teamId) => { - const managementEmails = await userHelper.getTeamManagementEmail(teamId); + const managementEmails = await userHelper.getTeamManagementEmail(teamId); if (Array.isArray(managementEmails) && managementEmails.length > 0) { managementEmails.forEach((management) => { recipients.push(management.email); diff --git a/src/models/currentWarnings.js b/src/models/currentWarnings.js new file mode 100644 index 000000000..18a446199 --- /dev/null +++ b/src/models/currentWarnings.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const currentWarnings = new Schema({ + warningTitle: { type: String, required: true }, + activeWarning: { type: Boolean, required: true }, + isPermanent: { type: Boolean, required: true }, +}); + +module.exports = mongoose.model('currentWarning', currentWarnings, 'currentWarnings'); diff --git a/src/models/jobs.js b/src/models/jobs.js new file mode 100644 index 000000000..b1f98a34a --- /dev/null +++ b/src/models/jobs.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const jobSchema = new Schema({ + title: { type: String, required: true }, // Job title + category: { type: String, required: true }, // General category (e.g., Engineering, Marketing) + description: { type: String, required: true }, // Detailed job description + imageUrl: { type: String, required: true }, // URL of the job-related image + location: { type: String, required: true }, // Job location (optional for remote jobs) + applyLink: { type: String, required: true }, // URL for the application form + featured: { type: Boolean, default: false }, // Whether the job should be featured prominently + datePosted: { type: Date, default: Date.now }, // Date the job was posted + jobDetailsLink: { type: String, required: true }, // Specific job details URL +}); + +module.exports = mongoose.model('Job', jobSchema); diff --git a/src/models/team.js b/src/models/team.js index 4d73615f5..109d93221 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -3,9 +3,9 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; /** - * This schema represents a team in the system. - * - * Deprecated field: teamCode. Team code is no longer associated with a team. + * This schema represents a team in the system. + * + * Deprecated field: teamCode. Team code is no longer associated with a team. * Team code is used as a text string identifier in the user profile data model. */ const team = new Schema({ @@ -15,9 +15,10 @@ const team = new Schema({ modifiedDatetime: { type: Date, default: Date.now() }, members: [ { - userId: { type: mongoose.SchemaTypes.ObjectId, required: true }, + userId: { type: mongoose.SchemaTypes.ObjectId, required: true, index : true }, addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, visible: { type : 'Boolean', default:true}, + }, ], // Deprecated field @@ -35,4 +36,5 @@ const team = new Schema({ }, }); + module.exports = mongoose.model('team', team, 'teams'); diff --git a/src/models/timeentry.js b/src/models/timeentry.js index ea5303b3a..1535ab13e 100644 --- a/src/models/timeentry.js +++ b/src/models/timeentry.js @@ -17,5 +17,7 @@ const TimeEntry = new Schema({ lastModifiedDateTime: { type: Date, default: Date.now }, isActive: { type: Boolean, default: true }, }); +TimeEntry.index({ personId: 1, dateOfWork: 1 }); +TimeEntry.index({ entryType: 1, teamId: 1, dateOfWork: 1, isActive: 1 }); module.exports = mongoose.model('timeEntry', TimeEntry, 'timeEntries'); diff --git a/src/models/title.js b/src/models/title.js index 64b9aed92..a41063aea 100644 --- a/src/models/title.js +++ b/src/models/title.js @@ -4,6 +4,7 @@ const { Schema } = mongoose; const title = new Schema({ titleName: { type: String, required: true }, + titleCode: { type: String, required: true }, teamCode: { type: String, require: true }, projectAssigned: { projectName: { type: String, required: true }, @@ -13,8 +14,7 @@ const title = new Schema({ teamAssiged: { teamName: { type: String }, _id: { type: String }, - }, - shortName: { type: String, require: true }, + }, }); diff --git a/src/models/userPermissionChangeLog.js b/src/models/userPermissionChangeLog.js new file mode 100644 index 000000000..051a597dc --- /dev/null +++ b/src/models/userPermissionChangeLog.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const User = require('./userProfile'); + + +const UserPermissionChangeLog = new Schema({ + logDateTime: { type: String, required: true }, + userId: { + type: mongoose.Types.ObjectId, + ref: User, + required: true, + }, + individualName: { type: String }, + permissions: { type: [String], required: true }, + permissionsAdded: { type: [String], default: [] }, + permissionsRemoved: { type: [String], default: [] }, + requestorRole: { type: String }, + requestorEmail: { type: String, required: true }, +}); + +module.exports = mongoose.model('UserPermissionChangeLog', UserPermissionChangeLog, 'UserPermissionChangeLogs'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 3a529294a..61e8e8f91 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -76,7 +76,7 @@ const userProfileSchema = new Schema({ startDate: { type: Date, required: true, - default () { + default() { return this.createdDate; }, }, @@ -128,6 +128,7 @@ const userProfileSchema = new Schema({ required: true, default: 'white', }, + iconId: { type: String, required: true }, }, ], location: { diff --git a/src/routes/curentWarningsRouter.js b/src/routes/curentWarningsRouter.js new file mode 100644 index 000000000..f1a004493 --- /dev/null +++ b/src/routes/curentWarningsRouter.js @@ -0,0 +1,23 @@ +const express = require('express'); + +const route = function (currentWarnings) { + const controller = require('../controllers/currentWarningsController')(currentWarnings); + + const currentWarningsRouter = express.Router(); + + currentWarningsRouter + .route('/currentWarnings') + .get(controller.getCurrentWarnings) + .post(controller.postNewWarningDescription); + + currentWarningsRouter.route('/currentWarnings/edit').put(controller.editWarningDescription); + + currentWarningsRouter + .route('/currentWarnings/:warningDescriptionId') + .delete(controller.deleteWarningDescription) + .put(controller.updateWarningDescription); + + return currentWarningsRouter; +}; + +module.exports = route; diff --git a/src/routes/jobsRouter.js b/src/routes/jobsRouter.js new file mode 100644 index 000000000..6587ebb92 --- /dev/null +++ b/src/routes/jobsRouter.js @@ -0,0 +1,16 @@ +const express = require('express'); +const jobsController = require('../controllers/jobsController'); // Adjust the path if needed + +const router = express.Router(); + +// Define routes +router.get('/suggestions', jobsController.getJobTitleSuggestions); +router.get('/reset-filters', jobsController.resetJobsFilters); +router.get('/summaries', jobsController.getJobSummaries); +router.get('/', jobsController.getJobs); +router.get('/categories', jobsController.getCategories); +router.get('/:id', jobsController.getJobById); +router.post('/', jobsController.createJob); +router.put('/:id', jobsController.updateJob); +router.delete('/:id', jobsController.deleteJob); +module.exports = router; diff --git a/src/routes/permissionChangeLogsRouter.js b/src/routes/permissionChangeLogsRouter.js index 50ed7696b..32d3323fd 100644 --- a/src/routes/permissionChangeLogsRouter.js +++ b/src/routes/permissionChangeLogsRouter.js @@ -1,7 +1,7 @@ const express = require('express'); -const routes = function (permissionChangeLog) { - const controller = require('../controllers/permissionChangeLogsController')(permissionChangeLog); +const routes = function (permissionChangeLog, userPermissionChangeLog) { + const controller = require('../controllers/permissionChangeLogsController')(permissionChangeLog, userPermissionChangeLog); const permissionChangeLogRouter = express.Router(); diff --git a/src/routes/teamRouter.js b/src/routes/teamRouter.js index 1bf8cfc44..3fbd8abb5 100644 --- a/src/routes/teamRouter.js +++ b/src/routes/teamRouter.js @@ -11,6 +11,10 @@ const router = function (team) { .post(controller.postTeam) .put(controller.updateTeamVisibility); + teamRouter + .route("/team/reports") + .post(controller.getAllTeamMembers); + teamRouter .route('/team/:teamId') .get(controller.getTeamById) diff --git a/src/routes/titleRouter.js b/src/routes/titleRouter.js index ce00e2279..1bced1e08 100644 --- a/src/routes/titleRouter.js +++ b/src/routes/titleRouter.js @@ -7,7 +7,7 @@ const router = function (title) { titleRouter.route('/title') .get(controller.getAllTitles) .post(controller.postTitle) - // .put(controller.putTitle); + // .put(controller.putTitle); titleRouter.route('/title/update').post(controller.updateTitle); diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index 73bc2ac5b..5babb9384 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -73,6 +73,10 @@ const routes = function (userProfile, project) { .route('/userProfile/:userId/rehireable') .patch(controller.changeUserRehireableStatus); + userProfileRouter + .route('/userProfile/:userId/toggleInvisibility') + .patch(controller.toggleInvisibility); + userProfileRouter .route('/userProfile/singleName/:singleName') .get(controller.getUserBySingleName); @@ -116,6 +120,10 @@ const routes = function (userProfile, project) { userProfileRouter.route('/userProfile/teamCode/list').get(controller.getAllTeamCode); + userProfileRouter + .route('/userProfile/autocomplete/:searchText') + .get(controller.getUserByAutocomplete); + return userProfileRouter; }; diff --git a/src/startup/middleware.js b/src/startup/middleware.js index 6304f1840..400f5af6c 100644 --- a/src/startup/middleware.js +++ b/src/startup/middleware.js @@ -39,6 +39,10 @@ module.exports = function (app) { next(); return; } + if (req.originalUrl.startsWith('/api/jobs') && req.method === 'GET') { + next(); + return; + } if (!req.header('Authorization')) { res.status(401).send({ 'error:': 'Unauthorized request' }); return; diff --git a/src/startup/routes.js b/src/startup/routes.js index a72527430..409c7c29a 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -4,7 +4,6 @@ const project = require('../models/project'); const information = require('../models/information'); const team = require('../models/team'); // const actionItem = require('../models/actionItem'); -const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); const popup = require('../models/popupEditor'); @@ -16,6 +15,8 @@ const inventoryItemType = require('../models/inventoryItemType'); const role = require('../models/role'); const rolePreset = require('../models/rolePreset'); const ownerMessage = require('../models/ownerMessage'); +const currentWarnings = require('../models/currentWarnings'); + // Title const title = require('../models/title'); const blueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); @@ -25,6 +26,7 @@ const profileInitialSetuptoken = require('../models/profileInitialSetupToken'); const reason = require('../models/reason'); const mouseoverText = require('../models/mouseoverText'); const permissionChangeLog = require('../models/permissionChangeLog'); +const userPermissionChangeLog = require('../models/userPermissionChangeLog'); const mapLocations = require('../models/mapLocation'); const buildingProject = require('../models/bmdashboard/buildingProject'); const buildingNewLesson = require('../models/bmdashboard/buildingNewLesson'); @@ -49,12 +51,14 @@ const followUp = require('../models/followUp'); const userProfileRouter = require('../routes/userProfileRouter')(userProfile, project); const warningRouter = require('../routes/warningRouter')(userProfile); +const currentWarningsRouter = require('../routes/curentWarningsRouter')(currentWarnings); const badgeRouter = require('../routes/badgeRouter')(badge); const dashboardRouter = require('../routes/dashboardRouter')(weeklySummaryAIPrompt); const timeEntryRouter = require('../routes/timeentryRouter')(timeEntry); const projectRouter = require('../routes/projectRouter')(project); const informationRouter = require('../routes/informationRouter')(information); const teamRouter = require('../routes/teamRouter')(team); +const jobsRouter = require('../routes/jobsRouter'); // const actionItemRouter = require('../routes/actionItemRouter')(actionItem); const notificationRouter = require('../routes/notificationRouter')(); const loginRouter = require('../routes/loginRouter')(); @@ -75,7 +79,7 @@ const profileInitialSetupRouter = require('../routes/profileInitialSetupRouter') mapLocations, ); const permissionChangeLogRouter = require('../routes/permissionChangeLogsRouter')( - permissionChangeLog, + permissionChangeLog, userPermissionChangeLog, ); const isEmailExistsRouter = require('../routes/isEmailExistsRouter')(); @@ -160,11 +164,13 @@ module.exports = function (app) { app.use('/api', isEmailExistsRouter); app.use('/api', mapLocationRouter); app.use('/api', warningRouter); + app.use('/api', currentWarningsRouter); app.use('/api', titleRouter); app.use('/api', timeOffRequestRouter); app.use('/api', followUpRouter); app.use('/api', blueSquareEmailAssignmentRouter); app.use('/api',formRouter); + app.use('/api/jobs', jobsRouter); // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); diff --git a/src/test/mock-request.js b/src/test/mock-request.js index f61d4d89a..52df892df 100644 --- a/src/test/mock-request.js +++ b/src/test/mock-request.js @@ -9,7 +9,7 @@ const mockReq = { requestorId: '65cf6c3706d8ac105827bb2e', // this one matches the id of the db/createUser for testing purposes }, }, - params: { userid: '5a7e21f00317bc1538def4b7', userId: '5a7e21f00317bc1538def4b7' }, + params: { userid: '5a7e21f00317bc1538def4b7', userId: '5a7e21f00317bc1538def4b7', teamId: '5a8e21f00317bc' }, }; module.exports = mockReq; diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 3a214bbec..7344c6016 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -52,6 +52,7 @@ const permissionsRoles = [ 'changeUserRehireableStatus', 'updatePassword', 'deleteUserProfile', + 'toggleInvisibility', 'addInfringements', 'editInfringements', 'deleteInfringements', @@ -256,8 +257,9 @@ const permissionsRoles = [ 'seeUsersInDashboard', 'changeUserRehireableStatus', + 'toggleInvisibility', 'manageAdminLinks', - + 'removeUserFromTask', 'editHeaderMessage', ], }, diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index eb8eca3de..b0fb40112 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -2,104 +2,113 @@ const nodemailer = require('nodemailer'); const { google } = require('googleapis'); const logger = require('../startup/logger'); -const closure = () => { - const queue = []; - - const CLIENT_EMAIL = process.env.REACT_APP_EMAIL; - const CLIENT_ID = process.env.REACT_APP_EMAIL_CLIENT_ID; - const CLIENT_SECRET = process.env.REACT_APP_EMAIL_CLIENT_SECRET; - const REDIRECT_URI = process.env.REACT_APP_EMAIL_CLIENT_REDIRECT_URI; - const REFRESH_TOKEN = process.env.REACT_APP_EMAIL_REFRESH_TOKEN; - // Create the email envelope (transport) - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - type: 'OAuth2', - user: CLIENT_EMAIL, - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - }, - }); +const config = { + email: process.env.REACT_APP_EMAIL, + clientId: process.env.REACT_APP_EMAIL_CLIENT_ID, + clientSecret: process.env.REACT_APP_EMAIL_CLIENT_SECRET, + redirectUri: process.env.REACT_APP_EMAIL_CLIENT_REDIRECT_URI, + refreshToken: process.env.REACT_APP_EMAIL_REFRESH_TOKEN, + batchSize: 50, + concurrency: 3, + rateLimitDelay: 1000, +}; - const OAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); +const OAuth2Client = new google.auth.OAuth2( + config.clientId, + config.clientSecret, + config.redirectUri, +); +OAuth2Client.setCredentials({ refresh_token: config.refreshToken }); - OAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN }); +// Create the email envelope (transport) +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + type: 'OAuth2', + user: config.email, + clientId: config.clientId, + clientSecret: config.clientSecret, + }, +}); - setInterval(async () => { - const nextItem = queue.shift(); +const sendEmail = async (mailOptions) => { + try { + const { token } = await OAuth2Client.getAccessToken(); + mailOptions.auth = { + user: config.email, + refreshToken: config.refreshToken, + accessToken: token, + }; + const result = await transporter.sendMail(mailOptions); + if (process.env.NODE_ENV === 'local') { + logger.logInfo(`Email sent: ${JSON.stringify(result)}`); + } + return result; + } catch (error) { + logger.logException(error, `Error sending email: ${mailOptions.to}`); + throw error; + } +}; - if (!nextItem) return; +const queue = []; +let isProcessing = false; - const { recipient, subject, message, cc, bcc, replyTo, acknowledgingReceipt } = nextItem; +const processQueue = async () => { + if (isProcessing || queue.length === 0) return; - try { - // Generate the accessToken on the fly - const res = await OAuth2Client.getAccessToken(); - const ACCESSTOKEN = res.token; + isProcessing = true; + console.log('Processing email queue...'); - const mailOptions = { - from: CLIENT_EMAIL, - to: recipient, - cc, - bcc, - subject, - html: message, - replyTo, - auth: { - user: CLIENT_EMAIL, - refreshToken: REFRESH_TOKEN, - accessToken: ACCESSTOKEN, - }, - }; + const processBatch = async () => { + if (queue.length === 0) { + isProcessing = false; + return; + } - const result = await transporter.sendMail(mailOptions); - if (typeof acknowledgingReceipt === 'function') { - acknowledgingReceipt(null, result); - } - // Prevent logging email in production - // Why? - // 1. Could create a security risk - // 2. Could create heavy loads on the server if emails are sent to many people - // 3. Contain limited useful info: - // result format : {"accepted":["emailAddr"],"rejected":[],"envelopeTime":209,"messageTime":566,"messageSize":317,"response":"250 2.0.0 OK 17***69 p11-2***322qvd.85 - gsmtp","envelope":{"from":"emailAddr", "to":"emailAddr"}} - if (process.env.NODE_ENV === 'local') { - logger.logInfo(`Email sent: ${JSON.stringify(result)}`); - } + const batch = queue.shift(); + try { + console.log('Sending email...'); + await sendEmail(batch); } catch (error) { - if (typeof acknowledgingReceipt === 'function') { - acknowledgingReceipt(error, null); - } - logger.logException( - error, - `Error sending email: from ${CLIENT_EMAIL} to ${recipient} subject ${subject}`, - `Extra Data: cc ${cc} bcc ${bcc}`, - ); + logger.logException(error, 'Failed to send email batch'); } - }, process.env.MAIL_QUEUE_INTERVAL || 1000); - const emailSender = function ( - recipient, - subject, - message, - cc = null, - bcc = null, - replyTo = null, - acknowledgingReceipt = null, - ) { - if (process.env.sendEmail) { - queue.push({ - recipient, - subject, - message, - cc, - bcc, - replyTo, - acknowledgingReceipt, - }); - } + setTimeout(processBatch, config.rateLimitDelay); }; - return emailSender; + const concurrentProcesses = Array(config.concurrency).fill().map(processBatch); + + try { + await Promise.all(concurrentProcesses); + } finally { + isProcessing = false; + } +}; + +const emailSender = ( + recipients, + subject, + message, + attachments = null, + cc = null, + replyTo = null, +) => { + if (!process.env.sendEmail) return; + const recipientsArray = Array.isArray(recipients) ? recipients : [recipients]; + for (let i = 0; i < recipients.length; i += config.batchSize) { + const batchRecipients = recipientsArray.slice(i, i + config.batchSize); + queue.push({ + from: config.email, + bcc: batchRecipients.join(','), + subject, + html: message, + attachments, + cc, + replyTo, + }); + } + console.log('Emails queued:', queue.length); + setImmediate(processQueue); }; -module.exports = closure(); +module.exports = emailSender; diff --git a/src/utilities/logPermissionChangeByAccount.js b/src/utilities/logPermissionChangeByAccount.js index f47c0c5b2..b08e8ee34 100644 --- a/src/utilities/logPermissionChangeByAccount.js +++ b/src/utilities/logPermissionChangeByAccount.js @@ -24,6 +24,10 @@ const changedPermissionsLogger = async (req, res, next) => { permissionsAdded = permissions; } + if (permissionsAdded.length === 0 && permissionsRemoved.length === 0) { + return next(); // No changes, proceed without saving a log + } + const logEntry = new PermissionChangeLog({ logDateTime: dateTime, roleId, diff --git a/src/utilities/logUserPermissionChangeByAccount.js b/src/utilities/logUserPermissionChangeByAccount.js new file mode 100644 index 000000000..700bceaf1 --- /dev/null +++ b/src/utilities/logUserPermissionChangeByAccount.js @@ -0,0 +1,58 @@ +const moment = require('moment-timezone'); +const UserPermissionChangeLog = require('../models/userPermissionChangeLog'); +const UserProfile = require('../models/userProfile'); + +const logUserPermissionChangeByAccount = async (req) => { + const { permissions, firstName, lastName, requestor } = req.body; + const dateTime = moment().tz('America/Los_Angeles').format(); + + try { + let permissionsAdded = []; + let permissionsRemoved = []; + const { userId } = req.params; + const Permissions = permissions.frontPermissions; + const requestorEmailId = await UserProfile.findById(requestor.requestorId).select('email').exec(); + const document = await findLatestRelatedLog(userId); + + if (document) { + const docPermissions = Array.isArray(document.permissions) ? document.permissions : []; + if(JSON.stringify(docPermissions) === JSON.stringify(Permissions)) { + return; + } + permissionsRemoved = docPermissions.filter((item) => !Permissions.includes(item)); + permissionsAdded = Permissions.filter((item) => !docPermissions.includes(item)); + } else { + permissionsAdded = Permissions; + } + + const logEntry = new UserPermissionChangeLog({ + logDateTime: dateTime, + userId, + individualName: `INDIVIDUAL: ${firstName} ${lastName}`, + permissions: Permissions, + permissionsAdded, + permissionsRemoved, + requestorRole: requestor.role, + requestorEmail: requestorEmailId.email, + }); + + await logEntry.save(); + console.log('Permission change logged successfully'); + } catch (error) { + console.error('Error logging permission change:', error); + } +}; + +const findLatestRelatedLog = (userId) => new Promise((resolve, reject) => { + UserPermissionChangeLog.findOne({ userId }) + .sort({ logDateTime: -1 }) + .exec((err, document) => { + if (err) { + reject(err); + return; + } + resolve(document); + }); +}); + +module.exports = logUserPermissionChangeByAccount; diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index 2299e8812..75aee3b59 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -64,10 +64,10 @@ const canRequestorUpdateUser = async (requestorId, targetUserId) => { // Find out a list of protected email account ids and allowed email id allowedEmailAccountIds = query .filter(({ email }) => ALLOWED_EMAIL_ACCOUNT.includes(email)) - .map(({ _id }) => _id); + .map(({ _id }) => _id.toString()); protectedEmailAccountIds = query .filter(({ email }) => PROTECTED_EMAIL_ACCOUNT.includes(email)) - .map(({ _id }) => _id); + .map(({ _id }) => _id.toString()); serverCache.setCache('protectedEmailAccountIds', protectedEmailAccountIds); serverCache.setCache('allowedEmailAccountIds', allowedEmailAccountIds);