From fd3d2f15d7b34040d90a0b4dd86166a3ccced7ad Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 20 Feb 2024 14:30:50 +0530 Subject: [PATCH 01/67] user service documentation --- src/api-doc/user-service-doc.md | 544 ++++++++++++++++++++++++++++++++ 1 file changed, 544 insertions(+) create mode 100644 src/api-doc/user-service-doc.md diff --git a/src/api-doc/user-service-doc.md b/src/api-doc/user-service-doc.md new file mode 100644 index 000000000..bbc0be058 --- /dev/null +++ b/src/api-doc/user-service-doc.md @@ -0,0 +1,544 @@ +# ShikshaLokam Elevate Project Documentation + +## User Service + +### System Requirements + +- **Operating System:** Ubuntu 22 +- **Node.js:** v20 +- **PostgreSQL:** 16 +- **Citus:** 12.1 + +## Install Node.js + +Refer to the [NodeSource distributions installation scripts](https://github.com/nodesource/distributions#installation-scripts) for Node.js installation. + +```bash +$ curl -SLO https://deb.nodesource.com/nsolid_setup_deb.sh +$ sudo chmod 500 nsolid_setup_deb.sh +$ sudo ./nsolid_setup_deb.sh 20 +$ sudo apt-get install nodejs -y +``` + +## Install Build Essential + +```bash +$ sudo apt-get install build-essential +``` + +## Install Kafka + +Refer to [Kafka Ubuntu 22.04 setup guide](https://www.fosstechnix.com/install-apache-kafka-on-ubuntu-22-04-lts/) + +1. Install OpenJDK 11: + + ```bash + $ sudo apt install openjdk-11-jdk + ``` + +2. Download and extract Kafka: + + ```bash + $ sudo wget https://downloads.apache.org/kafka/3.5.0/kafka_2.12-3.5.0.tgz + $ sudo tar xzf kafka_2.12-3.5.0.tgz + $ sudo mv kafka_2.12-3.5.0 /opt/kafka + ``` + +3. Configure Zookeeper: + + ```bash + $ sudo nano /etc/systemd/system/zookeeper.service + ``` + + Paste the following lines into the `zookeeper.service` file: + + ```ini + /etc/systemd/system/zookeeper.service + [Unit] + Description=Apache Zookeeper service + Documentation=http://zookeeper.apache.org + Requires=network.target remote-fs.target + After=network.target remote-fs.target + + [Service] + Type=simple + ExecStart=/opt/kafka/bin/zookeeper-server-start.sh /opt/kafka/config/zookeeper.properties + ExecStop=/opt/kafka/bin/zookeeper-server-stop.sh + Restart=on-abnormal + + [Install] + WantedBy=multi-user.target + ``` + + Save and exit. + +4. Reload systemd: + + ```bash + $ sudo systemctl daemon-reload + ``` + +5. Configure Kafka: + + ```bash + $ sudo nano /etc/systemd/system/kafka.service + ``` + + Paste the following lines into the `kafka.service` file: + + ```ini + [Unit] + Description=Apache Kafka Service + Documentation=http://kafka.apache.org/documentation.html + Requires=zookeeper.service + + [Service] + Type=simple + Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" + ExecStart=/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties + ExecStop=/opt/kafka/bin/kafka-server-stop.sh + + [Install] + WantedBy=multi-user.target + ``` + + Save and exit. + +6. Reload systemd: + + ```bash + $ sudo systemctl daemon-reload + ``` + +7. Start Zookeeper: + + ```bash + $ sudo systemctl start zookeeper + ``` + + Check status: + + ```bash + $ sudo systemctl status zookeeper + ``` + + Zookeeper service status should be shown as active (running). + +8. Start Kafka: + + ```bash + $ sudo systemctl start kafka + ``` + + Check status: + + ```bash + $ sudo systemctl status kafka + ``` + + Kafka status should be shown as active (running). + +## Install Redis + +Refer to [Redis Ubuntu 22.04 setup guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-22-04) + +1. Update the package list: + + ```bash + $ sudo apt update + ``` + +2. Install Redis: + + ```bash + $ sudo apt install redis-server + ``` + +3. Configure Redis for systemd: + + ```bash + $ sudo nano /etc/redis/redis.conf + ``` + + Find the `supervised` directive and change it to "systemd" as follows: + + ```conf + . . . + # If you run Redis from upstart or systemd, Redis can interact with your + # supervision tree. Options: + # supervised no - no supervision interaction + # supervised upstart - signal upstart by putting Redis into SIGSTOP mode + # supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET + # supervised auto - detect upstart or systemd method based on + # UPSTART_JOB or NOTIFY_SOCKET environment variables + # Note: these supervision methods only signal "process is ready." + # They do not enable continuous liveness pings back to your supervisor. + supervised systemd + . . . + ``` + + Save and exit. + +4. Restart the Redis service: + + ```bash + $ sudo systemctl restart redis.service + ``` + +## Install Single-Node Citus (Distributed Database) + +Refer to [official Citus single-node setup](https://docs.citusdata.com/en/stable/installation/single_node_debian.html) + +1. Download and install Citus: + + ```bash + $ curl https://install.citusdata.com/community/deb.sh | sudo bash + $ sudo apt-get -y install postgresql-16-citus-12.1 + ``` + +2. Switch to the PostgreSQL user: + + ```bash + $ sudo su - postgres + ``` + +3. Set the PostgreSQL bin directory in the PATH and create a directory for Citus: + + ```bash + $ export PATH=$PATH:/usr/lib/postgresql/16/bin + $ cd ~ + $ mkdir citus + ``` + +4. Initialize the Citus database: + + ```bash + $ initdb -D citus + ``` + +5. Configure Citus in `citus/postgresql.conf`: + + ```bash + $ echo "shared_preload_libraries = 'citus'" >> citus/postgresql.conf + ``` + +6. Start the Citus server: + + ```bash + $ pg_ctl -D citus -o "-p 9700" -l citus_logfile start + ``` + +7. Create the Citus extension: + + ```bash + $ psql -p 9700 -c "CREATE EXTENSION citus;" + ``` + +8. Check the Citus version: + + ```bash + $ psql -p 9700 -c "select citus_version();" + ``` + + You should see an output similar to the following, indicating that Citus is successfully installed: + + ```sql + postgres=# select citus_version(); + citus_version + ---------------------------------------------------------------------------------------------------- + Citus 12.1.1 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit + (1 row) + ``` + +## Install PM2 + +Refer to [How To Set Up a Node.js Application for Production on Ubuntu 22.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-22-04). + +**Exit the postgres user account** + +```bash +$ exit +``` + +```bash +$ sudo npm install pm2@latest -g +``` + +## Setting up Repository + +### Clone the user-service repository + +```bash +$ cd /opt/ +$ sudo mkdir backend +$ cd backend/ +$ git clone -b develop-2.5 --single-branch "https://github.com/ELEVATE-Project/user.git" +``` + +### Install Npm packages + +```bash +$ cd /opt/backend/user/src +$ sudo npm i +``` + +### Create .env file + +```bash +$ sudo nano .env +``` + +Copy-paste the following env variables to the `.env` file: + +NB : Make sure to update the credentials according to your configurations. + +```env +ACCESS_TOKEN_EXPIRY= 10 +ACCESS_TOKEN_SECRET= asadsd8as7df9as8df987asdf +ADMIN_INVITEE_UPLOAD_EMAIL_TEMPLATE_CODE= invitee_upload_status +ADMIN_SECRET_CODE= Na7ad23ws5cm3kfmw24dmdsflaksd +API_DOC_URL=/user/api-doc +APPLICATION_ENV=development +APPLICATION_PORT=3001 +APP_NAME=MentorED + +AWS_ACCESS_KEY_ID= "adsfg98a7sdfg" +AWS_BUCKET_ENDPOINT="s3.ap-south-1.amazonaws.com" +AWS_BUCKET_REGION="ap-south-1" +AWS_SECRET_ACCESS_KEY="asd9786fg9a8sd/asdfg9a8sd7fg" + + +AZURE_ACCOUNT_KEY=asd897gfa09sd87f09as8d +AZURE_ACCOUNT_NAME=mentoring +CLEAR_INTERNAL_CACHE=userinternal +CLOUD_STORAGE= GCP +DEFAULT_AWS_BUCKET_NAME=mentoring-dev-storage +DEFAULT_AZURE_CONTAINER_NAME=mentoring-images +DEFAULT_GCP_BUCKET_NAME=mentoring-dev-storage + +DEFAULT_ORGANISATION_CODE= default_code +DEFAULT_ORG_ID= 1 +DEFAULT_QUEUE= user-queue +DEFAULT_ROLE= mentee +DEV_DATABASE_URL= postgres://shikshalokam:slpassword123@localhost:9700/elevate_user +DISABLE_LOG= false +EMAIL_ID_ENCRYPTION_ALGORITHM= aes-256-cbc +EMAIL_ID_ENCRYPTION_IV= a19f1ewaqwei9e03edkc32e +EMAIL_ID_ENCRYPTION_KEY= 9bszawjkckw2e3dm35fcw27ws4ed5rftg6y6y7y7654tf4rwq5tr0ol2qa9owsie +ENABLE_EMAIL_OTP_VERIFICATION=true +ENABLE_LOG=true +ERROR_LOG_LEVEL=silly +EVENT_ENABLE_ORG_EVENTS=true +EVENT_ORG_LISTENER_URLS=http://localhost:3567/mentoring/v1/organization/eventListener +GCP_PATH=gcp.json +GCP_PROJECT_ID=sl-dev-project +GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite +INTERNAL_ACCESS_TOKEN= Fqdkfaswekdlwe +INTERNAL_CACHE_EXP_TIME= 86400 +INVITEE_EMAIL_TEMPLATE_CODE= invite_user +IV= LKYTTAqkajswiawqw/Z== +KAFKA_GROUP_ID=dev.users +KAFKA_TOPIC= dev.topic +KAFKA_URL= localhost:9092 +KEY= W/m2cr/aMswjrdsa23sgfy5e34d+bKcbAWZSLjJP2qY= +MENTEE_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentee +MENTORING_SERVICE_URL= http://localhost:3000 +MENTOR_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentor +MENTOR_REQUEST_ACCEPTED_EMAIL_TEMPLATE_CODE= mentor_request_accepted +MENTOR_REQUEST_REJECTED_EMAIL_TEMPLATE_CODE= mentor_request_rejected +MENTOR_SECRET_CODE=4567 +NOTIFICATION_KAFKA_TOPIC=dev.notification +ORG_ADMIN_INVITATION_EMAIL_TEMPLATE_CODE= invite_org_admin +OTP_EMAIL_TEMPLATE_CODE= emailotp +OTP_EXP_TIME= 86400 +PORTAL_URL= "https://dev.elevate-mentoring.shikshalokam.org/auth/login" +RATING_KAFKA_TOPIC= dev.mentor_rating +REDIS_HOST= redis://localhost:6379 +REFRESH_TOKEN_EXPIRY= 183 +REFRESH_TOKEN_SECRET=371hkjadidy2ashiKAkajshdkid23iuekw71yekiaskdvkvegxvy23t78veQwexqviveit6ttZyeeytx62tx236uv +REFRESH_VIEW_INTERVAL=30000 +REGISTRATION_EMAIL_TEMPLATE_CODE= registration +REGISTRATION_OTP_EMAIL_TEMPLATE_CODE= registrationotp +SALT_ROUNDS= 10 +SAMPLE_CSV_FILE_PATH= sample/bulk_user_creation.csv +SCHEDULER_SERVICE_BASE_URL= /scheduler/ +SCHEDULER_SERVICE_ERROR_REPORTING_EMAIL_ID= rakesh.k@pacewisdom.com +SCHEDULER_SERVICE_HOST= http://localhost:3567 +SCHEDULER_SERVICE_URL= http://localhost:3567/jobs/scheduleJob +created_time= 2024-02-08T07:40:04.571464939Z +custom_metadata= null +destroyed= false +version= 31 + +``` + +Save and exit. + +## Setting up Databases + +**Log into the postgres user** + +```bash +sudo su postgres +``` + +**Log into psql** + +```bash +psql -p 9700 +``` + +**Create a database user/role:** + +```sql +CREATE USER shikshalokam WITH ENCRYPTED PASSWORD 'slpassword'; +``` + +**Create the elevate_user database** + +```sql +CREATE DATABASE elevate_user; +GRANT ALL PRIVILEGES ON DATABASE elevate_user TO shikshalokam; +\c elevate_user +GRANT ALL ON SCHEMA public TO shikshalokam; +``` + +## Running Migrations To Create Tables + +**Exit the postgres user account** + +```bash +exit (run twice) +``` + +**Install sequelize-cli globally** + +```bash +sudo npm i sequelize-cli -g +``` + +**Navigate to the src folder of user and run sequelize-cli migration command:** + +```bash +cd /opt/backend/user/src +npx sequelize-cli db:migrate +``` + +**Now all the tables must be available in the Citus databases** + +## Setting up Distribution Columns in Citus PostgreSQL Database + +Refer [Choosing Distribution Column](https://docs.citusdata.com/en/stable/sharding/data_modeling.html) for more information regarding Citus distribution columns. + +**Login into the postgres user** + +```bash +sudo su postgres +``` + +**Login to psql** + +```bash +psql -p 9700 +``` + +**Login to the elevate_user database** + +```sql +\c elevate_user +``` + +**Enable Citus for elevate_user** + +```sql +CREATE EXTENSION citus; +``` + +**Within elevate_user, run the following queries:** + +```sql +SELECT create_distributed_table('entities', 'entity_type_id'); +SELECT create_distributed_table('entity_types', 'organization_id'); +SELECT create_distributed_table('file_uploads', 'organization_id'); +SELECT create_distributed_table('forms', 'organization_id'); +SELECT create_distributed_table('notification_templates', 'organization_id'); +SELECT create_distributed_table('organizations', 'id'); +SELECT create_distributed_table('organization_codes', 'code'); +SELECT create_distributed_table('organization_domains', 'domain'); +SELECT create_distributed_table('organization_role_requests','organization_id'); +SELECT create_distributed_table('organization_user_invites','organization_id'); +SELECT create_distributed_table('users_credentials','email'); +SELECT create_distributed_table('users', 'organization_id'); +``` + +## Running Seeder to Populate the Tables with Seed Data + +**Exit the postgres user** + +```bash +exit (run twice) +``` + +**Navigate to the src/scripts directory of the user service** + +```bash +cd /opt/backend/user/src/scripts +``` + +**Run the insertDefaultOrg.js script** + +```bash +node insertDefaultOrg.js +``` + +_Keep note of the default organization id generated by the script_ + +**Navigate to the src folder of the user service and update the .env file with these variables:** + +```bash +sudo nano /opt/backend/user/src/.env +``` + +```env +DEFAULT_ORG_ID= +DEFAULT_ORGANISATION_CODE=default_code +``` + +**Run the seeder command** + +```bash +cd /opt/backend/user/src +npm run db:seed:all +``` + +## Start the Service + +Navigate to the src folder of user service and run pm2 start command: + +```bash +$ cd /opt/backend/user/src +user/src$ pm2 start app.js -i 2 --name elevate-user +``` + +#### Run pm2 ls command + +```bash +$ pm2 ls +``` + +Output should look like this (Sample output, might slightly differ in your installation): + +```bash +┌────┬─────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ +│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ +├────┼─────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ +│ 29 │ elevate-user │ default │ 1.0.0 │ cluster │ 106976 │ 27h │ 0 │ online │ 0% │ 167.0mb │ jenkins │ disabled │ +│ 30 │ elevate-user │ default │ 1.0.0 │ cluster │ 106986 │ 27h │ 0 │ online │ 0% │ 169.3mb │ jenkins │ disabled │ +└────┴─────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ +``` + +This concludes the services and dependency setup. + +## Postman Collections + +- [User Service](https://github.com/ELEVATE-Project/user/tree/develop-2.5/src/api-doc) From dbe7425d60939988618e910c5011595710419d3c Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 20 Feb 2024 16:08:02 +0530 Subject: [PATCH 02/67] user service documentation - correction --- src/api-doc/user-service-doc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api-doc/user-service-doc.md b/src/api-doc/user-service-doc.md index bbc0be058..3642ea394 100644 --- a/src/api-doc/user-service-doc.md +++ b/src/api-doc/user-service-doc.md @@ -532,8 +532,8 @@ Output should look like this (Sample output, might slightly differ in your insta ┌────┬─────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ ├────┼─────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ -│ 29 │ elevate-user │ default │ 1.0.0 │ cluster │ 106976 │ 27h │ 0 │ online │ 0% │ 167.0mb │ jenkins │ disabled │ -│ 30 │ elevate-user │ default │ 1.0.0 │ cluster │ 106986 │ 27h │ 0 │ online │ 0% │ 169.3mb │ jenkins │ disabled │ +│ 1 │ elevate-user │ default │ 1.0.0 │ cluster │ 106976 │ 27h │ 0 │ online │ 0% │ 167.0mb │ jenkins │ disabled │ +│ 2 │ elevate-user │ default │ 1.0.0 │ cluster │ 106986 │ 27h │ 0 │ online │ 0% │ 169.3mb │ jenkins │ disabled │ └────┴─────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ ``` From 5f2a56e62b87e6417e3df520e07179c8e4d8a1b5 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 26 Feb 2024 15:11:41 +0530 Subject: [PATCH 03/67] event issue in updating the extensions of the users --- src/services/organization.js | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/services/organization.js b/src/services/organization.js index 76f61f18a..1f57e529d 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -16,6 +16,7 @@ const UserCredentialQueries = require('@database/queries/userCredential') const emailEncryption = require('@utils/emailEncryption') const { eventBodyDTO } = require('@dtos/eventBody') const responses = require('@helpers/responses') +const organization = require('@database/models/organization') module.exports = class OrganizationsHelper { /** @@ -157,6 +158,7 @@ module.exports = class OrganizationsHelper { static async update(id, bodyData, loggedInUserId) { try { bodyData.updated_by = loggedInUserId + bodyData.related_orgs = [...new Set([...bodyData.related_orgs])] //use Set to remove any duplicates const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id: id }) if (!orgDetailsBeforeUpdate) { return responses.failureResponse({ @@ -172,10 +174,15 @@ module.exports = class OrganizationsHelper { bodyData?.related_orgs && _.isEqual(orgDetails.updatedRows[0].related_orgs, bodyData.related_orgs) ) { - await organizationQueries.appendRelatedOrg(orgDetails.updatedRows[0].id, bodyData.related_orgs, { - returning: true, - raw: true, - }) + // const relatedOrgs = [...new Set([...bodyData.related_orgs])] + let x = await organizationQueries.appendRelatedOrg( + orgDetails.updatedRows[0].id, + bodyData.related_orgs, + { + returning: true, + raw: true, + } + ) } const removedOrgIds = _.difference( orgDetailsBeforeUpdate.related_orgs, @@ -186,10 +193,25 @@ module.exports = class OrganizationsHelper { raw: true, }) + // fetch the list of all unique related orgs + const allRelatedOrgs = [ + ...new Set([...orgDetails.updatedRows[0].related_orgs, orgDetails.updatedRows[0].id]), + ] + + const organizationDetails = await organizationQueries.findAll( + { + id: { [Op.in]: allRelatedOrgs }, + }, + { + attributes: ['id', 'related_orgs'], + } + ) + eventBroadcaster('updateRelatedOrgs', { requestBody: { related_organization_ids: orgDetails.updatedRows[0].related_orgs, organization_id: orgDetails.updatedRows[0].id, + organizationDetails, }, }) } From 7629a2ebf5e7a2e1fe718d42a4650cf2a221dada Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 26 Feb 2024 15:14:38 +0530 Subject: [PATCH 04/67] removed documentation related changes --- src/api-doc/user-service-doc.md | 544 -------------------------------- 1 file changed, 544 deletions(-) delete mode 100644 src/api-doc/user-service-doc.md diff --git a/src/api-doc/user-service-doc.md b/src/api-doc/user-service-doc.md deleted file mode 100644 index 3642ea394..000000000 --- a/src/api-doc/user-service-doc.md +++ /dev/null @@ -1,544 +0,0 @@ -# ShikshaLokam Elevate Project Documentation - -## User Service - -### System Requirements - -- **Operating System:** Ubuntu 22 -- **Node.js:** v20 -- **PostgreSQL:** 16 -- **Citus:** 12.1 - -## Install Node.js - -Refer to the [NodeSource distributions installation scripts](https://github.com/nodesource/distributions#installation-scripts) for Node.js installation. - -```bash -$ curl -SLO https://deb.nodesource.com/nsolid_setup_deb.sh -$ sudo chmod 500 nsolid_setup_deb.sh -$ sudo ./nsolid_setup_deb.sh 20 -$ sudo apt-get install nodejs -y -``` - -## Install Build Essential - -```bash -$ sudo apt-get install build-essential -``` - -## Install Kafka - -Refer to [Kafka Ubuntu 22.04 setup guide](https://www.fosstechnix.com/install-apache-kafka-on-ubuntu-22-04-lts/) - -1. Install OpenJDK 11: - - ```bash - $ sudo apt install openjdk-11-jdk - ``` - -2. Download and extract Kafka: - - ```bash - $ sudo wget https://downloads.apache.org/kafka/3.5.0/kafka_2.12-3.5.0.tgz - $ sudo tar xzf kafka_2.12-3.5.0.tgz - $ sudo mv kafka_2.12-3.5.0 /opt/kafka - ``` - -3. Configure Zookeeper: - - ```bash - $ sudo nano /etc/systemd/system/zookeeper.service - ``` - - Paste the following lines into the `zookeeper.service` file: - - ```ini - /etc/systemd/system/zookeeper.service - [Unit] - Description=Apache Zookeeper service - Documentation=http://zookeeper.apache.org - Requires=network.target remote-fs.target - After=network.target remote-fs.target - - [Service] - Type=simple - ExecStart=/opt/kafka/bin/zookeeper-server-start.sh /opt/kafka/config/zookeeper.properties - ExecStop=/opt/kafka/bin/zookeeper-server-stop.sh - Restart=on-abnormal - - [Install] - WantedBy=multi-user.target - ``` - - Save and exit. - -4. Reload systemd: - - ```bash - $ sudo systemctl daemon-reload - ``` - -5. Configure Kafka: - - ```bash - $ sudo nano /etc/systemd/system/kafka.service - ``` - - Paste the following lines into the `kafka.service` file: - - ```ini - [Unit] - Description=Apache Kafka Service - Documentation=http://kafka.apache.org/documentation.html - Requires=zookeeper.service - - [Service] - Type=simple - Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" - ExecStart=/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties - ExecStop=/opt/kafka/bin/kafka-server-stop.sh - - [Install] - WantedBy=multi-user.target - ``` - - Save and exit. - -6. Reload systemd: - - ```bash - $ sudo systemctl daemon-reload - ``` - -7. Start Zookeeper: - - ```bash - $ sudo systemctl start zookeeper - ``` - - Check status: - - ```bash - $ sudo systemctl status zookeeper - ``` - - Zookeeper service status should be shown as active (running). - -8. Start Kafka: - - ```bash - $ sudo systemctl start kafka - ``` - - Check status: - - ```bash - $ sudo systemctl status kafka - ``` - - Kafka status should be shown as active (running). - -## Install Redis - -Refer to [Redis Ubuntu 22.04 setup guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-22-04) - -1. Update the package list: - - ```bash - $ sudo apt update - ``` - -2. Install Redis: - - ```bash - $ sudo apt install redis-server - ``` - -3. Configure Redis for systemd: - - ```bash - $ sudo nano /etc/redis/redis.conf - ``` - - Find the `supervised` directive and change it to "systemd" as follows: - - ```conf - . . . - # If you run Redis from upstart or systemd, Redis can interact with your - # supervision tree. Options: - # supervised no - no supervision interaction - # supervised upstart - signal upstart by putting Redis into SIGSTOP mode - # supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET - # supervised auto - detect upstart or systemd method based on - # UPSTART_JOB or NOTIFY_SOCKET environment variables - # Note: these supervision methods only signal "process is ready." - # They do not enable continuous liveness pings back to your supervisor. - supervised systemd - . . . - ``` - - Save and exit. - -4. Restart the Redis service: - - ```bash - $ sudo systemctl restart redis.service - ``` - -## Install Single-Node Citus (Distributed Database) - -Refer to [official Citus single-node setup](https://docs.citusdata.com/en/stable/installation/single_node_debian.html) - -1. Download and install Citus: - - ```bash - $ curl https://install.citusdata.com/community/deb.sh | sudo bash - $ sudo apt-get -y install postgresql-16-citus-12.1 - ``` - -2. Switch to the PostgreSQL user: - - ```bash - $ sudo su - postgres - ``` - -3. Set the PostgreSQL bin directory in the PATH and create a directory for Citus: - - ```bash - $ export PATH=$PATH:/usr/lib/postgresql/16/bin - $ cd ~ - $ mkdir citus - ``` - -4. Initialize the Citus database: - - ```bash - $ initdb -D citus - ``` - -5. Configure Citus in `citus/postgresql.conf`: - - ```bash - $ echo "shared_preload_libraries = 'citus'" >> citus/postgresql.conf - ``` - -6. Start the Citus server: - - ```bash - $ pg_ctl -D citus -o "-p 9700" -l citus_logfile start - ``` - -7. Create the Citus extension: - - ```bash - $ psql -p 9700 -c "CREATE EXTENSION citus;" - ``` - -8. Check the Citus version: - - ```bash - $ psql -p 9700 -c "select citus_version();" - ``` - - You should see an output similar to the following, indicating that Citus is successfully installed: - - ```sql - postgres=# select citus_version(); - citus_version - ---------------------------------------------------------------------------------------------------- - Citus 12.1.1 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit - (1 row) - ``` - -## Install PM2 - -Refer to [How To Set Up a Node.js Application for Production on Ubuntu 22.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-22-04). - -**Exit the postgres user account** - -```bash -$ exit -``` - -```bash -$ sudo npm install pm2@latest -g -``` - -## Setting up Repository - -### Clone the user-service repository - -```bash -$ cd /opt/ -$ sudo mkdir backend -$ cd backend/ -$ git clone -b develop-2.5 --single-branch "https://github.com/ELEVATE-Project/user.git" -``` - -### Install Npm packages - -```bash -$ cd /opt/backend/user/src -$ sudo npm i -``` - -### Create .env file - -```bash -$ sudo nano .env -``` - -Copy-paste the following env variables to the `.env` file: - -NB : Make sure to update the credentials according to your configurations. - -```env -ACCESS_TOKEN_EXPIRY= 10 -ACCESS_TOKEN_SECRET= asadsd8as7df9as8df987asdf -ADMIN_INVITEE_UPLOAD_EMAIL_TEMPLATE_CODE= invitee_upload_status -ADMIN_SECRET_CODE= Na7ad23ws5cm3kfmw24dmdsflaksd -API_DOC_URL=/user/api-doc -APPLICATION_ENV=development -APPLICATION_PORT=3001 -APP_NAME=MentorED - -AWS_ACCESS_KEY_ID= "adsfg98a7sdfg" -AWS_BUCKET_ENDPOINT="s3.ap-south-1.amazonaws.com" -AWS_BUCKET_REGION="ap-south-1" -AWS_SECRET_ACCESS_KEY="asd9786fg9a8sd/asdfg9a8sd7fg" - - -AZURE_ACCOUNT_KEY=asd897gfa09sd87f09as8d -AZURE_ACCOUNT_NAME=mentoring -CLEAR_INTERNAL_CACHE=userinternal -CLOUD_STORAGE= GCP -DEFAULT_AWS_BUCKET_NAME=mentoring-dev-storage -DEFAULT_AZURE_CONTAINER_NAME=mentoring-images -DEFAULT_GCP_BUCKET_NAME=mentoring-dev-storage - -DEFAULT_ORGANISATION_CODE= default_code -DEFAULT_ORG_ID= 1 -DEFAULT_QUEUE= user-queue -DEFAULT_ROLE= mentee -DEV_DATABASE_URL= postgres://shikshalokam:slpassword123@localhost:9700/elevate_user -DISABLE_LOG= false -EMAIL_ID_ENCRYPTION_ALGORITHM= aes-256-cbc -EMAIL_ID_ENCRYPTION_IV= a19f1ewaqwei9e03edkc32e -EMAIL_ID_ENCRYPTION_KEY= 9bszawjkckw2e3dm35fcw27ws4ed5rftg6y6y7y7654tf4rwq5tr0ol2qa9owsie -ENABLE_EMAIL_OTP_VERIFICATION=true -ENABLE_LOG=true -ERROR_LOG_LEVEL=silly -EVENT_ENABLE_ORG_EVENTS=true -EVENT_ORG_LISTENER_URLS=http://localhost:3567/mentoring/v1/organization/eventListener -GCP_PATH=gcp.json -GCP_PROJECT_ID=sl-dev-project -GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite -INTERNAL_ACCESS_TOKEN= Fqdkfaswekdlwe -INTERNAL_CACHE_EXP_TIME= 86400 -INVITEE_EMAIL_TEMPLATE_CODE= invite_user -IV= LKYTTAqkajswiawqw/Z== -KAFKA_GROUP_ID=dev.users -KAFKA_TOPIC= dev.topic -KAFKA_URL= localhost:9092 -KEY= W/m2cr/aMswjrdsa23sgfy5e34d+bKcbAWZSLjJP2qY= -MENTEE_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentee -MENTORING_SERVICE_URL= http://localhost:3000 -MENTOR_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentor -MENTOR_REQUEST_ACCEPTED_EMAIL_TEMPLATE_CODE= mentor_request_accepted -MENTOR_REQUEST_REJECTED_EMAIL_TEMPLATE_CODE= mentor_request_rejected -MENTOR_SECRET_CODE=4567 -NOTIFICATION_KAFKA_TOPIC=dev.notification -ORG_ADMIN_INVITATION_EMAIL_TEMPLATE_CODE= invite_org_admin -OTP_EMAIL_TEMPLATE_CODE= emailotp -OTP_EXP_TIME= 86400 -PORTAL_URL= "https://dev.elevate-mentoring.shikshalokam.org/auth/login" -RATING_KAFKA_TOPIC= dev.mentor_rating -REDIS_HOST= redis://localhost:6379 -REFRESH_TOKEN_EXPIRY= 183 -REFRESH_TOKEN_SECRET=371hkjadidy2ashiKAkajshdkid23iuekw71yekiaskdvkvegxvy23t78veQwexqviveit6ttZyeeytx62tx236uv -REFRESH_VIEW_INTERVAL=30000 -REGISTRATION_EMAIL_TEMPLATE_CODE= registration -REGISTRATION_OTP_EMAIL_TEMPLATE_CODE= registrationotp -SALT_ROUNDS= 10 -SAMPLE_CSV_FILE_PATH= sample/bulk_user_creation.csv -SCHEDULER_SERVICE_BASE_URL= /scheduler/ -SCHEDULER_SERVICE_ERROR_REPORTING_EMAIL_ID= rakesh.k@pacewisdom.com -SCHEDULER_SERVICE_HOST= http://localhost:3567 -SCHEDULER_SERVICE_URL= http://localhost:3567/jobs/scheduleJob -created_time= 2024-02-08T07:40:04.571464939Z -custom_metadata= null -destroyed= false -version= 31 - -``` - -Save and exit. - -## Setting up Databases - -**Log into the postgres user** - -```bash -sudo su postgres -``` - -**Log into psql** - -```bash -psql -p 9700 -``` - -**Create a database user/role:** - -```sql -CREATE USER shikshalokam WITH ENCRYPTED PASSWORD 'slpassword'; -``` - -**Create the elevate_user database** - -```sql -CREATE DATABASE elevate_user; -GRANT ALL PRIVILEGES ON DATABASE elevate_user TO shikshalokam; -\c elevate_user -GRANT ALL ON SCHEMA public TO shikshalokam; -``` - -## Running Migrations To Create Tables - -**Exit the postgres user account** - -```bash -exit (run twice) -``` - -**Install sequelize-cli globally** - -```bash -sudo npm i sequelize-cli -g -``` - -**Navigate to the src folder of user and run sequelize-cli migration command:** - -```bash -cd /opt/backend/user/src -npx sequelize-cli db:migrate -``` - -**Now all the tables must be available in the Citus databases** - -## Setting up Distribution Columns in Citus PostgreSQL Database - -Refer [Choosing Distribution Column](https://docs.citusdata.com/en/stable/sharding/data_modeling.html) for more information regarding Citus distribution columns. - -**Login into the postgres user** - -```bash -sudo su postgres -``` - -**Login to psql** - -```bash -psql -p 9700 -``` - -**Login to the elevate_user database** - -```sql -\c elevate_user -``` - -**Enable Citus for elevate_user** - -```sql -CREATE EXTENSION citus; -``` - -**Within elevate_user, run the following queries:** - -```sql -SELECT create_distributed_table('entities', 'entity_type_id'); -SELECT create_distributed_table('entity_types', 'organization_id'); -SELECT create_distributed_table('file_uploads', 'organization_id'); -SELECT create_distributed_table('forms', 'organization_id'); -SELECT create_distributed_table('notification_templates', 'organization_id'); -SELECT create_distributed_table('organizations', 'id'); -SELECT create_distributed_table('organization_codes', 'code'); -SELECT create_distributed_table('organization_domains', 'domain'); -SELECT create_distributed_table('organization_role_requests','organization_id'); -SELECT create_distributed_table('organization_user_invites','organization_id'); -SELECT create_distributed_table('users_credentials','email'); -SELECT create_distributed_table('users', 'organization_id'); -``` - -## Running Seeder to Populate the Tables with Seed Data - -**Exit the postgres user** - -```bash -exit (run twice) -``` - -**Navigate to the src/scripts directory of the user service** - -```bash -cd /opt/backend/user/src/scripts -``` - -**Run the insertDefaultOrg.js script** - -```bash -node insertDefaultOrg.js -``` - -_Keep note of the default organization id generated by the script_ - -**Navigate to the src folder of the user service and update the .env file with these variables:** - -```bash -sudo nano /opt/backend/user/src/.env -``` - -```env -DEFAULT_ORG_ID= -DEFAULT_ORGANISATION_CODE=default_code -``` - -**Run the seeder command** - -```bash -cd /opt/backend/user/src -npm run db:seed:all -``` - -## Start the Service - -Navigate to the src folder of user service and run pm2 start command: - -```bash -$ cd /opt/backend/user/src -user/src$ pm2 start app.js -i 2 --name elevate-user -``` - -#### Run pm2 ls command - -```bash -$ pm2 ls -``` - -Output should look like this (Sample output, might slightly differ in your installation): - -```bash -┌────┬─────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ -│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ -├────┼─────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ -│ 1 │ elevate-user │ default │ 1.0.0 │ cluster │ 106976 │ 27h │ 0 │ online │ 0% │ 167.0mb │ jenkins │ disabled │ -│ 2 │ elevate-user │ default │ 1.0.0 │ cluster │ 106986 │ 27h │ 0 │ online │ 0% │ 169.3mb │ jenkins │ disabled │ -└────┴─────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ -``` - -This concludes the services and dependency setup. - -## Postman Collections - -- [User Service](https://github.com/ELEVATE-Project/user/tree/develop-2.5/src/api-doc) From 9c9f4799fe7f77130f3bf99699b99c6a0f7e78d5 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 11 Mar 2024 21:39:05 +0530 Subject: [PATCH 05/67] event issue in updating the extensions of the users --- src/controllers/v1/organization.js | 83 +++++++++++++++ src/services/organization.js | 159 ++++++++++++++++++++--------- 2 files changed, 194 insertions(+), 48 deletions(-) diff --git a/src/controllers/v1/organization.js b/src/controllers/v1/organization.js index b9d314845..1caec3d64 100644 --- a/src/controllers/v1/organization.js +++ b/src/controllers/v1/organization.js @@ -160,4 +160,87 @@ module.exports = class Organization { return error } } + + async addRelatedOrg(req) { + try { + let isAdmin, + isOrgAdmin = false + const roles = req.decodedToken.roles + + if (roles && roles.length > 0) { + isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE) + isOrgAdmin = utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE) + } + + if (req.params.id != req.decodedToken.organization_id && isOrgAdmin) { + if (req.body.related_orgs) { + throw responses.failureResponse({ + message: 'CONTACT_ADMIN_RELATED_ORGANIZATIONS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + throw responses.failureResponse({ + message: 'USER_DOES_NOT_HAVE_ACCESS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } else if (!isAdmin && !isOrgAdmin) { + throw responses.failureResponse({ + message: 'USER_IS_NOT_A_ADMIN', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + const result = await orgService.addRelatedOrg( + req.params.id ? req.params.id : '', + req.body.related_orgs ? req.body.related_orgs : [] + ) + return result + } catch (error) { + return error + } + } + async removeRelatedOrg(req) { + try { + let isAdmin, + isOrgAdmin = false + const roles = req.decodedToken.roles + + if (roles && roles.length > 0) { + isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE) + isOrgAdmin = utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE) + } + + if (req.params.id != req.decodedToken.organization_id && isOrgAdmin) { + if (req.body.related_orgs) { + throw responses.failureResponse({ + message: 'CONTACT_ADMIN_RELATED_ORGANIZATIONS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + throw responses.failureResponse({ + message: 'USER_DOES_NOT_HAVE_ACCESS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } else if (!isAdmin && !isOrgAdmin) { + throw responses.failureResponse({ + message: 'USER_IS_NOT_A_ADMIN', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + const result = await orgService.removeRelatedOrg( + req.params.id ? req.params.id : '', + req.body.related_orgs ? req.body.related_orgs : [] + ) + return result + } catch (error) { + return error + } + } } diff --git a/src/services/organization.js b/src/services/organization.js index 1f57e529d..0f9a1c2f7 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -17,6 +17,7 @@ const emailEncryption = require('@utils/emailEncryption') const { eventBodyDTO } = require('@dtos/eventBody') const responses = require('@helpers/responses') const organization = require('@database/models/organization') +const sequelize = require('@database/models/index').sequelize module.exports = class OrganizationsHelper { /** @@ -158,7 +159,9 @@ module.exports = class OrganizationsHelper { static async update(id, bodyData, loggedInUserId) { try { bodyData.updated_by = loggedInUserId - bodyData.related_orgs = [...new Set([...bodyData.related_orgs])] //use Set to remove any duplicates + if (bodyData.relatedOrgs) { + delete bodyData.relatedOrgs + } const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id: id }) if (!orgDetailsBeforeUpdate) { return responses.failureResponse({ @@ -169,53 +172,6 @@ module.exports = class OrganizationsHelper { } const orgDetails = await organizationQueries.update({ id: id }, bodyData, { returning: true, raw: true }) - if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, bodyData?.related_orgs)) { - if ( - bodyData?.related_orgs && - _.isEqual(orgDetails.updatedRows[0].related_orgs, bodyData.related_orgs) - ) { - // const relatedOrgs = [...new Set([...bodyData.related_orgs])] - let x = await organizationQueries.appendRelatedOrg( - orgDetails.updatedRows[0].id, - bodyData.related_orgs, - { - returning: true, - raw: true, - } - ) - } - const removedOrgIds = _.difference( - orgDetailsBeforeUpdate.related_orgs, - orgDetails.updatedRows[0].related_orgs - ) - await organizationQueries.removeRelatedOrg(orgDetails.updatedRows[0].id, removedOrgIds, { - returning: true, - raw: true, - }) - - // fetch the list of all unique related orgs - const allRelatedOrgs = [ - ...new Set([...orgDetails.updatedRows[0].related_orgs, orgDetails.updatedRows[0].id]), - ] - - const organizationDetails = await organizationQueries.findAll( - { - id: { [Op.in]: allRelatedOrgs }, - }, - { - attributes: ['id', 'related_orgs'], - } - ) - - eventBroadcaster('updateRelatedOrgs', { - requestBody: { - related_organization_ids: orgDetails.updatedRows[0].related_orgs, - organization_id: orgDetails.updatedRows[0].id, - organizationDetails, - }, - }) - } - let domains = [] if (bodyData.domains?.length) { let existingDomains = await orgDomainQueries.findAll({ @@ -408,6 +364,113 @@ module.exports = class OrganizationsHelper { throw error } } + + static async addRelatedOrg(id, relatedOrgs) { + try { + // fetch organization details before update + const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id: id }) + if (!orgDetailsBeforeUpdate) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + message: 'ORGANIZATION_NOT_FOUND', + }) + } + // append related organizations and make sure it is unique + let newRelatedOrgs = [...new Set([...orgDetailsBeforeUpdate?.related_orgs, ...relatedOrgs])] + + // check if there are any addition to related_org + if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, newRelatedOrgs)) { + // update parent org related orgs + const parentOrganzationUpdate = await organizationQueries.update( + { + id: id, + }, + { + related_orgs: newRelatedOrgs, + }, + { + returning: true, + raw: true, + } + ) + // update child org related orgs + const childOrganzationUpdate = await organizationQueries.appendRelatedOrg(id, newRelatedOrgs, { + returning: true, + raw: true, + }) + const deltaOrgs = _.difference(newRelatedOrgs, orgDetailsBeforeUpdate?.related_orgs) + + eventBroadcaster('updateRelatedOrgs', { + requestBody: { + delta_organization_ids: deltaOrgs, + organization_id: id, + action: 'PUSH', + }, + }) + } + + return responses.successResponse({ + statusCode: httpStatusCode.accepted, + message: 'ORGANIZATION_UPDATED_SUCCESSFULLY', + }) + } catch (error) { + throw error + } + } + static async removeRelatedOrg(id, relatedOrgs) { + try { + // fetch organization details before update + const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id: id }) + if (!orgDetailsBeforeUpdate) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + message: 'ORGANIZATION_NOT_FOUND', + }) + } + + const relatedOrganizations = _.difference(orgDetailsBeforeUpdate?.related_orgs, relatedOrgs) + + // check if there are any addition to related_org + if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, relatedOrganizations)) { + // update parent org related orgs + const parentOrganzationUpdate = await organizationQueries.update( + { + id: parseInt(id, 10), + }, + { + related_orgs: relatedOrganizations, + }, + { + returning: true, + raw: true, + } + ) + + // update child org related orgs + const childOrganzationUpdate = await organizationQueries.removeRelatedOrg(id, relatedOrgs, { + returning: true, + raw: true, + }) + + eventBroadcaster('updateRelatedOrgs', { + requestBody: { + delta_organization_ids: relatedOrgs, + organization_id: id, + action: 'POP', + }, + }) + } + + return responses.successResponse({ + statusCode: httpStatusCode.accepted, + message: 'ORGANIZATION_UPDATED_SUCCESSFULLY', + }) + } catch (error) { + throw error + } + } } async function createRoleRequest(bodyData, tokenInformation) { From 7ad1ae213b860275a4e912cc1d3972371effda9a Mon Sep 17 00:00:00 2001 From: Nevil Date: Wed, 13 Mar 2024 19:46:36 +0530 Subject: [PATCH 06/67] fixed an issue with index.js --- src/middlewares/validator.js | 5 ++- src/routes/index.js | 87 +++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js index 657baf27f..10e50282e 100644 --- a/src/middlewares/validator.js +++ b/src/middlewares/validator.js @@ -7,7 +7,10 @@ module.exports = (req, res, next) => { try { - require(`@validators/${req.params.version}/${req.params.controller}`)[req.params.method](req) + const version = (req.params.version.match(/^v\d+$/) || [])[0] // Match version like v1, v2, etc. + const controllerName = (req.params.controller.match(/^[a-zA-Z0-9_-]+$/) || [])[0] // Allow only alphanumeric characters, underscore, and hyphen + const method = (req.params.method.match(/^[a-zA-Z0-9]+$/) || [])[0] // Allow only alphanumeric characters + require(`@validators/${version}/${controllerName}`)[method](req) } catch {} next() } diff --git a/src/routes/index.js b/src/routes/index.js index 6e6928d31..55d2dd2a6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -12,15 +12,92 @@ const expressValidator = require('express-validator') const fs = require('fs') const { elevateLog, correlationId } = require('elevate-logger') const logger = elevateLog.init() +const path = require('path') module.exports = (app) => { app.use(authenticator) app.use(pagination) app.use(expressValidator()) + async function getAllowedControllers(directoryPath) { + try { + const getAllFilesAndDirectories = (dir) => { + let filesAndDirectories = [] + fs.readdirSync(dir).forEach((item) => { + const itemPath = path.join(dir, item) + const stat = fs.statSync(itemPath) + if (stat.isDirectory()) { + filesAndDirectories.push({ + name: item, + type: 'directory', + path: itemPath, + }) + filesAndDirectories = filesAndDirectories.concat(getAllFilesAndDirectories(itemPath)) + } else { + filesAndDirectories.push({ + name: item, + type: 'file', + path: itemPath, + }) + } + }) + return filesAndDirectories + } + + const allFilesAndDirectories = getAllFilesAndDirectories(directoryPath) + const allowedControllers = allFilesAndDirectories + .filter((item) => item.type === 'file' && item.name.endsWith('.js')) + .map((item) => path.basename(item.name, '.js')) // Remove the ".js" extension + const allowedVersions = allFilesAndDirectories + .filter((item) => item.type === 'directory') + .map((item) => item.name) + + return { + allowedControllers, + allowedVersions, + } + } catch (err) { + console.error('Unable to scan directory:', err) + return { + allowedControllers: [], + directories: [], + } + } + } async function router(req, res, next) { let controllerResponse let validationError + const version = (req.params.version.match(/^v\d+$/) || [])[0] // Match version like v1, v2, etc. + const controllerName = (req.params.controller.match(/^[a-zA-Z0-9_-]+$/) || [])[0] // Allow only alphanumeric characters, underscore, and hyphen + const file = req.params.file ? (req.params.file.match(/^[a-zA-Z0-9_-]+$/) || [])[0] : null // Same validation as controller, or null if file is not provided + const method = (req.params.method.match(/^[a-zA-Z0-9]+$/) || [])[0] // Allow only alphanumeric characters + try { + if (!version || !controllerName || !method || (req.params.file && !file)) { + // Invalid input, return an error response + const error = new Error('Invalid Path') + error.statusCode = 400 + throw error + } + + const directoryPath = path.resolve(__dirname, '..', 'controllers') + + const { allowedControllers, allowedVersions } = await getAllowedControllers(directoryPath) + + // Validate version + if (!allowedVersions.includes(version)) { + const error = new Error('Invalid version.') + error.statusCode = 400 + throw error + } + // Validate controller + if (!allowedControllers.includes(controllerName)) { + const error = new Error('Invalid controller.') + error.statusCode = 400 + throw error + } + } catch (error) { + return next(error) + } /* Check for input validation error */ try { @@ -53,16 +130,14 @@ module.exports = (app) => { '.js' ) if (folderExists) { - controller = require(`@controllers/${req.params.version}/${req.params.controller}/${req.params.file}`) + controller = require(`@controllers/${version}/${controllerName}/${file}`) } else { - controller = require(`@controllers/${req.params.version}/${req.params.controller}`) + controller = require(`@controllers/${version}/${controllerName}`) } } else { - controller = require(`@controllers/${req.params.version}/${req.params.controller}`) + controller = require(`@controllers/${version}/${controllerName}`) } - controllerResponse = new controller()[req.params.method] - ? await new controller()[req.params.method](req) - : next() + controllerResponse = new controller()[method] ? await new controller()[method](req) : next() } catch (error) { // If controller or service throws some random error return next(error) From cb75f8db51086451cec089739a2885ec5a0d3883 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Wed, 13 Mar 2024 21:31:33 +0530 Subject: [PATCH 07/67] review comments --- src/controllers/v1/organization.js | 60 --------- ...admin-permissions-for-related-orgs-apis.js | 63 ++++++++++ ...ermission-mapping-for-related-orgs-apis.js | 118 ++++++++++++++++++ src/locales/en.json | 3 +- src/services/organization.js | 35 ++++-- 5 files changed, 207 insertions(+), 72 deletions(-) create mode 100644 src/database/migrations/20240313130050-add-admin-permissions-for-related-orgs-apis.js create mode 100644 src/database/migrations/20240313134927-add-admin-role-permission-mapping-for-related-orgs-apis.js diff --git a/src/controllers/v1/organization.js b/src/controllers/v1/organization.js index 1caec3d64..610f8ed55 100644 --- a/src/controllers/v1/organization.js +++ b/src/controllers/v1/organization.js @@ -163,36 +163,6 @@ module.exports = class Organization { async addRelatedOrg(req) { try { - let isAdmin, - isOrgAdmin = false - const roles = req.decodedToken.roles - - if (roles && roles.length > 0) { - isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE) - isOrgAdmin = utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE) - } - - if (req.params.id != req.decodedToken.organization_id && isOrgAdmin) { - if (req.body.related_orgs) { - throw responses.failureResponse({ - message: 'CONTACT_ADMIN_RELATED_ORGANIZATIONS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - throw responses.failureResponse({ - message: 'USER_DOES_NOT_HAVE_ACCESS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } else if (!isAdmin && !isOrgAdmin) { - throw responses.failureResponse({ - message: 'USER_IS_NOT_A_ADMIN', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - const result = await orgService.addRelatedOrg( req.params.id ? req.params.id : '', req.body.related_orgs ? req.body.related_orgs : [] @@ -204,36 +174,6 @@ module.exports = class Organization { } async removeRelatedOrg(req) { try { - let isAdmin, - isOrgAdmin = false - const roles = req.decodedToken.roles - - if (roles && roles.length > 0) { - isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE) - isOrgAdmin = utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE) - } - - if (req.params.id != req.decodedToken.organization_id && isOrgAdmin) { - if (req.body.related_orgs) { - throw responses.failureResponse({ - message: 'CONTACT_ADMIN_RELATED_ORGANIZATIONS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - throw responses.failureResponse({ - message: 'USER_DOES_NOT_HAVE_ACCESS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } else if (!isAdmin && !isOrgAdmin) { - throw responses.failureResponse({ - message: 'USER_IS_NOT_A_ADMIN', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - const result = await orgService.removeRelatedOrg( req.params.id ? req.params.id : '', req.body.related_orgs ? req.body.related_orgs : [] diff --git a/src/database/migrations/20240313130050-add-admin-permissions-for-related-orgs-apis.js b/src/database/migrations/20240313130050-add-admin-permissions-for-related-orgs-apis.js new file mode 100644 index 000000000..a7d8571c7 --- /dev/null +++ b/src/database/migrations/20240313130050-add-admin-permissions-for-related-orgs-apis.js @@ -0,0 +1,63 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + try { + const permissionsData = [ + { + code: 'organization_data_update', + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/update/*', + status: 'ACTIVE', + }, + { + code: 'organization_append_relatedOrg', + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/addRelatedOrg/*', + status: 'ACTIVE', + }, + { + code: 'organization_remove_relatedOrg', + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/removeRelatedOrg/*', + status: 'ACTIVE', + }, + ] + + // Batch insert permissions + await queryInterface.bulkInsert( + 'permissions', + permissionsData.map((permission) => ({ + ...permission, + created_at: new Date(), + updated_at: new Date(), + })) + ) + } catch (error) { + console.error('Error in migration:', error) + throw error + } + }, + + async down(queryInterface, Sequelize) { + try { + // Rollback the batch insert + await queryInterface.bulkDelete('permissions', { + code: { + [Sequelize.Op.in]: [ + 'organization_data_update', + 'organization_append_relatedOrg', + 'organization_remove_relatedOrg', + ], + }, + }) + } catch (error) { + console.error('Error rolling back migration:', error) + throw error + } + }, +} diff --git a/src/database/migrations/20240313134927-add-admin-role-permission-mapping-for-related-orgs-apis.js b/src/database/migrations/20240313134927-add-admin-role-permission-mapping-for-related-orgs-apis.js new file mode 100644 index 000000000..dd9e82113 --- /dev/null +++ b/src/database/migrations/20240313134927-add-admin-role-permission-mapping-for-related-orgs-apis.js @@ -0,0 +1,118 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ + +require('module-alias/register') +require('dotenv').config() +const common = require('@constants/common') +const Permissions = require('@database/models/index').Permission +const rolePermission = require('@database/models/index').RolePermission + +const getPermissionId = async (module, request_type, api_path) => { + try { + const permission = await Permissions.findOne({ + where: { module, request_type, api_path }, + }) + if (!permission) { + throw new Error( + `Permission not found for module: ${module}, request_type: ${request_type}, api_path: ${api_path}` + ) + } + return permission.id + } catch (error) { + throw new Error(`Error while fetching permission: ${error.message}`) + } +} + +module.exports = { + async up(queryInterface, Sequelize) { + try { + const rolePermissionsData = await Promise.all([ + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('organization', ['POST'], 'user/v1/organization/update/*'), + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/update/*', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId( + 'organization', + ['POST'], + 'user/v1/organization/addRelatedOrg/*' + ), + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/addRelatedOrg/*', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId( + 'organization', + ['POST'], + 'user/v1/organization/removeRelatedOrg/*' + ), + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/removeRelatedOrg/*', + }, + ]) + + await queryInterface.bulkInsert( + 'role_permission_mapping', + rolePermissionsData.map((data) => ({ + ...data, + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + })) + ) + } catch (error) { + console.log(error) + console.error(`Migration error: ${error.message}`) + throw error + } + }, + + async down(queryInterface, Sequelize) { + try { + // Array of objects representing data to be deleted + const dataToDelete = [ + { + role_title: common.ADMIN_ROLE, + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/update/*', + }, + { + role_title: common.ADMIN_ROLE, + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/addRelatedOrg/*', + }, + { + role_title: common.ADMIN_ROLE, + module: 'organization', + request_type: ['POST'], + api_path: 'user/v1/organization/removeRelatedOrg/*', + }, + ] + + // Delete records based on each object's criteria + for (const item of dataToDelete) { + const permissionId = await getPermissionId(item.module, item.request_type, item.api_path) + + await queryInterface.bulkDelete('role_permission_mapping', { + role_title: item.role_title, + permission_id: permissionId, + module: item.module, + api_path: item.api_path, + }) + } + } catch (error) { + console.error('Error rolling back migration:', error) + throw error + } + }, +} diff --git a/src/locales/en.json b/src/locales/en.json index 8a4d9913c..9cba64a1a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -112,5 +112,6 @@ "ROLE_NOT_DELETED": "Roles not deleted", "ROLES_HAS_EMPTY_LIST": "Empty roles list", "COLUMN_DOES_NOT_EXISTS": "Role column does not exists", - "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance." + "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance.", + "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values." } diff --git a/src/services/organization.js b/src/services/organization.js index 0f9a1c2f7..8fa974c6c 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -368,7 +368,7 @@ module.exports = class OrganizationsHelper { static async addRelatedOrg(id, relatedOrgs) { try { // fetch organization details before update - const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id: id }) + const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id }) if (!orgDetailsBeforeUpdate) { return responses.failureResponse({ statusCode: httpStatusCode.not_acceptable, @@ -381,10 +381,10 @@ module.exports = class OrganizationsHelper { // check if there are any addition to related_org if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, newRelatedOrgs)) { - // update parent org related orgs - const parentOrganzationUpdate = await organizationQueries.update( + // update org related orgs + await organizationQueries.update( { - id: id, + id, }, { related_orgs: newRelatedOrgs, @@ -394,8 +394,8 @@ module.exports = class OrganizationsHelper { raw: true, } ) - // update child org related orgs - const childOrganzationUpdate = await organizationQueries.appendRelatedOrg(id, newRelatedOrgs, { + // update related orgs to append org Id + await organizationQueries.appendRelatedOrg(id, newRelatedOrgs, { returning: true, raw: true, }) @@ -421,7 +421,7 @@ module.exports = class OrganizationsHelper { static async removeRelatedOrg(id, relatedOrgs) { try { // fetch organization details before update - const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id: id }) + const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id }) if (!orgDetailsBeforeUpdate) { return responses.failureResponse({ statusCode: httpStatusCode.not_acceptable, @@ -432,10 +432,23 @@ module.exports = class OrganizationsHelper { const relatedOrganizations = _.difference(orgDetailsBeforeUpdate?.related_orgs, relatedOrgs) + // check if the given org ids are present in the organization's related org + const relatedOrgMismatchFlag = relatedOrgs.some( + (orgId) => !orgDetailsBeforeUpdate?.related_orgs.includes(orgId) + ) + + if (relatedOrgMismatchFlag) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + message: 'RELATED_ORG_REMOVAL_FAILED', + }) + } + // check if there are any addition to related_org if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, relatedOrganizations)) { - // update parent org related orgs - const parentOrganzationUpdate = await organizationQueries.update( + // update org remove related orgs + await organizationQueries.update( { id: parseInt(id, 10), }, @@ -448,8 +461,8 @@ module.exports = class OrganizationsHelper { } ) - // update child org related orgs - const childOrganzationUpdate = await organizationQueries.removeRelatedOrg(id, relatedOrgs, { + // update related orgs remove orgId + await organizationQueries.removeRelatedOrg(id, relatedOrgs, { returning: true, raw: true, }) From 66fe8b695fd3c42598bcd0128fe170134e97648d Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Fri, 15 Mar 2024 11:19:05 +0530 Subject: [PATCH 08/67] related organization fix --- src/services/organization.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/services/organization.js b/src/services/organization.js index 8fa974c6c..6781de686 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -365,7 +365,7 @@ module.exports = class OrganizationsHelper { } } - static async addRelatedOrg(id, relatedOrgs) { + static async addRelatedOrg(id, relatedOrgs = []) { try { // fetch organization details before update const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id }) @@ -377,7 +377,7 @@ module.exports = class OrganizationsHelper { }) } // append related organizations and make sure it is unique - let newRelatedOrgs = [...new Set([...orgDetailsBeforeUpdate?.related_orgs, ...relatedOrgs])] + let newRelatedOrgs = [...new Set([...(orgDetailsBeforeUpdate?.related_orgs ?? []), ...relatedOrgs])] // check if there are any addition to related_org if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, newRelatedOrgs)) { @@ -418,7 +418,7 @@ module.exports = class OrganizationsHelper { throw error } } - static async removeRelatedOrg(id, relatedOrgs) { + static async removeRelatedOrg(id, relatedOrgs = []) { try { // fetch organization details before update const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id }) @@ -429,7 +429,13 @@ module.exports = class OrganizationsHelper { message: 'ORGANIZATION_NOT_FOUND', }) } - + if (orgDetailsBeforeUpdate?.related_orgs == null) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + message: 'RELATED_ORG_REMOVAL_FAILED', + }) + } const relatedOrganizations = _.difference(orgDetailsBeforeUpdate?.related_orgs, relatedOrgs) // check if the given org ids are present in the organization's related org From 05f40f0691f9221007c5e5bf8970eb3ebfe97842 Mon Sep 17 00:00:00 2001 From: joffinjoy Date: Fri, 15 Mar 2024 14:54:50 +0530 Subject: [PATCH 09/67] 2.5 Setup Docs Added --- src/api-doc/user-2.5-setup.md | 511 ++++++++++++++++++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 src/api-doc/user-2.5-setup.md diff --git a/src/api-doc/user-2.5-setup.md b/src/api-doc/user-2.5-setup.md new file mode 100644 index 000000000..967d72f0c --- /dev/null +++ b/src/api-doc/user-2.5-setup.md @@ -0,0 +1,511 @@ +# ShikshaLokam User Project Documentation + +## System Requirements + +- **Operating System:** Ubuntu 22 +- **Node.js:** v20 +- **PostgreSQL:** 16 +- **Citus:** 12.1 + +## Installations + +### Install Node.js LTS + +Refer to the [NodeSource distributions installation scripts](https://github.com/nodesource/distributions#installation-scripts) for Node.js installation. + +```bash +$ curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - &&\ +sudo apt-get install -y nodejs +``` + +### Install Build Essential + +```bash +$ sudo apt-get install build-essential +``` + +### Install Kafka + +Refer to [Kafka Ubuntu 22.04 setup guide](https://www.fosstechnix.com/install-apache-kafka-on-ubuntu-22-04-lts/) + +1. Install OpenJDK 11: + + ```bash + $ sudo apt install openjdk-11-jdk + ``` + +2. Download and extract Kafka: + + ```bash + $ sudo wget https://downloads.apache.org/kafka/3.5.0/kafka_2.12-3.5.0.tgz + $ sudo tar xzf kafka_2.12-3.5.0.tgz + $ sudo mv kafka_2.12-3.5.0 /opt/kafka + ``` + +3. Configure Zookeeper: + + ```bash + $ sudo nano /etc/systemd/system/zookeeper.service + ``` + + Paste the following lines into the `zookeeper.service` file: + + ```ini + /etc/systemd/system/zookeeper.service + [Unit] + Description=Apache Zookeeper service + Documentation=http://zookeeper.apache.org + Requires=network.target remote-fs.target + After=network.target remote-fs.target + + [Service] + Type=simple + ExecStart=/opt/kafka/bin/zookeeper-server-start.sh /opt/kafka/config/zookeeper.properties + ExecStop=/opt/kafka/bin/zookeeper-server-stop.sh + Restart=on-abnormal + + [Install] + WantedBy=multi-user.target + ``` + + Save and exit. + +4. Reload systemd: + + ```bash + $ sudo systemctl daemon-reload + ``` + +5. Configure Kafka: + + ```bash + $ sudo nano /etc/systemd/system/kafka.service + ``` + + Paste the following lines into the `kafka.service` file: + + ```ini + [Unit] + Description=Apache Kafka Service + Documentation=http://kafka.apache.org/documentation.html + Requires=zookeeper.service + + [Service] + Type=simple + Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" + ExecStart=/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties + ExecStop=/opt/kafka/bin/kafka-server-stop.sh + + [Install] + WantedBy=multi-user.target + ``` + + Save and exit. + +6. Reload systemd: + + ```bash + $ sudo systemctl daemon-reload + ``` + +7. Start Zookeeper: + + ```bash + $ sudo systemctl start zookeeper + ``` + + Check status: + + ```bash + $ sudo systemctl status zookeeper + ``` + + Zookeeper service status should be shown as active (running). + +8. Start Kafka: + + ```bash + $ sudo systemctl start kafka + ``` + + Check status: + + ```bash + $ sudo systemctl status kafka + ``` + + Kafka status should be shown as active (running). + +### Install Redis + +Refer to [Redis Ubuntu 22.04 setup guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-22-04) + +1. Update the package list: + + ```bash + $ sudo apt update + ``` + +2. Install Redis: + + ```bash + $ sudo apt install redis-server + ``` + +3. Configure Redis for systemd: + + ```bash + $ sudo nano /etc/redis/redis.conf + ``` + + Find the `supervised` directive and change it to "systemd" as follows: + + ```conf + . . . + # If you run Redis from upstart or systemd, Redis can interact with your + # supervision tree. Options: + # supervised no - no supervision interaction + # supervised upstart - signal upstart by putting Redis into SIGSTOP mode + # supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET + # supervised auto - detect upstart or systemd method based on + # UPSTART_JOB or NOTIFY_SOCKET environment variables + # Note: these supervision methods only signal "process is ready." + # They do not enable continuous liveness pings back to your supervisor. + supervised systemd + . . . + ``` + + Save and exit. + +4. Restart the Redis service: + + ```bash + $ sudo systemctl restart redis.service + ``` + +### Install Single-Node Citus (Distributed Database) + +Refer to [official Citus single-node setup](https://docs.citusdata.com/en/stable/installation/single_node_debian.html) + +1. Download and install Citus: + + ```bash + $ curl https://install.citusdata.com/community/deb.sh | sudo bash + $ sudo apt-get -y install postgresql-16-citus-12.1 + ``` + +2. Switch to the PostgreSQL user: + + ```bash + $ sudo su - postgres + ``` + +3. Set the PostgreSQL bin directory in the PATH and create a directory for Citus: + + ```bash + $ export PATH=$PATH:/usr/lib/postgresql/16/bin + $ cd ~ + $ mkdir citus + ``` + +4. Initialize the Citus database: + + ```bash + $ initdb -D citus + ``` + +5. Configure Citus in `citus/postgresql.conf`: + + ```bash + $ echo "shared_preload_libraries = 'citus'" >> citus/postgresql.conf + ``` + +6. Start the Citus server: + + ```bash + $ pg_ctl -D citus -o "-p 9700" -l citus_logfile start + ``` + +7. Create the Citus extension: + + ```bash + $ psql -p 9700 -c "CREATE EXTENSION citus;" + ``` + +8. Check the Citus version: + + ```bash + $ psql -p 9700 -c "select citus_version();" + ``` + + You should see an output similar to the following, indicating that Citus is successfully installed: + + ```sql + postgres=# select citus_version(); + citus_version + ---------------------------------------------------------------------------------------------------- + Citus 12.1.1 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit + (1 row) + ``` + +### Install PM2 + +Refer to [How To Set Up a Node.js Application for Production on Ubuntu 22.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-22-04). + +**Exit the postgres user account and run the following command** + +```bash +$ sudo npm install pm2@latest -g +``` + +## Setting up Repository + +### Clone the user repository to /opt/backend directory + +```bash +opt/backend$ git clone -b develop-2.5 --single-branch "https://github.com/ELEVATE-Project/user.git" +``` + +### Install Npm packages from src directory + +```bash +backend/user/src$ sudo npm i +``` + +### Create .env file in src directory + +```bash +user/src$ sudo nano .env +``` + +Copy-paste the following env variables to the `.env` file: + +NB : Make sure to update the credentials according to your configurations. + +```env +ACCESS_TOKEN_EXPIRY= 10 +ACCESS_TOKEN_SECRET= asadsd8as7df9as8df987asdf +ADMIN_INVITEE_UPLOAD_EMAIL_TEMPLATE_CODE= invitee_upload_status +ADMIN_SECRET_CODE= Na7ad23ws5cm3kfmw24dmdsflaksd +API_DOC_URL=/user/api-doc +APPLICATION_ENV=development +APPLICATION_PORT=3001 +APP_NAME=MentorED + +AWS_ACCESS_KEY_ID= "adsfg98a7sdfg" +AWS_BUCKET_ENDPOINT="s3.ap-south-1.amazonaws.com" +AWS_BUCKET_REGION="ap-south-1" +AWS_SECRET_ACCESS_KEY="asd9786fg9a8sd/asdfg9a8sd7fg" + + +AZURE_ACCOUNT_KEY=asd897gfa09sd87f09as8d +AZURE_ACCOUNT_NAME=mentoring +CLEAR_INTERNAL_CACHE=userinternal +CLOUD_STORAGE= GCP +DEFAULT_AWS_BUCKET_NAME=mentoring-dev-storage +DEFAULT_AZURE_CONTAINER_NAME=mentoring-images +DEFAULT_GCP_BUCKET_NAME=mentoring-dev-storage + +DEFAULT_ORGANISATION_CODE= default_code +DEFAULT_ORG_ID= 1 +DEFAULT_QUEUE= user-queue +DEFAULT_ROLE= mentee +DEV_DATABASE_URL= postgres://shikshalokam:slpassword123@localhost:9700/elevate_user +DISABLE_LOG= false +EMAIL_ID_ENCRYPTION_ALGORITHM= aes-256-cbc +EMAIL_ID_ENCRYPTION_IV= a19f1ewaqwei9e03edkc32e +EMAIL_ID_ENCRYPTION_KEY= 9bszawjkckw2e3dm35fcw27ws4ed5rftg6y6y7y7654tf4rwq5tr0ol2qa9owsie +ENABLE_EMAIL_OTP_VERIFICATION=true +ENABLE_LOG=true +ERROR_LOG_LEVEL=silly +EVENT_ENABLE_ORG_EVENTS=true +EVENT_ORG_LISTENER_URLS=http://localhost:3567/mentoring/v1/organization/eventListener +GCP_PATH=gcp.json +GCP_PROJECT_ID=sl-dev-project +GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite +INTERNAL_ACCESS_TOKEN= Fqdkfaswekdlwe +INTERNAL_CACHE_EXP_TIME= 86400 +INVITEE_EMAIL_TEMPLATE_CODE= invite_user +IV= LKYTTAqkajswiawqw/Z== +KAFKA_GROUP_ID=dev.users +KAFKA_TOPIC= dev.topic +KAFKA_URL= localhost:9092 +KEY= W/m2cr/aMswjrdsa23sgfy5e34d+bKcbAWZSLjJP2qY= +MENTEE_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentee +MENTORING_SERVICE_URL= http://localhost:3000 +MENTOR_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentor +MENTOR_REQUEST_ACCEPTED_EMAIL_TEMPLATE_CODE= mentor_request_accepted +MENTOR_REQUEST_REJECTED_EMAIL_TEMPLATE_CODE= mentor_request_rejected +MENTOR_SECRET_CODE=4567 +NOTIFICATION_KAFKA_TOPIC=dev.notification +ORG_ADMIN_INVITATION_EMAIL_TEMPLATE_CODE= invite_org_admin +OTP_EMAIL_TEMPLATE_CODE= emailotp +OTP_EXP_TIME= 86400 +PORTAL_URL= "https://dev.elevate-mentoring.shikshalokam.org/auth/login" +RATING_KAFKA_TOPIC= dev.mentor_rating +REDIS_HOST= redis://localhost:6379 +REFRESH_TOKEN_EXPIRY= 183 +REFRESH_TOKEN_SECRET=371hkjadidy2ashiKAkajshdkid23iuekw71yekiaskdvkvegxvy23t78veQwexqviveit6ttZyeeytx62tx236uv +REFRESH_VIEW_INTERVAL=30000 +REGISTRATION_EMAIL_TEMPLATE_CODE= registration +REGISTRATION_OTP_EMAIL_TEMPLATE_CODE= registrationotp +SALT_ROUNDS= 10 +SAMPLE_CSV_FILE_PATH= sample/bulk_user_creation.csv +SCHEDULER_SERVICE_BASE_URL= /scheduler/ +SCHEDULER_SERVICE_ERROR_REPORTING_EMAIL_ID= rakesh.k@pacewisdom.com +SCHEDULER_SERVICE_HOST= http://localhost:3567 +SCHEDULER_SERVICE_URL= http://localhost:3567/jobs/scheduleJob +created_time= 2024-02-08T07:40:04.571464939Z +custom_metadata= null +destroyed= false +version= 31 + +``` + +Save and exit. + +## Setting up Databases + +**Log into the postgres user** + +```bash +sudo su postgres +``` + +**Log into psql** + +```bash +psql -p 9700 +``` + +**Create a database user/role:** + +```sql +CREATE USER shikshalokam WITH ENCRYPTED PASSWORD 'slpassword'; +``` + +**Create the elevate_user database** + +```sql +CREATE DATABASE elevate_user; +GRANT ALL PRIVILEGES ON DATABASE elevate_user TO shikshalokam; +\c elevate_user +GRANT ALL ON SCHEMA public TO shikshalokam; +``` + +## Running Migrations To Create Tables + +**Exit the postgres user account and install sequelize-cli globally** + +```bash +$ sudo npm i sequelize-cli -g +``` + +**Navigate to the src folder of user service and run sequelize-cli migration command:** + +```bash +user/src$ npx sequelize-cli db:migrate +``` + +**Now all the tables must be available in the Citus databases** + +## Setting up Distribution Columns in Citus PostgreSQL Database + +Refer [Choosing Distribution Column](https://docs.citusdata.com/en/stable/sharding/data_modeling.html) for more information regarding Citus distribution columns. + +**Login into the postgres user** + +```bash +sudo su postgres +``` + +**Login to psql** + +```bash +psql -p 9700 +``` + +**Login to the elevate_user database** + +```sql +\c elevate_user +``` + +**Enable Citus for elevate_user** + +```sql +CREATE EXTENSION citus; +``` + +**Within elevate_user, run the following queries:** + +```sql +SELECT create_distributed_table('entities', 'entity_type_id'); +SELECT create_distributed_table('entity_types', 'organization_id'); +SELECT create_distributed_table('file_uploads', 'organization_id'); +SELECT create_distributed_table('forms', 'organization_id'); +SELECT create_distributed_table('notification_templates', 'organization_id'); +SELECT create_distributed_table('organizations', 'id'); +SELECT create_distributed_table('organization_codes', 'code'); +SELECT create_distributed_table('organization_domains', 'domain'); +SELECT create_distributed_table('organization_role_requests','organization_id'); +SELECT create_distributed_table('organization_user_invites','organization_id'); +SELECT create_distributed_table('users_credentials','email'); +SELECT create_distributed_table('users', 'organization_id'); +``` + +## Running Seeder to Populate the Tables with Seed Data + +**Exit the postgres user navigate to the script folder of the user service** + +**Run the insertDefaultOrg.js script** + +```bash +src/scripts$ node insertDefaultOrg.js +``` + +_Keep note of the default organization id generated by the script_ + +**Navigate to the src folder of the user service and update the .env file with these variables:** + +```env +DEFAULT_ORG_ID= +DEFAULT_ORGANISATION_CODE=default_code +``` + +**Run the seeder command** + +```bash +src$ npm run db:seed:all +``` + +## Start the Service + +Run pm2 start command: + +```bash +user/src$ pm2 start app.js -i 2 --name elevate-user +``` + +#### Run pm2 ls command + +```bash +$ pm2 ls +``` + +Output should look like this (Sample output, might slightly differ in your installation): + +```bash +┌────┬─────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ +│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ +├────┼─────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ +│ 1 │ elevate-user │ default │ 1.0.0 │ cluster │ 106976 │ 27h │ 0 │ online │ 0% │ 167.0mb │ jenkins │ disabled │ +│ 2 │ elevate-user │ default │ 1.0.0 │ cluster │ 106986 │ 27h │ 0 │ online │ 0% │ 169.3mb │ jenkins │ disabled │ +└────┴─────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ +``` + +This concludes the services and dependency setup. + +## Postman Collections + +- [User Service](https://github.com/ELEVATE-Project/user/tree/develop-2.5/src/api-doc) From 802c5560cc4f07405ffdfca645ce3880009a8b2b Mon Sep 17 00:00:00 2001 From: joffinjoy Date: Fri, 15 Mar 2024 17:50:50 +0530 Subject: [PATCH 10/67] Added Setup Guide To README --- README.md | 543 +++++++++++++++++++++++++++++----- src/api-doc/user-2.5-setup.md | 1 + 2 files changed, 462 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 02437da5e..a152daa23 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ The Mentoring building block enables effective mentoring interactions between me
+## System Requirements + +- **Operating System:** Ubuntu 22 +- **Node.js:** v20 +- **PostgreSQL:** 16 +- **Citus:** 12.1 +- **Apache Kafka:** 3.5.0 + # Setup Options Elevate user services can be setup in local using two methods: @@ -132,146 +140,517 @@ Elevate user services can be setup in local using two methods: -
Local Service with local dependencies(Hardest) +
+ +Local Service with local dependencies(Hardest) **Expectation**: Run single service with existing local dependencies in host (**Non-Docker Implementation**). -### Steps +## Installations + +### Install Node.js LTS + +Refer to the [NodeSource distributions installation scripts](https://github.com/nodesource/distributions#installation-scripts) for Node.js installation. -1. Install required tools & dependencies +```bash +$ curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - &&\ +sudo apt-get install -y nodejs +``` - Install any IDE (eg: VScode) +### Install Build Essential - Install Nodejs: https://nodejs.org/en/download/ +```bash +$ sudo apt-get install build-essential +``` - Install MongoDB: https://docs.mongodb.com/manual/installation/ +### Install Kafka - Install Robo-3T: ​​ https://robomongo.org/ +Refer to [Kafka Ubuntu 22.04 setup guide](https://www.fosstechnix.com/install-apache-kafka-on-ubuntu-22-04-lts/) -2. Clone the **User service** repository. +1. Install OpenJDK 11: + ```bash + $ sudo apt install openjdk-11-jdk ``` - git clone https://github.com/ELEVATE-Project/user.git + +2. Download and extract Kafka: + + ```bash + $ sudo wget https://downloads.apache.org/kafka/3.5.0/kafka_2.12-3.5.0.tgz + $ sudo tar xzf kafka_2.12-3.5.0.tgz + $ sudo mv kafka_2.12-3.5.0 /opt/kafka ``` -3. Add **.env** file to the project directory +3. Configure Zookeeper: + + ```bash + $ sudo nano /etc/systemd/system/zookeeper.service + ``` + + Paste the following lines into the `zookeeper.service` file: + + ```ini + /etc/systemd/system/zookeeper.service + [Unit] + Description=Apache Zookeeper service + Documentation=http://zookeeper.apache.org + Requires=network.target remote-fs.target + After=network.target remote-fs.target - Create a **.env** file in **src** directory of the project and copy these environment variables into it. + [Service] + Type=simple + ExecStart=/opt/kafka/bin/zookeeper-server-start.sh /opt/kafka/config/zookeeper.properties + ExecStop=/opt/kafka/bin/zookeeper-server-stop.sh + Restart=on-abnormal + [Install] + WantedBy=multi-user.target ``` - #User Service Config - # Port on which service runs - APPLICATION_PORT = 3000 + Save and exit. - # Service environment - APPLICATION_ENV = development +4. Reload systemd: - # Database connectivity url - MONGODB_URL = mongodb://localhost:27017/db-name + ```bash + $ sudo systemctl daemon-reload + ``` + +5. Configure Kafka: + + ```bash + $ sudo nano /etc/systemd/system/kafka.service + ``` - # Token secret to generate access token - ACCESS_TOKEN_SECRET = 'access-token-secret' + Paste the following lines into the `kafka.service` file: - # Token secret to generate refresh token - REFRESH_TOKEN_SECRET = 'refresh-token-secret' + ```ini + [Unit] + Description=Apache Kafka Service + Documentation=http://kafka.apache.org/documentation.html + Requires=zookeeper.service - # Kafka hosted server url - KAFKA_URL = localhost:9092 + [Service] + Type=simple + Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" + ExecStart=/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties + ExecStop=/opt/kafka/bin/kafka-server-stop.sh - # Kafka group to which consumer belongs - KAFKA_GROUP_ID = userservice + [Install] + WantedBy=multi-user.target + ``` - # Kafka topic to consume data from - KAFKA_TOPIC = 'topic' + Save and exit. - # Kafka topic to push notification data - NOTIFICATION_KAFKA_TOPIC = notificationtopic +6. Reload systemd: - # Any one of three features available for cloud storage - CLOUD_STORAGE = 'GCP/AWS/AZURE' + ```bash + $ sudo systemctl daemon-reload + ``` - # Gcp json config file path - GCP_PATH = 'gcp.json' +7. Start Zookeeper: - # Gcp bucket name which stores files - DEFAULT_GCP_BUCKET_NAME = 'gcp-bucket-storage-name' + ```bash + $ sudo systemctl start zookeeper + ``` - # Gcp project id - GCP_PROJECT_ID = 'project-id' + Check status: - # Aws access key id - AWS_ACCESS_KEY_ID = 'aws-access-key-id' + ```bash + $ sudo systemctl status zookeeper + ``` - # Aws secret access key - AWS_SECRET_ACCESS_KEY = 'aws-secret-access-key' + Zookeeper service status should be shown as active (running). - # Aws region where bucket will be located - AWS_BUCKET_REGION = 'ap-south-1' +8. Start Kafka: - # Aws end point - AWS_BUCKET_ENDPOINT = 's3.ap-south-1.amazonaws.com' + ```bash + $ sudo systemctl start kafka + ``` - # Aws bucket name which stores files - DEFAULT_AWS_BUCKET_NAME = 'aws-bucket-storage-name' + Check status: - # Azure storage account name - AZURE_ACCOUNT_NAME = 'account-name' + ```bash + $ sudo systemctl status kafka + ``` - # Azure storage account key - AZURE_ACCOUNT_KEY = 'azure-account-key' + Kafka status should be shown as active (running). - # Azure storage container which stores files - DEFAULT_AZURE_CONTAINER_NAME = 'azure-container-storage-name' +### Install Redis - # Internal access token for communicationcation between services via network call - INTERNAL_ACCESS_TOKEN = 'internal-access-token' +Refer to [Redis Ubuntu 22.04 setup guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-22-04) - # JWT Access Token expiry In Days - ACCESS_TOKEN_EXPIRY = '1' +1. Update the package list: - # JWT Refresh Token expiry In Days - REFRESH_TOKEN_EXPIRY = '183' + ```bash + $ sudo apt update + ``` - # Redis Host connectivity url - REDIS_HOST = 'redis://localhost:6379' +2. Install Redis: - # Otp expiration time for forgetpassword or registration process - OTP_EXP_TIME = 86400 + ```bash + $ sudo apt install redis-server + ``` - # Enable email based otp verification for registration process - ENABLE_EMAIL_OTP_VERIFICATION = true +3. Configure Redis for systemd: - # Api doc url - API_DOC_URL = '/api-doc' + ```bash + $ sudo nano /etc/redis/redis.conf ``` -4. Start MongoDB locally + Find the `supervised` directive and change it to "systemd" as follows: + + ```conf + . . . + # If you run Redis from upstart or systemd, Redis can interact with your + # supervision tree. Options: + # supervised no - no supervision interaction + # supervised upstart - signal upstart by putting Redis into SIGSTOP mode + # supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET + # supervised auto - detect upstart or systemd method based on + # UPSTART_JOB or NOTIFY_SOCKET environment variables + # Note: these supervision methods only signal "process is ready." + # They do not enable continuous liveness pings back to your supervisor. + supervised systemd + . . . + ``` - Based on your host operating system and method used, start MongoDB. + Save and exit. -5. Install Npm packages +4. Restart the Redis service: + ```bash + $ sudo systemctl restart redis.service ``` - ELEVATE/user/src$ npm install + +### Install Single-Node Citus (Distributed Database) + +Refer to [official Citus single-node setup](https://docs.citusdata.com/en/stable/installation/single_node_debian.html) + +1. Download and install Citus: + + ```bash + $ curl https://install.citusdata.com/community/deb.sh | sudo bash + $ sudo apt-get -y install postgresql-16-citus-12.1 ``` -6. Start User server +2. Switch to the PostgreSQL user: + ```bash + $ sudo su - postgres ``` - ELEVATE/user/src$ npm start + +3. Set the PostgreSQL bin directory in the PATH and create a directory for Citus: + + ```bash + $ export PATH=$PATH:/usr/lib/postgresql/16/bin + $ cd ~ + $ mkdir citus ``` -
-
+4. Initialize the Citus database: + + ```bash + $ initdb -D citus + ``` + +5. Configure Citus in `citus/postgresql.conf`: + + ```bash + $ echo "shared_preload_libraries = 'citus'" >> citus/postgresql.conf + ``` + +6. Start the Citus server: + + ```bash + $ pg_ctl -D citus -o "-p 9700" -l citus_logfile start + ``` + +7. Create the Citus extension: + + ```bash + $ psql -p 9700 -c "CREATE EXTENSION citus;" + ``` + +8. Check the Citus version: + + ```bash + $ psql -p 9700 -c "select citus_version();" + ``` + + You should see an output similar to the following, indicating that Citus is successfully installed: + + ```sql + postgres=# select citus_version(); + citus_version + ---------------------------------------------------------------------------------------------------- + Citus 12.1.1 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit + (1 row) + ``` + +### Install PM2 + +Refer to [How To Set Up a Node.js Application for Production on Ubuntu 22.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-22-04). + +**Exit the postgres user account and run the following command** + +```bash +$ sudo npm install pm2@latest -g +``` + +## Setting up Repository + +### Clone the user repository to /opt/backend directory + +```bash +opt/backend$ git clone -b develop-2.5 --single-branch "https://github.com/ELEVATE-Project/user.git" +``` + +### Install Npm packages from src directory + +```bash +backend/user/src$ sudo npm i +``` + +### Create .env file in src directory + +```bash +user/src$ sudo nano .env +``` + +Copy-paste the following env variables to the `.env` file: + +NB : Make sure to update the credentials according to your configurations. + +```env +ACCESS_TOKEN_EXPIRY= 10 +ACCESS_TOKEN_SECRET= asadsd8as7df9as8df987asdf +ADMIN_INVITEE_UPLOAD_EMAIL_TEMPLATE_CODE= invitee_upload_status +ADMIN_SECRET_CODE= Na7ad23ws5cm3kfmw24dmdsflaksd +API_DOC_URL=/user/api-doc +APPLICATION_ENV=development +APPLICATION_PORT=3001 +APP_NAME=MentorED + +AWS_ACCESS_KEY_ID= "adsfg98a7sdfg" +AWS_BUCKET_ENDPOINT="s3.ap-south-1.amazonaws.com" +AWS_BUCKET_REGION="ap-south-1" +AWS_SECRET_ACCESS_KEY="asd9786fg9a8sd/asdfg9a8sd7fg" + + +AZURE_ACCOUNT_KEY=asd897gfa09sd87f09as8d +AZURE_ACCOUNT_NAME=mentoring +CLEAR_INTERNAL_CACHE=userinternal +CLOUD_STORAGE= GCP +DEFAULT_AWS_BUCKET_NAME=mentoring-dev-storage +DEFAULT_AZURE_CONTAINER_NAME=mentoring-images +DEFAULT_GCP_BUCKET_NAME=mentoring-dev-storage + +DEFAULT_ORGANISATION_CODE= default_code +DEFAULT_ORG_ID= 1 +DEFAULT_QUEUE= user-queue +DEFAULT_ROLE= mentee +DEV_DATABASE_URL= postgres://shikshalokam:slpassword123@localhost:9700/elevate_user +DISABLE_LOG= false +EMAIL_ID_ENCRYPTION_ALGORITHM= aes-256-cbc +EMAIL_ID_ENCRYPTION_IV= a19f1ewaqwei9e03edkc32e +EMAIL_ID_ENCRYPTION_KEY= 9bszawjkckw2e3dm35fcw27ws4ed5rftg6y6y7y7654tf4rwq5tr0ol2qa9owsie +ENABLE_EMAIL_OTP_VERIFICATION=true +ENABLE_LOG=true +ERROR_LOG_LEVEL=silly +EVENT_ENABLE_ORG_EVENTS=true +EVENT_ORG_LISTENER_URLS=http://localhost:3567/mentoring/v1/organization/eventListener +GCP_PATH=gcp.json +GCP_PROJECT_ID=sl-dev-project +GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite +INTERNAL_ACCESS_TOKEN= Fqdkfaswekdlwe +INTERNAL_CACHE_EXP_TIME= 86400 +INVITEE_EMAIL_TEMPLATE_CODE= invite_user +IV= LKYTTAqkajswiawqw/Z== +KAFKA_GROUP_ID=dev.users +KAFKA_TOPIC= dev.topic +KAFKA_URL= localhost:9092 +KEY= W/m2cr/aMswjrdsa23sgfy5e34d+bKcbAWZSLjJP2qY= +MENTEE_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentee +MENTORING_SERVICE_URL= http://localhost:3000 +MENTOR_INVITATION_EMAIL_TEMPLATE_CODE= invite_mentor +MENTOR_REQUEST_ACCEPTED_EMAIL_TEMPLATE_CODE= mentor_request_accepted +MENTOR_REQUEST_REJECTED_EMAIL_TEMPLATE_CODE= mentor_request_rejected +MENTOR_SECRET_CODE=4567 +NOTIFICATION_KAFKA_TOPIC=dev.notification +ORG_ADMIN_INVITATION_EMAIL_TEMPLATE_CODE= invite_org_admin +OTP_EMAIL_TEMPLATE_CODE= emailotp +OTP_EXP_TIME= 86400 +PORTAL_URL= "https://dev.elevate-mentoring.shikshalokam.org/auth/login" +RATING_KAFKA_TOPIC= dev.mentor_rating +REDIS_HOST= redis://localhost:6379 +REFRESH_TOKEN_EXPIRY= 183 +REFRESH_TOKEN_SECRET=371hkjadidy2ashiKAkajshdkid23iuekw71yekiaskdvkvegxvy23t78veQwexqviveit6ttZyeeytx62tx236uv +REFRESH_VIEW_INTERVAL=30000 +REGISTRATION_EMAIL_TEMPLATE_CODE= registration +REGISTRATION_OTP_EMAIL_TEMPLATE_CODE= registrationotp +SALT_ROUNDS= 10 +SAMPLE_CSV_FILE_PATH= sample/bulk_user_creation.csv +SCHEDULER_SERVICE_BASE_URL= /scheduler/ +SCHEDULER_SERVICE_ERROR_REPORTING_EMAIL_ID= rakesh.k@pacewisdom.com +SCHEDULER_SERVICE_HOST= http://localhost:3567 +SCHEDULER_SERVICE_URL= http://localhost:3567/jobs/scheduleJob +created_time= 2024-02-08T07:40:04.571464939Z +custom_metadata= null +destroyed= false +version= 31 + +``` -# Tech stack +Save and exit. -- Node - 16.0.0 -- Kafka - 3.1.0 -- Jest - 28.1.1 -- MongoDB - 4.1.4 -- Redis - 7.0.0 +## Setting up Databases + +**Log into the postgres user** + +```bash +sudo su postgres +``` + +**Log into psql** + +```bash +psql -p 9700 +``` + +**Create a database user/role:** + +```sql +CREATE USER shikshalokam WITH ENCRYPTED PASSWORD 'slpassword'; +``` + +**Create the elevate_user database** + +```sql +CREATE DATABASE elevate_user; +GRANT ALL PRIVILEGES ON DATABASE elevate_user TO shikshalokam; +\c elevate_user +GRANT ALL ON SCHEMA public TO shikshalokam; +``` + +## Running Migrations To Create Tables + +**Exit the postgres user account and install sequelize-cli globally** + +```bash +$ sudo npm i sequelize-cli -g +``` + +**Navigate to the src folder of user service and run sequelize-cli migration command:** + +```bash +user/src$ npx sequelize-cli db:migrate +``` + +**Now all the tables must be available in the Citus databases** + +## Setting up Distribution Columns in Citus PostgreSQL Database + +Refer [Choosing Distribution Column](https://docs.citusdata.com/en/stable/sharding/data_modeling.html) for more information regarding Citus distribution columns. + +**Login into the postgres user** + +```bash +sudo su postgres +``` + +**Login to psql** + +```bash +psql -p 9700 +``` + +**Login to the elevate_user database** + +```sql +\c elevate_user +``` + +**Enable Citus for elevate_user** + +```sql +CREATE EXTENSION citus; +``` + +**Within elevate_user, run the following queries:** + +```sql +SELECT create_distributed_table('entities', 'entity_type_id'); +SELECT create_distributed_table('entity_types', 'organization_id'); +SELECT create_distributed_table('file_uploads', 'organization_id'); +SELECT create_distributed_table('forms', 'organization_id'); +SELECT create_distributed_table('notification_templates', 'organization_id'); +SELECT create_distributed_table('organizations', 'id'); +SELECT create_distributed_table('organization_codes', 'code'); +SELECT create_distributed_table('organization_domains', 'domain'); +SELECT create_distributed_table('organization_role_requests','organization_id'); +SELECT create_distributed_table('organization_user_invites','organization_id'); +SELECT create_distributed_table('users_credentials','email'); +SELECT create_distributed_table('users', 'organization_id'); +``` + +## Running Seeder to Populate the Tables with Seed Data + +**Exit the postgres user navigate to the script folder of the user service** + +**Run the insertDefaultOrg.js script** + +```bash +src/scripts$ node insertDefaultOrg.js +``` + +_Keep note of the default organization id generated by the script_ + +**Navigate to the src folder of the user service and update the .env file with these variables:** + +```env +DEFAULT_ORG_ID= +DEFAULT_ORGANISATION_CODE=default_code +``` + +**Run the seeder command** + +```bash +src$ npm run db:seed:all +``` + +## Start the Service + +Run pm2 start command: + +```bash +user/src$ pm2 start app.js -i 2 --name elevate-user +``` + +#### Run pm2 ls command + +```bash +$ pm2 ls +``` + +Output should look like this (Sample output, might slightly differ in your installation): + +```bash +┌────┬─────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ +│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ +├────┼─────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ +│ 1 │ elevate-user │ default │ 1.0.0 │ cluster │ 106976 │ 27h │ 0 │ online │ 0% │ 167.0mb │ jenkins │ disabled │ +│ 2 │ elevate-user │ default │ 1.0.0 │ cluster │ 106986 │ 27h │ 0 │ online │ 0% │ 169.3mb │ jenkins │ disabled │ +└────┴─────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ +``` + +This concludes the services and dependency setup. + +## Postman Collections + +- [User Service](https://github.com/ELEVATE-Project/user/tree/develop-2.5/src/api-doc) + +
+
# Migrations Commands diff --git a/src/api-doc/user-2.5-setup.md b/src/api-doc/user-2.5-setup.md index 967d72f0c..06983f273 100644 --- a/src/api-doc/user-2.5-setup.md +++ b/src/api-doc/user-2.5-setup.md @@ -6,6 +6,7 @@ - **Node.js:** v20 - **PostgreSQL:** 16 - **Citus:** 12.1 +- **Apache Kafka:** 3.5.0 ## Installations From c5d225551db792569899acc09bb663059ae43f05 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 18 Mar 2024 14:41:16 +0530 Subject: [PATCH 11/67] Username Enumeration - generateOtp --- src/services/account.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/services/account.js b/src/services/account.js index c0ce8fbed..3c77fa6f6 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -639,10 +639,9 @@ module.exports = class AccountHelper { }, }) if (!userCredentials) - return responses.failureResponse({ - message: 'USER_DOESNOT_EXISTS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'OTP_SENT_SUCCESSFULLY', }) const user = await userQueries.findOne({ @@ -650,10 +649,9 @@ module.exports = class AccountHelper { organization_id: userCredentials.organization_id, }) if (!user) - return responses.failureResponse({ - message: 'USER_DOESNOT_EXISTS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'OTP_SENT_SUCCESSFULLY', }) const isPasswordSame = bcryptJs.compareSync(bodyData.password, userCredentials.password) From 57b10797ef1e2b20c23f61aa82090490d9f2622d Mon Sep 17 00:00:00 2001 From: vishnu Date: Mon, 18 Mar 2024 16:00:08 +0530 Subject: [PATCH 12/67] story-1155 security fix --- src/.env.sample | 3 +++ src/app.js | 6 ++++++ src/envVariables.js | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/src/.env.sample b/src/.env.sample index 35b8fd905..ad7a7da79 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -164,3 +164,6 @@ EVENT_ORG_LISTENER_URLS='http://interface:3567/mentoring/v1/organization/eventLi EVENT_ENABLE_ORG_EVENTS=true #Generic Email template for new users GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite + +# Allowed host by CORS +ALLOWED_HOST = "http://examplDomain.com" diff --git a/src/app.js b/src/app.js index b2a599015..01b82dd28 100644 --- a/src/app.js +++ b/src/app.js @@ -65,6 +65,12 @@ app.use(bodyParser.json({ limit: '50MB' })) app.use(express.static('public')) +// Middleware to set Access-Control-Allow-Origin header +app.use((req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_HOST) + next() +}) + /* Logs request info if environment is configured to enable log */ app.all('*', (req, res, next) => { logger.info('***User Service Request Log***', { diff --git a/src/envVariables.js b/src/envVariables.js index ea6875187..e5ad54689 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -232,6 +232,11 @@ let enviromentVariables = { optional: true, default: 'generic_invite', }, + ALLOWED_HOST: { + message: 'Required CORS allowed host', + optional: true, + default: '*', + }, } let success = true From 195eb24187cb3fe4550786711a2c44c20a3c6144 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 19 Mar 2024 15:32:27 +0530 Subject: [PATCH 13/67] Audit Bug - 1148 , 1152 , 1153 --- src/constants/common.js | 2 +- src/envVariables.js | 4 ++-- src/locales/en.json | 4 +++- src/services/org-admin.js | 13 +++++++++++++ src/validators/v1/account.js | 20 ++++++++++++++++++-- src/validators/v1/admin.js | 10 +++++++++- 6 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/constants/common.js b/src/constants/common.js index 159efde6f..ce9011075 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -28,7 +28,7 @@ module.exports = { '/user/v1/user-role/default', ], notificationEmailType: 'email', - accessTokenExpiry: `${process.env.ACCESS_TOKEN_EXPIRY}d`, + accessTokenExpiry: process.env.ACCESS_TOKEN_EXPIRY, refreshTokenExpiry: `${process.env.REFRESH_TOKEN_EXPIRY}d`, refreshTokenExpiryInMs: Number(process.env.REFRESH_TOKEN_EXPIRY) * 24 * 60 * 60 * 1000, refreshTokenLimit: 3, diff --git a/src/envVariables.js b/src/envVariables.js index e5ad54689..b61ff67ae 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -96,11 +96,11 @@ let enviromentVariables = { optional: process.env.CLOUD_STORAGE === 'AZURE' ? false : true, }, ACCESS_TOKEN_EXPIRY: { - message: 'Required access token expiry in days', + message: 'Required access token expiry', optional: false, }, REFRESH_TOKEN_EXPIRY: { - message: 'Required refresh token expiry in days', + message: 'Required refresh token expiry', optional: false, }, API_DOC_URL: { diff --git a/src/locales/en.json b/src/locales/en.json index 9cba64a1a..b5dad36d7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -113,5 +113,7 @@ "ROLES_HAS_EMPTY_LIST": "Empty roles list", "COLUMN_DOES_NOT_EXISTS": "Role column does not exists", "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance.", - "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values." + "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values.", + "INAVLID_ORG_ROLE_REQ": "Invalid organisation request" + } diff --git a/src/services/org-admin.js b/src/services/org-admin.js index 3b5d136bc..bd496d222 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -237,6 +237,19 @@ module.exports = class OrgAdminHelper { const requestId = bodyData.request_id delete bodyData.request_id + const requestDetail = await orgRoleReqQueries.requestDetails({ + id: requestId, + organization_id: tokenInformation.organization_id, + }) + + if (requestDetail.status !== common.REQUESTED_STATUS) { + return responses.failureResponse({ + message: 'INAVLID_ORG_ROLE_REQ', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + bodyData.handled_by = tokenInformation.id const rowsAffected = await orgRoleReqQueries.update( { id: requestId, organization_id: tokenInformation.organization_id }, diff --git a/src/validators/v1/account.js b/src/validators/v1/account.js index 449860c2d..260c05c98 100644 --- a/src/validators/v1/account.js +++ b/src/validators/v1/account.js @@ -25,7 +25,15 @@ module.exports = { .withMessage('email is invalid') .normalizeEmail({ gmail_remove_dots: false }) - req.checkBody('password').trim().notEmpty().withMessage('password field is empty') + req.checkBody('password') + .notEmpty() + .withMessage('Password field is empty') + .matches(/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+{}|:"<>?~`\-=[\];',.\/])[^ ]{10,}$/) + .withMessage( + 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long' + ) + .custom((value) => !/\s/.test(value)) + .withMessage('Password cannot contain spaces') if (req.body.role) { req.checkBody('role').trim().not().isIn([common.ADMIN_ROLE]).withMessage("User does't have admin access") @@ -64,7 +72,15 @@ module.exports = { resetPassword: (req) => { req.checkBody('email').notEmpty().withMessage('email field is empty').isEmail().withMessage('email is invalid') - req.checkBody('password').notEmpty().withMessage('password field is empty') + req.checkBody('password') + .notEmpty() + .withMessage('Password field is empty') + .matches(/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+{}|:"<>?~`\-=[\];',.\/])[^ ]{10,}$/) + .withMessage( + 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long' + ) + .custom((value) => !/\s/.test(value)) + .withMessage('Password cannot contain spaces') req.checkBody('otp') .notEmpty() diff --git a/src/validators/v1/admin.js b/src/validators/v1/admin.js index 575be9190..75ffdbc89 100644 --- a/src/validators/v1/admin.js +++ b/src/validators/v1/admin.js @@ -27,7 +27,15 @@ module.exports = { .withMessage('email is invalid') .normalizeEmail() - req.checkBody('password').trim().notEmpty().withMessage('password field is empty') + req.checkBody('password') + .notEmpty() + .withMessage('Password field is empty') + .matches(/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+{}|:"<>?~`\-=[\];',.\/])[^ ]{10,}$/) + .withMessage( + 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long' + ) + .custom((value) => !/\s/.test(value)) + .withMessage('Password cannot contain spaces') }, login: (req) => { From 861c2a0afacfa58b2f05fa57735e415c9319f5a8 Mon Sep 17 00:00:00 2001 From: vishnu Date: Tue, 19 Mar 2024 16:18:40 +0530 Subject: [PATCH 14/67] 1142 -security fix, downloadableUrl API --- src/.env.sample | 3 +++ src/envVariables.js | 5 +++++ src/generics/utils.js | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/.env.sample b/src/.env.sample index ad7a7da79..858787335 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -167,3 +167,6 @@ GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite # Allowed host by CORS ALLOWED_HOST = "http://examplDomain.com" + +# Downloadabale url exipres after +DOWNLOAD_URL_EXPIRATION_DURATION = 120000 \ No newline at end of file diff --git a/src/envVariables.js b/src/envVariables.js index e5ad54689..5fc0ce707 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -237,6 +237,11 @@ let enviromentVariables = { optional: true, default: '*', }, + DOWNLOAD_URL_EXPIRATION_DURATION: { + message: 'Required downloadable url expiration time', + optional: true, + default: 3600000, + }, } let success = true diff --git a/src/generics/utils.js b/src/generics/utils.js index 7a57f1fc8..2e45d706f 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -53,8 +53,9 @@ const getDownloadableUrl = async (imgPath) => { bucketName: process.env.DEFAULT_GCP_BUCKET_NAME, gcpProjectId: process.env.GCP_PROJECT_ID, gcpJsonFilePath: path.join(__dirname, '../', process.env.GCP_PATH), + expiry: Date.now() + parseFloat(process.env.DOWNLOAD_URL_EXPIRATION_DURATION), } - imgPath = await GcpFileHelper.getDownloadableUrl(options) + imgPath = await GcpFileHelper.getSignedDownloadableUrl(options) } else if (process.env.CLOUD_STORAGE === 'AWS') { const options = { destFilePath: imgPath, From 0e825a01ece75029fa87859adbee2f6785e83448 Mon Sep 17 00:00:00 2001 From: vishnu Date: Tue, 19 Mar 2024 16:24:39 +0530 Subject: [PATCH 15/67] package version updated --- src/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.json b/src/package.json index c6da7f3ee..39f87028e 100644 --- a/src/package.json +++ b/src/package.json @@ -37,7 +37,7 @@ "crypto": "^1.0.1", "csvtojson": "^2.0.10", "dotenv": "^10.0.0", - "elevate-cloud-storage": "2.0.0", + "elevate-cloud-storage": "2.6.1", "elevate-encryption": "^1.0.1", "elevate-logger": "^3.1.0", "elevate-node-cache": "^1.0.6", From cfe344da767dc54e43a83bc98ea9d1165c5fca65 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 19 Mar 2024 23:04:08 +0530 Subject: [PATCH 16/67] blacklisting update --- src/constants/blacklistConfig.js | 385 +++++++++++++++++++ src/validators/common.js | 3 + src/validators/v1/account.js | 32 ++ src/validators/v1/admin.js | 6 + src/validators/v1/entity-type.js | 5 +- src/validators/v1/entity.js | 4 + src/validators/v1/form.js | 4 + src/validators/v1/modules.js | 4 + src/validators/v1/notification.js | 6 +- src/validators/v1/org-admin.js | 4 + src/validators/v1/organization.js | 6 +- src/validators/v1/permissions.js | 5 +- src/validators/v1/role-permission-mapping.js | 4 + src/validators/v1/user-role.js | 4 + src/validators/v1/user.js | 4 +- 15 files changed, 470 insertions(+), 6 deletions(-) create mode 100644 src/constants/blacklistConfig.js create mode 100644 src/validators/common.js diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js new file mode 100644 index 000000000..72c39e26f --- /dev/null +++ b/src/constants/blacklistConfig.js @@ -0,0 +1,385 @@ +const account = { + create: ['id', 'last_logged_in_at', 'has_accepted_terms_and_conditions', 'refresh_tokens', 'organization_id'], + login: [ + 'id', + 'email_verified', + 'name', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + logout: [ + 'id', + 'email', + 'email_verified', + 'name', + 'password', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + generateToken: [ + 'id', + 'email', + 'email_verified', + 'name', + 'password', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + generateOtp: [ + 'id', + 'email_verified', + 'name', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + registrationOtp: [ + 'id', + 'email_verified', + 'name', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + resetPassword: [ + 'id', + 'email_verified', + 'name', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + changeRole: [ + 'id', + 'email_verified', + 'name', + 'password', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'custom_entity_text', + 'meta', + ], +} +const admin = { + create: [ + 'id', + 'email_verified', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + login: [ + 'id', + 'email_verified', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + addOrgAdmin: [ + 'id', + 'email_verified', + 'gender', + 'location', + 'about', + 'password', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'roles', + 'custom_entity_text', + 'meta', + ], + deactivateUser: [ + 'email_verified', + 'gender', + 'location', + 'about', + 'password', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], +} +const entityType = { + create: [ + 'id', + 'status', + 'created_by', + 'updated_by', + 'organization_id', + 'parent_id', + 'allow_custom_entities', + 'has_entities', + ], + update: [ + 'id', + 'created_by', + 'updated_by', + 'allow_filtering', + 'organization_id', + 'parent_id', + 'allow_custom_entities', + 'has_entities', + ], +} + +const entity = { + create: ['id', 'status', 'type', 'created_by', 'updated_by'], + update: ['id', 'entity_type_id', 'created_by', 'updated_by'], +} + +const form = { + create: ['id', 'version', 'organization_id'], + update: ['id', 'version', 'organization_id'], +} + +const modules = { + create: ['id'], + update: [], +} + +const notification = { + create: ['id', 'created_by', 'updated_by'], + update: ['id', 'created_by', 'updated_by'], +} + +const orgAdmin = { + bulkUserCreate: [ + 'id', + 'email_verified', + 'name', + 'gender', + 'location', + 'about', + 'share_link', + 'status', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], + updateRequestStatus: [ + 'id', + 'email_verified', + 'name', + 'gender', + 'location', + 'about', + 'share_link', + 'image', + 'last_logged_in_at', + 'has_accepted_terms_and_conditions', + 'refresh_tokens', + 'languages', + 'preferred_language', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], +} + +const organization = { + create: [ + 'id', + 'description', + 'status', + 'org_admin', + 'parent_id', + 'related_orgs', + 'in_domain_visibility', + 'created_by', + 'updated_by', + ], + update: [ + 'id', + 'description', + 'status', + 'org_admin', + 'parent_id', + 'related_orgs', + 'in_domain_visibility', + 'created_by', + 'updated_by', + ], + requestOrgRole: [ + 'id', + 'description', + 'status', + 'org_admin', + 'parent_id', + 'related_orgs', + 'in_domain_visibility', + 'created_by', + 'updated_by', + ], +} + +const permissions = { + create: ['id'], + update: ['id'], +} + +const rolePermissionMapping = { + create: ['created_by'], + update: ['module', 'request_type', 'api_path', 'created_by'], +} + +const userRole = { + create: ['id', 'organization_id'], + update: ['id', 'organization_id'], +} + +const user = { + update: [ + 'id', + 'share_link', + 'last_logged_in_at', + 'refresh_tokens', + 'organization_id', + 'roles', + 'custom_entity_text', + 'meta', + ], +} + +module.exports = { + account, + admin, + entityType, + entity, + form, + modules, + notification, + orgAdmin, + organization, + permissions, + rolePermissionMapping, + userRole, + user, +} diff --git a/src/validators/common.js b/src/validators/common.js new file mode 100644 index 000000000..b4d539e4a --- /dev/null +++ b/src/validators/common.js @@ -0,0 +1,3 @@ +module.exports = function filterRequestBody(reqBody, blacklist) { + return Object.fromEntries(Object.entries(reqBody).filter(([key]) => !blacklist.includes(key))) +} diff --git a/src/validators/v1/account.js b/src/validators/v1/account.js index 449860c2d..3d9491666 100644 --- a/src/validators/v1/account.js +++ b/src/validators/v1/account.js @@ -5,8 +5,11 @@ * Description : Validations of accounts controller */ const common = require('@constants/common') +const filterRequestBody = require('../common') +const { account } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, account.create) req.checkBody('name') .trim() .notEmpty() @@ -33,6 +36,7 @@ module.exports = { }, login: (req) => { + req.body = filterRequestBody(req.body, account.login) req.checkBody('email') .trim() .notEmpty() @@ -45,24 +49,29 @@ module.exports = { }, logout: (req) => { + req.body = filterRequestBody(req.body, account.logout) req.checkBody('refresh_token').notEmpty().withMessage('refresh_token field is empty') }, generateToken: (req) => { + req.body = filterRequestBody(req.body, account.generateToken) req.checkBody('refresh_token').notEmpty().withMessage('refresh_token field is empty') }, generateOtp: (req) => { + req.body = filterRequestBody(req.body, account.generateOtp) req.checkBody('email').notEmpty().withMessage('email field is empty').isEmail().withMessage('email is invalid') req.checkBody('password').trim().notEmpty().withMessage('password field is empty') }, registrationOtp: (req) => { + req.body = filterRequestBody(req.body, account.registrationOtp) req.checkBody('email').notEmpty().withMessage('email field is empty').isEmail().withMessage('email is invalid') req.checkBody('name').notEmpty().withMessage('name field is empty') }, resetPassword: (req) => { + req.body = filterRequestBody(req.body, account.resetPassword) req.checkBody('email').notEmpty().withMessage('email field is empty').isEmail().withMessage('email is invalid') req.checkBody('password').notEmpty().withMessage('password field is empty') @@ -76,6 +85,7 @@ module.exports = { }, changeRole: (req) => { + req.body = filterRequestBody(req.body, account.changeRole) req.checkBody('email').notEmpty().withMessage('email field is empty').isEmail().withMessage('email is invalid') req.checkBody('role').notEmpty().withMessage('role field is empty') }, @@ -83,4 +93,26 @@ module.exports = { listUser: (req) => { req.checkQuery('type').notEmpty().withMessage('type can not be null').isString() }, + + search: (req) => { + req.checkQuery('type') + .notEmpty() + .withMessage('type can not be null') + .isString() + .notIn([common.ADMIN_ROLE, common.MENTEE_ROLE, common.MENTEE_ROLE, common.ORG_ADMIN_ROLE]) + .withMessage('Invalid type value') + req.checkQuery('organization_id').isNumeric().withMessage('organization_id must be an Id') + req.checkBody('user_ids') + .isArray() + .withMessage('user_ids must be an array') + .custom((value) => { + // Check if all elements in the array are integers + for (const id of value) { + if (!Number.isInteger(id)) { + throw new Error('All elements in user_ids must be integers') + } + } + return true + }) + }, } diff --git a/src/validators/v1/admin.js b/src/validators/v1/admin.js index 575be9190..e5aaa41d9 100644 --- a/src/validators/v1/admin.js +++ b/src/validators/v1/admin.js @@ -5,12 +5,15 @@ * Description : Validations of admin controller */ +const filterRequestBody = require('../common') +const { admin } = require('@constants/blacklistConfig') module.exports = { deleteUser: (req) => { req.checkParams('id').notEmpty().withMessage('id param is empty') }, create: (req) => { + req.body = filterRequestBody(req.body, admin.create) req.checkBody('secret_code').trim().notEmpty().withMessage('secret_code field is empty') req.checkBody('name') .trim() @@ -31,6 +34,7 @@ module.exports = { }, login: (req) => { + req.body = filterRequestBody(req.body, admin.login) req.checkBody('email') .trim() .notEmpty() @@ -43,6 +47,7 @@ module.exports = { }, addOrgAdmin: (req) => { + req.body = filterRequestBody(req.body, admin.addOrgAdmin) req.checkBody('organization_id').notEmpty().withMessage('organization_id field is empty') req.checkBody(['user_id', 'email']).custom(() => { @@ -65,6 +70,7 @@ module.exports = { }, deactivateUser: (req) => { + req.body = filterRequestBody(req.body, admin.deactivateUser) const field = req.body.email ? 'email' : req.body.id ? 'id' : null if (field) { req.checkBody(field).isArray().notEmpty().withMessage(` ${field} must be an array and should not be empty.`) diff --git a/src/validators/v1/entity-type.js b/src/validators/v1/entity-type.js index fca56b740..d39584a29 100644 --- a/src/validators/v1/entity-type.js +++ b/src/validators/v1/entity-type.js @@ -4,9 +4,11 @@ * Date : 04-Nov-2021 * Description : Validations of user entities controller */ - +const filterRequestBody = require('../common') +const { entityType } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, entityType.create) req.checkBody('value') .trim() .notEmpty() @@ -41,6 +43,7 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, entityType.update) req.checkParams('id').notEmpty().withMessage('id param is empty') req.checkBody('value') diff --git a/src/validators/v1/entity.js b/src/validators/v1/entity.js index ddaf66a65..6db5c7099 100644 --- a/src/validators/v1/entity.js +++ b/src/validators/v1/entity.js @@ -4,9 +4,12 @@ * Date : 04-Nov-2021 * Description : Validations of user entities controller */ +const filterRequestBody = require('../common') +const { entity } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, entity.create) req.checkBody('value') .trim() .notEmpty() @@ -29,6 +32,7 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, entity.update) req.checkParams('id').notEmpty().withMessage('id param is empty') req.checkBody('value') diff --git a/src/validators/v1/form.js b/src/validators/v1/form.js index 93bde96b5..6a1af4c67 100644 --- a/src/validators/v1/form.js +++ b/src/validators/v1/form.js @@ -4,9 +4,12 @@ * Date : 03-Nov-2021 * Description : Validations of forms controller */ +const filterRequestBody = require('../common') +const { form } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, form.create) req.checkBody('type') .trim() .notEmpty() @@ -34,6 +37,7 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, form.update) req.checkBody('type') .notEmpty() .withMessage('type field is empty') diff --git a/src/validators/v1/modules.js b/src/validators/v1/modules.js index 35e3f2fca..8f0394643 100644 --- a/src/validators/v1/modules.js +++ b/src/validators/v1/modules.js @@ -1,5 +1,8 @@ +const filterRequestBody = require('../common') +const { modules } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, modules.create) req.checkBody('code') .trim() .notEmpty() @@ -16,6 +19,7 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, modules.update) req.checkParams('id').notEmpty().withMessage('id param is empty') req.checkBody('code') diff --git a/src/validators/v1/notification.js b/src/validators/v1/notification.js index 265c3e27f..9cef644a9 100644 --- a/src/validators/v1/notification.js +++ b/src/validators/v1/notification.js @@ -4,9 +4,11 @@ * Date : 03-Nov-2023 * Description : Validations of notification controller */ - +const filterRequestBody = require('../common') +const { notification } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, notification.create) req.checkBody('type').trim().notEmpty().withMessage('type field is empty') req.checkBody('code').trim().notEmpty().withMessage('code field is empty') @@ -17,6 +19,7 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, notification.update) req.checkBody('type').trim().notEmpty().withMessage('type field is empty') req.checkBody('code').trim().notEmpty().withMessage('code field is empty') @@ -27,7 +30,6 @@ module.exports = { }, read: (req) => { - console.log(req.params.id, req.query.code, 'kjkkkkkkk') if (req.params.id || req.query.code) { if (req.params.id) { req.checkParams('id').notEmpty().withMessage('id param is empty') diff --git a/src/validators/v1/org-admin.js b/src/validators/v1/org-admin.js index c31f4bdbf..a8bcd1dac 100644 --- a/src/validators/v1/org-admin.js +++ b/src/validators/v1/org-admin.js @@ -4,15 +4,19 @@ * Date : 19-Jun-2023 * Description : Validations of admin controller */ +const filterRequestBody = require('../common') +const { orgAdmin } = require('@constants/blacklistConfig') module.exports = { bulkUserCreate: (req) => { + req.body = filterRequestBody(req.body, orgAdmin.bulkUserCreate) req.checkBody('file_path').notEmpty().withMessage('file_path field is empty') }, getRequestDetails: (req) => { req.checkParams('id').notEmpty().withMessage('id param is empty') }, updateRequestStatus: (req) => { + req.body = filterRequestBody(req.body, orgAdmin.updateRequestStatus) req.checkBody('request_id').notEmpty().withMessage('request_id field is empty') req.checkBody('status').notEmpty().withMessage('status field is empty') }, diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index 0c2a8b8e0..be2cacd1d 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -4,9 +4,11 @@ * Date : 25-July-2023 * Description : Validations of Organization controller */ - +const filterRequestBody = require('../common') +const { organization } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, organization.create) req.checkBody('code').trim().notEmpty().withMessage('code field is empty') req.checkBody('name') .trim() @@ -20,10 +22,12 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, organization.update) req.checkParams('id').notEmpty().withMessage('id param is empty') }, requestOrgRole: (req) => { + req.body = filterRequestBody(req.body, organization.requestOrgRole) req.checkBody('role').notEmpty().withMessage('role field is empty') req.checkBody('form_data').notEmpty().withMessage('form_data field is empty') }, diff --git a/src/validators/v1/permissions.js b/src/validators/v1/permissions.js index 4f7d6d510..caba25a77 100644 --- a/src/validators/v1/permissions.js +++ b/src/validators/v1/permissions.js @@ -1,5 +1,6 @@ const Permission = require('@database/models/index').Permission - +const filterRequestBody = require('../common') +const { permissions } = require('@constants/blacklistConfig') async function isUniqueCode(value) { const existingRecord = await Permission.findOne({ where: { code: value } }) if (existingRecord) { @@ -10,6 +11,7 @@ async function isUniqueCode(value) { module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, permissions.create) req.checkBody('code') .trim() .notEmpty() @@ -48,6 +50,7 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, permissions.update) req.checkParams('id').notEmpty().withMessage('id param is empty') req.checkBody('code') diff --git a/src/validators/v1/role-permission-mapping.js b/src/validators/v1/role-permission-mapping.js index 043982d1c..c10f4a10b 100644 --- a/src/validators/v1/role-permission-mapping.js +++ b/src/validators/v1/role-permission-mapping.js @@ -1,5 +1,8 @@ +const filterRequestBody = require('../common') +const { rolePermissionMapping } = require('@constants/blacklistConfig') module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, rolePermissionMapping.create) req.checkBody('permission_id') .trim() .notEmpty() @@ -15,6 +18,7 @@ module.exports = { }, delete: (req) => { + req.body = filterRequestBody(req.body, rolePermissionMapping.delete) req.checkBody('permission_id') .trim() .notEmpty() diff --git a/src/validators/v1/user-role.js b/src/validators/v1/user-role.js index db654cb72..6efaa4a33 100644 --- a/src/validators/v1/user-role.js +++ b/src/validators/v1/user-role.js @@ -1,3 +1,5 @@ +const filterRequestBody = require('../common') +const { userRole } = require('@constants/blacklistConfig') const validateList = (req, allowedVariables) => { allowedVariables.forEach((variable) => { req.checkQuery(variable) @@ -9,6 +11,7 @@ const validateList = (req, allowedVariables) => { module.exports = { create: (req) => { + req.body = filterRequestBody(req.body, userRole.create) req.checkBody('title') .trim() .notEmpty() @@ -40,6 +43,7 @@ module.exports = { }, update: (req) => { + req.body = filterRequestBody(req.body, userRole.update) req.checkParams('id').notEmpty().withMessage('id param is empty') req.checkBody('title') diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js index 312b81d7b..d0734ed71 100644 --- a/src/validators/v1/user.js +++ b/src/validators/v1/user.js @@ -4,9 +4,11 @@ * Date : 17-July-2023 * Description : Validations of user controller */ - +const filterRequestBody = require('../common') +const { user } = require('@constants/blacklistConfig') module.exports = { update: (req) => { + req.body = filterRequestBody(req.body, user.update) if (req.body.preferred_language) { req.checkBody('preferred_language') .trim() From 5817165679a96671c4b515f8b55fc5f688ddfdae Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Wed, 20 Mar 2024 16:21:17 +0530 Subject: [PATCH 17/67] Comment Changes regarding the password --- src/envVariables.js | 11 +++++++++++ src/validators/v1/account.js | 14 +++++--------- src/validators/v1/admin.js | 8 +++----- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/envVariables.js b/src/envVariables.js index b61ff67ae..0a9a29855 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -237,6 +237,17 @@ let enviromentVariables = { optional: true, default: '*', }, + PASSWORD_POLICY_REGEX: { + message: 'Required password policy', + optional: true, + default: '/^(?=.*[A-Z])(?=.*d)(?=.*[!@#$%^&*()_+{}|:<>?~`-=[];,./])[^ ]{11,}$/', + }, + PASSWORD_POLICY_MESSAGE: { + message: 'Required password policy message', + optional: true, + default: + 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long', + }, } let success = true diff --git a/src/validators/v1/account.js b/src/validators/v1/account.js index 260c05c98..aa906017c 100644 --- a/src/validators/v1/account.js +++ b/src/validators/v1/account.js @@ -25,13 +25,11 @@ module.exports = { .withMessage('email is invalid') .normalizeEmail({ gmail_remove_dots: false }) - req.checkBody('password') + req.checkBody('password') .notEmpty() .withMessage('Password field is empty') - .matches(/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+{}|:"<>?~`\-=[\];',.\/])[^ ]{10,}$/) - .withMessage( - 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long' - ) + .matches(process.env.PASSWORD_POLICY_REGEX) + .withMessage(process.env.PASSWORD_POLICY_MESSAGE) .custom((value) => !/\s/.test(value)) .withMessage('Password cannot contain spaces') @@ -75,10 +73,8 @@ module.exports = { req.checkBody('password') .notEmpty() .withMessage('Password field is empty') - .matches(/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+{}|:"<>?~`\-=[\];',.\/])[^ ]{10,}$/) - .withMessage( - 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long' - ) + .matches(process.env.PASSWORD_POLICY_REGEX) + .withMessage(process.env.PASSWORD_POLICY_MESSAGE) .custom((value) => !/\s/.test(value)) .withMessage('Password cannot contain spaces') diff --git a/src/validators/v1/admin.js b/src/validators/v1/admin.js index 75ffdbc89..1877d9f36 100644 --- a/src/validators/v1/admin.js +++ b/src/validators/v1/admin.js @@ -27,13 +27,11 @@ module.exports = { .withMessage('email is invalid') .normalizeEmail() - req.checkBody('password') + req.checkBody('password') .notEmpty() .withMessage('Password field is empty') - .matches(/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+{}|:"<>?~`\-=[\];',.\/])[^ ]{10,}$/) - .withMessage( - 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long' - ) + .matches(process.env.PASSWORD_POLICY_REGEX) + .withMessage(process.env.PASSWORD_POLICY_MESSAGE) .custom((value) => !/\s/.test(value)) .withMessage('Password cannot contain spaces') }, From e78676718bf37830bfa6351b79e444780235eb60 Mon Sep 17 00:00:00 2001 From: Nevil Date: Thu, 21 Mar 2024 10:28:14 +0530 Subject: [PATCH 18/67] updated validators --- src/constants/blacklistConfig.js | 38 +++++-------------------------- src/validators/v1/entity-type.js | 7 ++---- src/validators/v1/entity.js | 26 ++++++++++++++++----- src/validators/v1/organization.js | 22 +++++++++++++++++- src/validators/v1/user.js | 14 ++++++++++-- 5 files changed, 61 insertions(+), 46 deletions(-) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index 72c39e26f..a212cd6e3 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -220,26 +220,8 @@ const admin = { ], } const entityType = { - create: [ - 'id', - 'status', - 'created_by', - 'updated_by', - 'organization_id', - 'parent_id', - 'allow_custom_entities', - 'has_entities', - ], - update: [ - 'id', - 'created_by', - 'updated_by', - 'allow_filtering', - 'organization_id', - 'parent_id', - 'allow_custom_entities', - 'has_entities', - ], + create: ['id', 'status', 'created_by', 'updated_by', 'organization_id', 'parent_id', 'allow_filtering'], + update: ['id', 'created_by', 'updated_by', 'allow_filtering', 'organization_id', 'parent_id'], } const entity = { @@ -307,18 +289,6 @@ const orgAdmin = { const organization = { create: [ 'id', - 'description', - 'status', - 'org_admin', - 'parent_id', - 'related_orgs', - 'in_domain_visibility', - 'created_by', - 'updated_by', - ], - update: [ - 'id', - 'description', 'status', 'org_admin', 'parent_id', @@ -327,6 +297,7 @@ const organization = { 'created_by', 'updated_by', ], + update: ['id', 'org_admin', 'parent_id', 'related_orgs', 'in_domain_visibility', 'created_by', 'updated_by'], requestOrgRole: [ 'id', 'description', @@ -365,6 +336,9 @@ const user = { 'roles', 'custom_entity_text', 'meta', + 'email', + 'email_verified', + 'password', ], } diff --git a/src/validators/v1/entity-type.js b/src/validators/v1/entity-type.js index d39584a29..ce8bc7ebd 100644 --- a/src/validators/v1/entity-type.js +++ b/src/validators/v1/entity-type.js @@ -27,7 +27,7 @@ module.exports = { .trim() .notEmpty() .withMessage('data_type field is empty') - .matches(/^[A-Za-z]+$/) + .matches(/^[A-Za-z\[\]]+$/) .withMessage('data_type is invalid, must not contain spaces') req.checkBody('model_names') @@ -61,14 +61,11 @@ module.exports = { .matches(/^[A-Z]+$/) .withMessage('status is invalid, must be in all caps') - req.checkBody('deleted').optional().isBoolean().withMessage('deleted is invalid') - req.checkBody('allow_filtering').optional().isEmpty().withMessage('allow_filtering is not allowed in create') - req.checkBody('data_type') .trim() .notEmpty() .withMessage('data_type field is empty') - .matches(/^[A-Za-z]+$/) + .matches(/^[A-Za-z\[\]]+$/) .withMessage('data_type is invalid, must not contain spaces') req.checkBody('model_names') diff --git a/src/validators/v1/entity.js b/src/validators/v1/entity.js index 6db5c7099..13dc7850f 100644 --- a/src/validators/v1/entity.js +++ b/src/validators/v1/entity.js @@ -29,6 +29,18 @@ module.exports = { .withMessage('entity_type_id field is empty') .isNumeric() .withMessage('entity_type_id is invalid, must be numeric') + + req.checkBody('status') + .optional() + .notEmpty() + .withMessage('status field is empty') + .matches(/^[A-Z]+$/) + .withMessage('status is invalid, must be in all caps and no spaces') + req.checkBody('type') + .notEmpty() + .withMessage('type field is empty') + .matches(/^[A-Z]+$/) + .withMessage('type is invalid, must be in all caps and no spaces') }, update: (req) => { @@ -47,15 +59,17 @@ module.exports = { req.checkBody('status') .optional() + .optional() + .notEmpty() + .withMessage('status field is empty') .matches(/^[A-Z]+$/) - .withMessage('status is invalid, must be in all caps') - - req.checkBody('deleted').optional().isBoolean().withMessage('deleted is invalid') - + .withMessage('status is invalid, must be in all caps and no spaces') req.checkBody('type') .optional() - .matches(/^[A-Za-z]+$/) - .withMessage('type is invalid, must not contain spaces') + .notEmpty() + .withMessage('type field is empty') + .matches(/^[A-Z]+$/) + .withMessage('type is invalid, must be in all caps and no spaces') }, read: (req) => { diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index be2cacd1d..58651f2f2 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -17,13 +17,33 @@ module.exports = { .matches(/^[A-Za-z ]+$/) .withMessage('name is invalid') - req.checkBody('description').trim().notEmpty().withMessage('description field is empty') + req.checkBody('description') + .trim() + .notEmpty() + .withMessage('description field is empty') + .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .withMessage('invalid description') req.checkBody('domains').trim().notEmpty().withMessage('domains field is empty') }, update: (req) => { req.body = filterRequestBody(req.body, organization.update) req.checkParams('id').notEmpty().withMessage('id param is empty') + req.checkBody('name') + .optional() + .trim() + .notEmpty() + .withMessage('name field is empty') + .matches(/^[A-Za-z ]+$/) + .withMessage('name is invalid') + + req.checkBody('description') + .optional() + .trim() + .notEmpty() + .withMessage('description field is empty') + .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .withMessage('invalid description') }, requestOrgRole: (req) => { diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js index d0734ed71..40feb3f7e 100644 --- a/src/validators/v1/user.js +++ b/src/validators/v1/user.js @@ -21,7 +21,12 @@ module.exports = { .isIn(['MALE', 'FEMALE', 'OTHER']) .withMessage('gender is invalid, must be either MALE, FEMALE or OTHER') - req.checkBody('name').trim().notEmpty().withMessage('name field is empty') + req.checkBody('name') + .trim() + .notEmpty() + .withMessage('name field is empty') + .matches(/^[A-Za-z ]+$/) + .withMessage('This field can only contain alphabets') req.checkBody('location') .notEmpty() @@ -29,7 +34,12 @@ module.exports = { .isString() .withMessage('location is invalid') - req.checkBody('about').trim().notEmpty().withMessage('about field is empty') + req.checkBody('about') + .trim() + .notEmpty() + .withMessage('about field is empty') + .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .withMessage('invalid about') req.checkBody('has_accepted_terms_and_conditions') .optional() From cf8509226bf48372fdb9ec470d471fdff9fdd984 Mon Sep 17 00:00:00 2001 From: Nevil Date: Thu, 21 Mar 2024 10:39:25 +0530 Subject: [PATCH 19/67] updated blackList --- src/constants/blacklistConfig.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index a212cd6e3..a5ee04a55 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -1,5 +1,5 @@ const account = { - create: ['id', 'last_logged_in_at', 'has_accepted_terms_and_conditions', 'refresh_tokens', 'organization_id'], + create: ['id', 'last_logged_in_at', 'refresh_tokens', 'organization_id'], login: [ 'id', 'email_verified', @@ -152,7 +152,6 @@ const admin = { 'status', 'image', 'last_logged_in_at', - 'has_accepted_terms_and_conditions', 'refresh_tokens', 'languages', 'preferred_language', From a607cb4f79ede831e6e78649fe4660b8346ae8a4 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Thu, 21 Mar 2024 12:16:20 +0530 Subject: [PATCH 20/67] user update blacklist updated --- src/constants/blacklistConfig.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index 72c39e26f..b48a1dcae 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -358,11 +358,18 @@ const userRole = { const user = { update: [ 'id', + 'email', + 'email_verified', + 'password', + 'about', 'share_link', + 'status', 'last_logged_in_at', + 'has_accepted_terms_and_conditions', 'refresh_tokens', + 'languages', + 'preferred_language', 'organization_id', - 'roles', 'custom_entity_text', 'meta', ], From c633acb182abfa7bf84ad19e7545f61a34c82011 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Thu, 21 Mar 2024 12:24:41 +0530 Subject: [PATCH 21/67] user update blacklist updated - Added roles --- src/constants/blacklistConfig.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index b48a1dcae..f9bad8bcc 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -370,6 +370,7 @@ const user = { 'languages', 'preferred_language', 'organization_id', + 'roles', 'custom_entity_text', 'meta', ], From 88c6a8930163bf8fe3ba8b6049d95f2ccf564747 Mon Sep 17 00:00:00 2001 From: Nevil Date: Thu, 21 Mar 2024 12:30:51 +0530 Subject: [PATCH 22/67] updated blacklist --- src/constants/blacklistConfig.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index a5ee04a55..0830cf6b9 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -338,6 +338,7 @@ const user = { 'email', 'email_verified', 'password', + 'has_accepted_terms_and_conditions', ], } From 182188c6b24aef06b52526d4c0106388d6d6b836 Mon Sep 17 00:00:00 2001 From: Nevil Date: Thu, 21 Mar 2024 18:28:30 +0530 Subject: [PATCH 23/67] added cloud-services to valid list --- src/routes/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/index.js b/src/routes/index.js index 55d2dd2a6..b73743c62 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -89,7 +89,9 @@ module.exports = (app) => { error.statusCode = 400 throw error } + // Validate controller + allowedControllers.push('cloud-services') if (!allowedControllers.includes(controllerName)) { const error = new Error('Invalid controller.') error.statusCode = 400 From e180323dfcf503fd11366f8b49342e6da505113c Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Fri, 22 Mar 2024 12:49:16 +0530 Subject: [PATCH 24/67] user update blacklist updated - changes --- src/constants/blacklistConfig.js | 1 - src/validators/v1/organization.js | 2 ++ src/validators/v1/user.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index 19a3f876d..cf4038180 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -84,7 +84,6 @@ const account = { registrationOtp: [ 'id', 'email_verified', - 'name', 'gender', 'location', 'about', diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index 58651f2f2..9a0bcf838 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -21,6 +21,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') + .not() .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) .withMessage('invalid description') req.checkBody('domains').trim().notEmpty().withMessage('domains field is empty') @@ -42,6 +43,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') + .not() .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) .withMessage('invalid description') }, diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js index 40feb3f7e..a0ceb2c2f 100644 --- a/src/validators/v1/user.js +++ b/src/validators/v1/user.js @@ -38,6 +38,7 @@ module.exports = { .trim() .notEmpty() .withMessage('about field is empty') + .not() .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) .withMessage('invalid about') From c7241baa106c640cf774c33602f43b4abdf9c6b9 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Fri, 22 Mar 2024 12:53:46 +0530 Subject: [PATCH 25/67] user update blacklist updated - changes --- src/constants/blacklistConfig.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index cf4038180..f6e21fc1f 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -327,17 +327,9 @@ const userRole = { const user = { update: [ 'id', - 'email', - 'email_verified', - 'password', - 'about', 'share_link', - 'status', 'last_logged_in_at', - 'has_accepted_terms_and_conditions', 'refresh_tokens', - 'languages', - 'preferred_language', 'organization_id', 'roles', 'custom_entity_text', From 353b1bb52b94f6fd69cea547a19393246a0bf39d Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Fri, 22 Mar 2024 14:35:28 +0530 Subject: [PATCH 26/67] Default variable changes for regex in env --- src/envVariables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/envVariables.js b/src/envVariables.js index 0a9a29855..a335ee039 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -240,7 +240,7 @@ let enviromentVariables = { PASSWORD_POLICY_REGEX: { message: 'Required password policy', optional: true, - default: '/^(?=.*[A-Z])(?=.*d)(?=.*[!@#$%^&*()_+{}|:<>?~`-=[];,./])[^ ]{11,}$/', + default: '^(?=.*[A-Z])(?=.*d)(?=.*[!@$%^&*()_+{}|:<>?~`-=[],./])[^ ]{11,}$', }, PASSWORD_POLICY_MESSAGE: { message: 'Required password policy message', From 03de8c715b64f9b86300153ac316268d9535a2ed Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 22 Mar 2024 16:29:51 +0530 Subject: [PATCH 27/67] bulk upload internal process failure fix --- src/services/userInvite.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/services/userInvite.js b/src/services/userInvite.js index bd6cb052d..d74c240f2 100644 --- a/src/services/userInvite.js +++ b/src/services/userInvite.js @@ -50,8 +50,8 @@ module.exports = class UserInviteHelper { // upload output file to cloud const uploadRes = await this.uploadFileToCloud(outputFilename, inviteeFileDir, data.user.id) - const output_path = uploadRes.result.uploadDest + const output_path = uploadRes.result.uploadDest const update = { output_path, updated_by: data.user.id, @@ -103,13 +103,17 @@ module.exports = class UserInviteHelper { static async downloadCSV(filePath) { try { const downloadableUrl = await utils.getDownloadableUrl(filePath) - const fileName = path.basename(downloadableUrl) - const downloadPath = path.join(inviteeFileDir, fileName) + let fileName = path.basename(downloadableUrl) + // Find the index of the first occurrence of '?' + const index = fileName.indexOf('?') + // Extract the portion of the string before the '?' if it exists, otherwise use the entire string + fileName = index !== -1 ? fileName.substring(0, index) : fileName + + const downloadPath = path.join(inviteeFileDir, fileName) const response = await axios.get(downloadableUrl, { responseType: common.responseType, }) - const writeStream = fs.createWriteStream(downloadPath) response.data.pipe(writeStream) From 655b9352cda17abab3ad32e688172cbbe7108609 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Fri, 22 Mar 2024 16:58:26 +0530 Subject: [PATCH 28/67] envVariables chnages --- src/envVariables.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/envVariables.js b/src/envVariables.js index f86629912..a8a3ba38a 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -244,14 +244,14 @@ let enviromentVariables = { PASSWORD_POLICY_REGEX: { message: 'Required password policy', optional: true, - default: '^(?=.*[A-Z])(?=.*d)(?=.*[!@$%^&*()_+{}|:<>?~`-=[],./])[^ ]{11,}$', + default: '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{10,}$', }, PASSWORD_POLICY_MESSAGE: { message: 'Required password policy message', optional: true, default: 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long', - }, + }, DOWNLOAD_URL_EXPIRATION_DURATION: { message: 'Required downloadable url expiration time', optional: true, From d23c25bf1ae6c84a14a1293e9a8b42d089f13a24 Mon Sep 17 00:00:00 2001 From: Nevil Date: Fri, 22 Mar 2024 19:40:55 +0530 Subject: [PATCH 29/67] fixed an issue with refresh token update --- src/services/account.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/account.js b/src/services/account.js index 3c77fa6f6..7c46c6c3f 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -279,7 +279,7 @@ module.exports = class AccountHelper { last_logged_in_at: new Date().getTime(), } - await userQueries.updateUser({ id: user.id, organization_id: userCredentials.organization_id }, update) + await userQueries.updateUser({ id: user.id, organization_id: user.organization_id }, update) await utilsHelper.redisDel(encryptedEmailId) //make the user as org admin From c8f09627cb881983a263300f0757e5d4608c50f3 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 26 Mar 2024 13:52:32 +0530 Subject: [PATCH 30/67] Change Password API --- .../MentorED-Users.postman_collection.json | 47 ++++++ src/controllers/v1/account.js | 22 +++ ...07-change-password-email--template-data.js | 36 +++++ ...40326060546-change-password-permissions.js | 41 +++++ ...060601-change-password-role-permissions.js | 96 ++++++++++++ src/envVariables.js | 5 +- src/services/account.js | 143 ++++++++++++++++++ 7 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 src/database/migrations/20240325113407-change-password-email--template-data.js create mode 100644 src/database/migrations/20240326060546-change-password-permissions.js create mode 100644 src/database/migrations/20240326060601-change-password-role-permissions.js diff --git a/src/api-doc/MentorED-Users.postman_collection.json b/src/api-doc/MentorED-Users.postman_collection.json index 86505b02f..089183e0e 100644 --- a/src/api-doc/MentorED-Users.postman_collection.json +++ b/src/api-doc/MentorED-Users.postman_collection.json @@ -450,6 +450,53 @@ } ] }, + + { + "name": "Change Password", + "request": { + "method": "POST", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "oldPassword", + "value": "password", + "type": "text" + }, + { + "key": "newPassword", + "value": "Password@123", + "type": "text" + } + ] + }, + "url": { + "raw": "{{UserDevBaseUrl}}user/v1/account/changePassword", + "host": ["{{UserDevBaseUrl}}user"], + "path": ["v1", "account", "changePassword"], + "query": [ + { + "key": "oldPassword", + "value": "password", + "disabled": true + }, + { + "key": "newPassword", + "value": "Password@123", + "disabled": true + } + ] + } + }, + "response": [] + }, { "name": "User", "item": [ diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 9ac9c015a..6202a5cc0 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -244,4 +244,26 @@ module.exports = class Account { return error } } + + /** + * change password + * @method + * @name changePassword + * @param {Object} req -request data. + * @param {Object} req.decodedToken.id - UserId. + * @param {string} req.body - request body contains user password + * @param {string} req.body.OldPassword - user Old Password. + * @param {string} req.body.NewPassword - user New Password. + * @param {string} req.body.ConfirmNewPassword - user Confirming New Password. + * @returns {JSON} - password changed response + */ + + async changePassword(req) { + try { + const result = await accountService.changePassword(req.body, req.decodedToken.id, req.decodedToken.name) + return result + } catch (error) { + return error + } + } } diff --git a/src/database/migrations/20240325113407-change-password-email--template-data.js b/src/database/migrations/20240325113407-change-password-email--template-data.js new file mode 100644 index 000000000..8371e564f --- /dev/null +++ b/src/database/migrations/20240325113407-change-password-email--template-data.js @@ -0,0 +1,36 @@ +const moment = require('moment') + +module.exports = { + up: async (queryInterface, Sequelize) => { + const defaultOrgId = queryInterface.sequelize.options.defaultOrgId + if (!defaultOrgId) { + throw new Error('Default org ID is undefined. Please make sure it is set in sequelize options.') + } + const emailTemplates = [ + { + code: 'change_password', + subject: 'Password Change', + body: '

Dear {name},

Your password has been changed successfully.', + }, + ] + + let notificationTemplateData = [] + emailTemplates.forEach(async function (emailTemplate) { + emailTemplate['status'] = 'ACTIVE' + emailTemplate['type'] = 'email' + emailTemplate['updated_at'] = moment().format() + emailTemplate['created_at'] = moment().format() + emailTemplate['organization_id'] = defaultOrgId + emailTemplate['email_footer'] = 'email_footer' + emailTemplate['email_header'] = 'email_header' + + notificationTemplateData.push(emailTemplate) + }) + + await queryInterface.bulkInsert('notification_templates', notificationTemplateData, {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('notification_templates', null, {}) + }, +} diff --git a/src/database/migrations/20240326060546-change-password-permissions.js b/src/database/migrations/20240326060546-change-password-permissions.js new file mode 100644 index 000000000..7ba2f6292 --- /dev/null +++ b/src/database/migrations/20240326060546-change-password-permissions.js @@ -0,0 +1,41 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + try { + const permissionsData = [ + { + code: 'change_password_update', + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + status: 'ACTIVE', + }, + ] + + // Batch insert permissions + await queryInterface.bulkInsert( + 'permissions', + permissionsData.map((permission) => ({ + ...permission, + created_at: new Date(), + updated_at: new Date(), + })) + ) + } catch (error) { + console.error('Error in migration:', error) + throw error + } + }, + + async down(queryInterface, Sequelize) { + try { + // Rollback migration by deleting all permissions + await queryInterface.bulkDelete('permissions', null, {}) + } catch (error) { + console.error('Error in rollback migration:', error) + throw error + } + }, +} diff --git a/src/database/migrations/20240326060601-change-password-role-permissions.js b/src/database/migrations/20240326060601-change-password-role-permissions.js new file mode 100644 index 000000000..f2e018b8c --- /dev/null +++ b/src/database/migrations/20240326060601-change-password-role-permissions.js @@ -0,0 +1,96 @@ +'use strict' + +require('module-alias/register') +require('dotenv').config() +const common = require('@constants/common') +const Permissions = require('@database/models/index').Permission + +const getPermissionId = async (module, request_type, api_path) => { + try { + const permission = await Permissions.findOne({ + where: { module, request_type, api_path }, + }) + if (!permission) { + throw new Error( + `Permission not found for module: ${module}, request_type: ${request_type}, api_path: ${api_path}` + ) + } + return permission.id + } catch (error) { + throw new Error(`Error while fetching permission: ${error.message}`) + } +} + +module.exports = { + up: async (queryInterface, Sequelize) => { + try { + const rolePermissionsData = await Promise.all([ + { + role_title: common.MENTOR_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.MENTEE_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.ORG_ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.USER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.SESSION_MANAGER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + ]) + + await queryInterface.bulkInsert( + 'role_permission_mapping', + rolePermissionsData.map((data) => ({ + ...data, + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + })) + ) + } catch (error) { + console.log(error) + console.error(`Migration error: ${error.message}`) + throw error + } + }, + + down: async (queryInterface, Sequelize) => { + try { + await queryInterface.bulkDelete('role_permission_mapping', null, {}) + } catch (error) { + console.error(`Rollback migration error: ${error.message}`) + throw error + } + }, +} diff --git a/src/envVariables.js b/src/envVariables.js index a8a3ba38a..941d068eb 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -244,13 +244,12 @@ let enviromentVariables = { PASSWORD_POLICY_REGEX: { message: 'Required password policy', optional: true, - default: '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{10,}$', + default: '^.{8,}$', }, PASSWORD_POLICY_MESSAGE: { message: 'Required password policy message', optional: true, - default: - 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long', + default: 'Password must have at least 8 characters long', }, DOWNLOAD_URL_EXPIRATION_DURATION: { message: 'Required downloadable url expiration time', diff --git a/src/services/account.js b/src/services/account.js index 3c77fa6f6..7f1fcd7b4 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1253,4 +1253,147 @@ module.exports = class AccountHelper { throw error } } + + /** + * change password + * @method + * @name changePassword + * @param {Object} req -request data. + * @param {Object} req.decodedToken.id - UserId. + * @param {string} req.body - request body contains user password + * @param {string} req.body.OldPassword - user Old Password. + * @param {string} req.body.NewPassword - user New Password. + * @param {string} req.body.ConfirmNewPassword - user Confirming New Password. + * @returns {JSON} - password changed response + */ + + static async changePassword(bodyData, userId, userName) { + const projection = ['location'] + try { + const userCredentials = await UserCredentialQueries.findOne({ user_id: userId }) + const plaintextEmailId = emailEncryption.decrypt(userCredentials.email) + if (!userCredentials) { + return responses.failureResponse({ + message: 'USER_DOESNOT_EXISTS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + let user = await userQueries.findOne( + { id: userCredentials.user_id, organization_id: userCredentials.organization_id }, + { + attributes: { + exclude: projection, + }, + } + ) + if (!user) { + return responses.failureResponse({ + message: 'USER_DOESNOT_EXISTS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + let roles = await roleQueries.findAll({ id: user.roles, status: common.ACTIVE_STATUS }) + if (!roles) { + return responses.failureResponse({ + message: 'ROLE_NOT_FOUND', + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + }) + } + user.user_roles = roles + + const verifyOldPassword = utilsHelper.comparePassword(bodyData.oldPassword, userCredentials.password) + if (!verifyOldPassword) { + return responses.failureResponse({ + message: 'INCORRECT_OLD_PASSWORD', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + const isPasswordSame = bcryptJs.compareSync(bodyData.newPassword, userCredentials.password) + if (isPasswordSame) { + return responses.failureResponse({ + message: 'INCORRECT_OLD_PASSWORD', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) + + const updateParams = { + lastLoggedInAt: new Date().getTime(), + password: bodyData.newPassword, + } + + await userQueries.updateUser( + { id: user.id, organization_id: userCredentials.organization_id }, + updateParams + ) + await UserCredentialQueries.updateUser( + { + email: userCredentials.email, + }, + { password: bodyData.newPassword } + ) + await utilsHelper.redisDel(userCredentials.email) + + delete user.newPassword + + const templateData = await notificationTemplateQueries.findOneEmailTemplate( + process.env.CHANGE_PASSWORD_TEMPLATE_CODE + ) + + if (templateData) { + // Push successful registration email to kafka + const payload = { + type: common.notificationEmailType, + email: { + to: plaintextEmailId, + subject: templateData.subject, + body: utilsHelper.composeEmailBody(templateData.body, { + name: userName, + }), + }, + } + + await kafkaCommunication.pushEmailToKafka(payload) + } + + let defaultOrg = await organizationQueries.findOne( + { code: process.env.DEFAULT_ORGANISATION_CODE }, + { attributes: ['id'] } + ) + let defaultOrgId = defaultOrg.id + const modelName = await userQueries.getModelName() + + let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ + status: 'ACTIVE', + organization_id: { + [Op.in]: [user.organization_id, defaultOrgId], + }, + model_names: { [Op.contains]: [modelName] }, + }) + + const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) + user = utils.processDbResponse(user, prunedEntities) + + if (user && user.image) { + user.image = await utils.getDownloadableUrl(user.image) + } + user.email = plaintextEmailId + + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'PASSWORD_CHANGED_SUCCESSFULLY', + result, + }) + } catch (error) { + console.log(error) + throw error + } + } } From c259ef15a60c38a6acb442bdfceb0e2215400841 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 26 Mar 2024 15:19:52 +0530 Subject: [PATCH 31/67] google recaptcha --- src/constants/common.js | 6 ++++++ src/locales/en.json | 4 ++-- src/middlewares/authenticator.js | 15 +++++++++++++++ src/utils/captcha.js | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/utils/captcha.js diff --git a/src/constants/common.js b/src/constants/common.js index ce9011075..317783a9e 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -89,4 +89,10 @@ module.exports = { DELETED_STATUS: 'DELETED', DEFAULT_ORG_VISIBILITY: 'PUBLIC', ROLE_TYPE_NON_SYSTEM: 0, + captchaEnabledAPIs: ['/user/v1/account/login', '/interface/v1/account/login'], + google_recaptcha_API: { + HOST: 'https://www.google.com', + URL: '/recaptcha/api/siteverify', + METHOD: 'POST', + }, } diff --git a/src/locales/en.json b/src/locales/en.json index b5dad36d7..dbc9e757a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -114,6 +114,6 @@ "COLUMN_DOES_NOT_EXISTS": "Role column does not exists", "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance.", "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values.", - "INAVLID_ORG_ROLE_REQ": "Invalid organisation request" - + "INAVLID_ORG_ROLE_REQ": "Invalid organisation request", + "CAPTCHA_VERIFICATION_FAILED": "Captcha verification Failed!" } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index caed59bad..c6f6576da 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -14,6 +14,7 @@ const roleQueries = require('@database/queries/user-role') const rolePermissionMappingQueries = require('@database/queries/role-permission-mapping') const { Op } = require('sequelize') const responses = require('@helpers/responses') +const { verifyCaptchaToken } = require('@utils/captcha') async function checkPermissions(roleTitle, requestPath, requestMethod) { const parts = requestPath.match(/[^/]+/g) @@ -57,6 +58,20 @@ module.exports = async function (req, res, next) { } return false }) + if (process.env.CAPTCHA_ENABLE && process.env.CAPTCHA_TYPE == 'recaptcha') { + let isCaptchaEnabled = false + isCaptchaEnabled = common.captchaEnabledAPIs.includes(req.path) + + const captchaToken = req.get('captcha-token') + + if (isCaptchaEnabled && !(await verifyCaptchaToken(captchaToken))) { + throw responses.failureResponse({ + message: 'CAPTCHA_VERIFICATION_FAILED', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + } + } common.roleValidationPaths.map(function (path) { if (req.path.includes(path)) { diff --git a/src/utils/captcha.js b/src/utils/captcha.js new file mode 100644 index 000000000..d9bc42bb8 --- /dev/null +++ b/src/utils/captcha.js @@ -0,0 +1,32 @@ +'use strict' +const common = require('@constants/common') +const requester = require('@utils/requester') + +exports.verifyCaptchaToken = async (token, options = {}) => { + const headers = { + secret: process.env.RECAPTCHA_SECRET_KEY, + response: token, + } + const requestBody = {} + const queryParams = {} + let response + if (common.google_recaptcha_API.METHOD === 'POST') + response = await requester.post( + common.google_recaptcha_API.HOST, + common.google_recaptcha_API.URL, + headers, + requestBody, + queryParams + ) + else if (common.google_recaptcha_API.METHOD === 'GET') + response = await requester.get( + ecommon.google_recaptcha_API.HOST, + common.google_recaptcha_API.URL, + headers, + pathParams, + queryParams + ) + if (response.success) return true + + return false +} From 45d2f941e73bee9e5e317f4f4ca4d92360404b8c Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 26 Mar 2024 15:39:53 +0530 Subject: [PATCH 32/67] Comment changes --- src/controllers/v1/account.js | 2 +- src/locales/en.json | 6 ++-- src/services/account.js | 60 +++++------------------------------ 3 files changed, 13 insertions(+), 55 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 6202a5cc0..b5fa81738 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -260,7 +260,7 @@ module.exports = class Account { async changePassword(req) { try { - const result = await accountService.changePassword(req.body, req.decodedToken.id, req.decodedToken.name) + const result = await accountService.changePassword(req.body, req.decodedToken.id) return result } catch (error) { return error diff --git a/src/locales/en.json b/src/locales/en.json index b5dad36d7..9f754e900 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -114,6 +114,8 @@ "COLUMN_DOES_NOT_EXISTS": "Role column does not exists", "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance.", "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values.", - "INAVLID_ORG_ROLE_REQ": "Invalid organisation request" - + "INAVLID_ORG_ROLE_REQ": "Invalid organisation request", + "INCORRECT_OLD_PASSWORD": "Invalid old password", + "SAME_PASSWORD_ERROR": "New password cannot be same as old password", + "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully." } diff --git a/src/services/account.js b/src/services/account.js index 7f1fcd7b4..af8844d7e 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1267,11 +1267,10 @@ module.exports = class AccountHelper { * @returns {JSON} - password changed response */ - static async changePassword(bodyData, userId, userName) { + static async changePassword(bodyData, userId) { const projection = ['location'] try { const userCredentials = await UserCredentialQueries.findOne({ user_id: userId }) - const plaintextEmailId = emailEncryption.decrypt(userCredentials.email) if (!userCredentials) { return responses.failureResponse({ message: 'USER_DOESNOT_EXISTS', @@ -1279,13 +1278,11 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) } + const plaintextEmailId = emailEncryption.decrypt(userCredentials.email) + let user = await userQueries.findOne( { id: userCredentials.user_id, organization_id: userCredentials.organization_id }, - { - attributes: { - exclude: projection, - }, - } + { attributes: { exclude: projection } } ) if (!user) { return responses.failureResponse({ @@ -1294,15 +1291,6 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) } - let roles = await roleQueries.findAll({ id: user.roles, status: common.ACTIVE_STATUS }) - if (!roles) { - return responses.failureResponse({ - message: 'ROLE_NOT_FOUND', - statusCode: httpStatusCode.not_acceptable, - responseCode: 'CLIENT_ERROR', - }) - } - user.user_roles = roles const verifyOldPassword = utilsHelper.comparePassword(bodyData.oldPassword, userCredentials.password) if (!verifyOldPassword) { @@ -1316,28 +1304,20 @@ module.exports = class AccountHelper { const isPasswordSame = bcryptJs.compareSync(bodyData.newPassword, userCredentials.password) if (isPasswordSame) { return responses.failureResponse({ - message: 'INCORRECT_OLD_PASSWORD', + message: 'SAME_PASSWORD_ERROR', statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) } bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) - const updateParams = { - lastLoggedInAt: new Date().getTime(), - password: bodyData.newPassword, - } + const updateParams = { password: bodyData.newPassword } await userQueries.updateUser( { id: user.id, organization_id: userCredentials.organization_id }, updateParams ) - await UserCredentialQueries.updateUser( - { - email: userCredentials.email, - }, - { password: bodyData.newPassword } - ) + await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) await utilsHelper.redisDel(userCredentials.email) delete user.newPassword @@ -1354,37 +1334,13 @@ module.exports = class AccountHelper { to: plaintextEmailId, subject: templateData.subject, body: utilsHelper.composeEmailBody(templateData.body, { - name: userName, + name: user.name, }), }, } - await kafkaCommunication.pushEmailToKafka(payload) } - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id - const modelName = await userQueries.getModelName() - - let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ - status: 'ACTIVE', - organization_id: { - [Op.in]: [user.organization_id, defaultOrgId], - }, - model_names: { [Op.contains]: [modelName] }, - }) - - const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) - user = utils.processDbResponse(user, prunedEntities) - - if (user && user.image) { - user.image = await utils.getDownloadableUrl(user.image) - } - user.email = plaintextEmailId - const result = {} return responses.successResponse({ statusCode: httpStatusCode.ok, From fbfb5dfffcfb03b772e07daf76c04c1d6eac2b08 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 26 Mar 2024 15:49:00 +0530 Subject: [PATCH 33/67] comment changes --- src/services/account.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/account.js b/src/services/account.js index af8844d7e..2b740c47d 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1320,8 +1320,6 @@ module.exports = class AccountHelper { await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) await utilsHelper.redisDel(userCredentials.email) - delete user.newPassword - const templateData = await notificationTemplateQueries.findOneEmailTemplate( process.env.CHANGE_PASSWORD_TEMPLATE_CODE ) From 16dfe703126fccc1a52c159436c286280aa87d71 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 26 Mar 2024 16:34:27 +0530 Subject: [PATCH 34/67] validators for changePassword --- src/validators/v1/account.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/validators/v1/account.js b/src/validators/v1/account.js index f0af585fc..434a3025b 100644 --- a/src/validators/v1/account.js +++ b/src/validators/v1/account.js @@ -127,4 +127,16 @@ module.exports = { return true }) }, + + changePassword: (req) => { + req.checkBody('oldPassword').notEmpty().withMessage('Password field is empty') + + req.checkBody('newPassword') + .notEmpty() + .withMessage('Password field is empty') + .matches(process.env.PASSWORD_POLICY_REGEX) + .withMessage(process.env.PASSWORD_POLICY_MESSAGE) + .custom((value) => !/\s/.test(value)) + .withMessage('Password cannot contain spaces') + }, } From 4806aff19d7e7c40ec813fdf3441c95b8e751032 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 26 Mar 2024 17:55:47 +0530 Subject: [PATCH 35/67] removed interface apis --- src/constants/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/common.js b/src/constants/common.js index 317783a9e..e5a0f6397 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -89,7 +89,7 @@ module.exports = { DELETED_STATUS: 'DELETED', DEFAULT_ORG_VISIBILITY: 'PUBLIC', ROLE_TYPE_NON_SYSTEM: 0, - captchaEnabledAPIs: ['/user/v1/account/login', '/interface/v1/account/login'], + captchaEnabledAPIs: ['/user/v1/account/login'], google_recaptcha_API: { HOST: 'https://www.google.com', URL: '/recaptcha/api/siteverify', From decdc024a7553594fa1ecb1c4996d843c4a7e3a6 Mon Sep 17 00:00:00 2001 From: Nevil Date: Tue, 26 Mar 2024 18:21:02 +0530 Subject: [PATCH 36/67] updated signed URL expiry to be dynamic --- src/.env.sample | 2 ++ src/envVariables.js | 5 +++++ src/generics/cloud-services.js | 8 ++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/.env.sample b/src/.env.sample index fcb9b60bc..8c94049f4 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -176,3 +176,5 @@ DOWNLOAD_URL_EXPIRATION_DURATION = 120000 #database url DATABASE_URL=postgres://postgres:postgres@localhost:5432/elevate-user +# Expiry time for the signed urls +SIGNED_URL_EXPIRY_IN_MILLISECONDS = 120000 \ No newline at end of file diff --git a/src/envVariables.js b/src/envVariables.js index 941d068eb..22d106dba 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -256,6 +256,11 @@ let enviromentVariables = { optional: true, default: 3600000, }, + SIGNED_URL_EXPIRY_IN_MILLISECONDS: { + message: 'Required signed url expiration time in milliseconds', + optional: true, + default: 3600000, + }, } let success = true diff --git a/src/generics/cloud-services.js b/src/generics/cloud-services.js index 5610c7297..60001d220 100644 --- a/src/generics/cloud-services.js +++ b/src/generics/cloud-services.js @@ -17,7 +17,7 @@ module.exports = class FilesHelper { destFilePath: destFilePath, // Stored file path - location from bucket - example - users/abc.png bucketName: bucketName, // google cloud storage bucket in which action is peformed over file actionType: actionType, // signed url usage type - example ('read' | 'write' | 'delete' | 'resumable') - expiry: Date.now() + 1000 * 60 * 30, // signed url expiration time - In ms from current time - type number | string | Date + expiry: Date.now() + parseFloat(process.env.SIGNED_URL_EXPIRY_IN_MILLISECONDS), // signed url expiration time - In ms from current time - type number | string | Date gcpProjectId: process.env.GCP_PROJECT_ID, // google cloud storage project id gcpJsonFilePath: path.join(__dirname, '../', process.env.GCP_PATH), // google cloud storage json configuration file absolute path for connectivity contentType: 'multipart/form-data', // content type of file, example multipart/form-data, image/png, csv/text etc @@ -37,7 +37,7 @@ module.exports = class FilesHelper { destFilePath: destFilePath, // Stored file path - i.e location from bucket - ex - users/abc.png bucketName: bucketName, // aws s3 storage bucket in which action is peformed over file actionType: actionType, // signed url usage type - example ('putObject' | 'getObject') - expiry: 30 * 60, // signed url expiration time - In sec - type number + expiry: parseFloat(process.env.SIGNED_URL_EXPIRY_IN_MILLISECONDS) / 1000, // signed url expiration time - In sec - type number accessKeyId: process.env.AWS_ACCESS_KEY_ID, // aws s3 access key id secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // aws s3 secret access key bucketRegion: process.env.AWS_BUCKET_REGION, // aws region where bucket will be located, example - 'ap-south-1' @@ -61,7 +61,7 @@ module.exports = class FilesHelper { const options = { destFilePath: destFilePath, // Stored file path - i.e location from container - ex - users/abc.png containerName: containerName, // container in which file gets saved - expiry: 30, // signed url expiration time - In minute - type number + expiry: parseFloat(process.env.SIGNED_URL_EXPIRY_IN_MILLISECONDS) / 60000, // signed url expiration time - In minute - type number actionType: 'w', // signed url usage type - example ('w' | 'r' | 'wr' | 'racwdl') - pair of any alphabets among racwdl accountName: process.env.AZURE_ACCOUNT_NAME, // account name of azure storage accountKey: process.env.AZURE_ACCOUNT_KEY, // account key of azure storage @@ -82,7 +82,7 @@ module.exports = class FilesHelper { destFilePath: destFilePath, // Stored file path - i.e location from bucket - ex - users/abc.png bucketName: bucketName, // Oci storage bucket in which action is peformed over file actionType: actionType, // signed url usage type - example ('putObject' | 'getObject') - expiry: 30 * 60, // signed url expiration time - In sec - type number + expiry: parseFloat(process.env.SIGNED_URL_EXPIRY_IN_MILLISECONDS) / 1000, // signed url expiration time - In sec - type number accessKeyId: process.env.OCI_ACCESS_KEY_ID, // Oci access key id secretAccessKey: process.env.OCI_SECRET_ACCESS_KEY, // Oci secret access key bucketRegion: process.env.OCI_BUCKET_REGION, // Oci region where bucket will be located, example - 'ap-south-1' From 5c618fc26058cf5984df58e71bce8c5139749259 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 26 Mar 2024 21:13:55 +0530 Subject: [PATCH 37/67] review comments --- src/constants/common.js | 5 ----- src/envVariables.js | 28 ++++++++++++++++++++++++++++ src/middlewares/authenticator.js | 16 +++++++++++----- src/utils/captcha.js | 29 +++++++++++------------------ 4 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/constants/common.js b/src/constants/common.js index e5a0f6397..74c54835d 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -90,9 +90,4 @@ module.exports = { DEFAULT_ORG_VISIBILITY: 'PUBLIC', ROLE_TYPE_NON_SYSTEM: 0, captchaEnabledAPIs: ['/user/v1/account/login'], - google_recaptcha_API: { - HOST: 'https://www.google.com', - URL: '/recaptcha/api/siteverify', - METHOD: 'POST', - }, } diff --git a/src/envVariables.js b/src/envVariables.js index a8a3ba38a..468afebb9 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -257,6 +257,34 @@ let enviromentVariables = { optional: true, default: 3600000, }, + CAPTCHA_ENABLE: { + message: 'Required CAPTCHA ENABLE true or false', + optional: false, + }, + CAPTCHA_SERVICE: { + message: 'Required CAPTCHA SERVICE', + optional: true, + default: 'googleRecaptcha', + }, + RECAPTCHA_SECRET_KEY: { + message: 'Required CAPTCHA SERVICE secret key', + optional: false, + }, + GOOGLE_RECAPTCHA_HOST: { + message: 'Required CAPTCHA Host IP', + optional: true, + default: 'https://www.google.com', + }, + GOOGLE_RECAPTCHA_URL: { + message: 'Required CAPTCHA SERVICE API URL', + optional: true, + default: '/recaptcha/api/siteverify', + }, + GOOGLE_RECAPTCHA_METHOD: { + message: 'Required CAPTCHA SERVICE Method', + optional: true, + default: 'POST', + }, } let success = true diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index c6f6576da..66e2587d1 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -58,13 +58,19 @@ module.exports = async function (req, res, next) { } return false }) - if (process.env.CAPTCHA_ENABLE && process.env.CAPTCHA_TYPE == 'recaptcha') { - let isCaptchaEnabled = false - isCaptchaEnabled = common.captchaEnabledAPIs.includes(req.path) - const captchaToken = req.get('captcha-token') + // check if captcha is enabled for the route + const isCaptchaEnabledForRoute = common.captchaEnabledAPIs.includes(req.path) ? true : false + + // check if captcha check is enabled in the env + const isCaptchaEnabled = + process.env.CAPTCHA_ENABLE.toLowerCase() == 'true' || process.env.CAPTCHA_ENABLE == true ? true : false - if (isCaptchaEnabled && !(await verifyCaptchaToken(captchaToken))) { + if (isCaptchaEnabled && isCaptchaEnabledForRoute) { + // get the token from API + const captchaToken = req.get('captcha-token') + // verify token + if (!(await verifyCaptchaToken(captchaToken))) { throw responses.failureResponse({ message: 'CAPTCHA_VERIFICATION_FAILED', statusCode: httpStatusCode.unauthorized, diff --git a/src/utils/captcha.js b/src/utils/captcha.js index d9bc42bb8..f716977eb 100644 --- a/src/utils/captcha.js +++ b/src/utils/captcha.js @@ -10,23 +10,16 @@ exports.verifyCaptchaToken = async (token, options = {}) => { const requestBody = {} const queryParams = {} let response - if (common.google_recaptcha_API.METHOD === 'POST') - response = await requester.post( - common.google_recaptcha_API.HOST, - common.google_recaptcha_API.URL, - headers, - requestBody, - queryParams - ) - else if (common.google_recaptcha_API.METHOD === 'GET') - response = await requester.get( - ecommon.google_recaptcha_API.HOST, - common.google_recaptcha_API.URL, - headers, - pathParams, - queryParams - ) + if (process.env.CAPTCHA_SERVICE == 'googleRecaptcha') { + if (process.env.GOOGLE_RECAPTCHA_METHOD === 'POST') + response = await requester.post( + process.env.GOOGLE_RECAPTCHA_HOST, + process.env.GOOGLE_RECAPTCHA_URL, + headers, + requestBody, + queryParams + ) + } if (response.success) return true - - return false + else if (!response.success) return false } From a24b42be2331f6a8afc78a7280fdc38b8d918680 Mon Sep 17 00:00:00 2001 From: vishnu Date: Tue, 26 Mar 2024 22:21:43 +0530 Subject: [PATCH 38/67] user-sessions table added to user service --- Dockerfile | 2 +- ...240326110128-create-user-sessions-table.js | 43 +++++++++ src/database/models/user-sessions.js | 46 ++++++++++ src/database/queries/user-sessions.js | 91 +++++++++++++++++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/database/migrations/20240326110128-create-user-sessions-table.js create mode 100644 src/database/models/user-sessions.js create mode 100644 src/database/queries/user-sessions.js diff --git a/Dockerfile b/Dockerfile index d9688ab63..ddb04c98d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16 +FROM node:20 #Set working directory WORKDIR /var/src/ diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js new file mode 100644 index 000000000..663b335e1 --- /dev/null +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -0,0 +1,43 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('user_sessions', { + id: { + type: Sequelize.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: Sequelize.INTEGER, + allowNull: false, + }, + started_at: { + type: Sequelize.DATE, + allowNull: false, + }, + ended_at: { + type: Sequelize.DATE, + allowNull: true, + }, + token: { + type: Sequelize.STRING, + allowNull: false, + }, + device_info: { + type: Sequelize.JSONB, + allowNull: true, + }, + refresh_token: { + type: Sequelize.STRING, + allowNull: false, + }, + }) + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('user_sessions') + }, +} diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js new file mode 100644 index 000000000..00fdec943 --- /dev/null +++ b/src/database/models/user-sessions.js @@ -0,0 +1,46 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const UserSessions = sequelize.define( + 'UserSessions', + { + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + started_at: { + type: DataTypes.DATE, + allowNull: false, + }, + ended_at: { + type: DataTypes.DATE, + allowNull: true, + }, + token: { + type: DataTypes.STRING, + allowNull: false, + }, + device_info: { + type: DataTypes.JSONB, + allowNull: true, + }, + refresh_token: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'UserSessions', + tableName: 'user_sessions', + freezeTableName: true, + paranoid: true, + } + ) + return UserSessions +} diff --git a/src/database/queries/user-sessions.js b/src/database/queries/user-sessions.js new file mode 100644 index 000000000..599d60819 --- /dev/null +++ b/src/database/queries/user-sessions.js @@ -0,0 +1,91 @@ +/** + * name : queries/user-sessions.js + * author : Vishnu + * created-date : 26-Mar-2024 + * Description : user-sessions table query methods. + */ + +// Dependencies +'use strict' +const UserSessions = require('@database/models/index').UserSessions +const { Op } = require('sequelize') + +/** + * Find one record based on the provided filter. + * @param {Object} filter - The filter object to specify the condition for selecting the record. + * @param {Object} options - Additional options for the query (optional). + * @returns {Promise} - A promise that resolves to the found record or an error. + */ +exports.findOne = async (filter, options = {}) => { + try { + return await UserSessions.findOne({ + where: filter, + ...options, + raw: true, + }) + } catch (error) { + return error + } +} + +/** + * Find a record by its primary key. + * @param {number|string} id - The primary key value of the record. + * @returns {Promise} - A promise that resolves to the found record or an error. + */ +exports.findByPk = async (id) => { + try { + return await UserSessions.findByPk(id, { raw: true }) + } catch (error) { + return error + } +} + +/** + * Find all records based on the provided filter. + * @param {Object} filter - The filter object to specify the condition for selecting records. + * @param {Object} options - Additional options for the query (optional). + * @returns {Promise} - A promise that resolves to an array of found records or an error. + */ +exports.findAll = async (filter, options = {}) => { + try { + return await UserSessions.findAll({ + where: filter, + ...options, + raw: true, + }) + } catch (error) { + return error + } +} + +/** + * Update records based on the provided filter and update data. + * @param {Object} filter - The filter object to specify the condition for updating records. + * @param {Object} update - The update data to be applied to matching records. + * @param {Object} options - Additional options for the update operation (optional). + * @returns {Promise} - A promise that resolves to the number of updated records or an error. + */ +exports.update = async (filter, update, options = {}) => { + try { + return await await Session.update(update, { + where: filter, + ...options, + }) + } catch (error) { + return error + } +} + +/** + * Create a new record with the provided data. + * @param {Object} data - The data object representing the record to be created. + * @returns {Promise} - A promise that resolves to the created record or an error. + */ +exports.create = async (data) => { + try { + return await UserSessions.create(data, { returning: true }) + } catch (error) { + throw error + } +} From 9f1814f885f532fe538f8fc9264a1a2afea0a83b Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Wed, 27 Mar 2024 11:26:13 +0530 Subject: [PATCH 39/67] review comments --- src/envVariables.js | 5 ----- src/middlewares/authenticator.js | 32 ++++++++++++++++---------------- src/utils/captcha.js | 18 +++++++++--------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/envVariables.js b/src/envVariables.js index 50ae18326..d8650137e 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -279,11 +279,6 @@ let enviromentVariables = { optional: true, default: '/recaptcha/api/siteverify', }, - GOOGLE_RECAPTCHA_METHOD: { - message: 'Required CAPTCHA SERVICE Method', - optional: true, - default: 'POST', - }, SIGNED_URL_EXPIRY_IN_MILLISECONDS: { message: 'Required signed url expiration time in milliseconds', optional: true, diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 66e2587d1..5f303c345 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -59,23 +59,23 @@ module.exports = async function (req, res, next) { return false }) - // check if captcha is enabled for the route - const isCaptchaEnabledForRoute = common.captchaEnabledAPIs.includes(req.path) ? true : false - // check if captcha check is enabled in the env - const isCaptchaEnabled = - process.env.CAPTCHA_ENABLE.toLowerCase() == 'true' || process.env.CAPTCHA_ENABLE == true ? true : false - - if (isCaptchaEnabled && isCaptchaEnabledForRoute) { - // get the token from API - const captchaToken = req.get('captcha-token') - // verify token - if (!(await verifyCaptchaToken(captchaToken))) { - throw responses.failureResponse({ - message: 'CAPTCHA_VERIFICATION_FAILED', - statusCode: httpStatusCode.unauthorized, - responseCode: 'UNAUTHORIZED', - }) + const isCaptchaEnabled = process.env.CAPTCHA_ENABLE.toLowerCase() == 'true' + + if (isCaptchaEnabled) { + // check if captcha is enabled for the route + const isCaptchaEnabledForRoute = common.captchaEnabledAPIs.includes(req.path) + if (isCaptchaEnabledForRoute) { + // get the token from API + const captchaToken = req.get('captcha-token') + // verify token + if (!(await verifyCaptchaToken(captchaToken))) { + throw responses.failureResponse({ + message: 'CAPTCHA_VERIFICATION_FAILED', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + } } } diff --git a/src/utils/captcha.js b/src/utils/captcha.js index f716977eb..ac3783202 100644 --- a/src/utils/captcha.js +++ b/src/utils/captcha.js @@ -11,15 +11,15 @@ exports.verifyCaptchaToken = async (token, options = {}) => { const queryParams = {} let response if (process.env.CAPTCHA_SERVICE == 'googleRecaptcha') { - if (process.env.GOOGLE_RECAPTCHA_METHOD === 'POST') - response = await requester.post( - process.env.GOOGLE_RECAPTCHA_HOST, - process.env.GOOGLE_RECAPTCHA_URL, - headers, - requestBody, - queryParams - ) + response = await requester.post( + process.env.GOOGLE_RECAPTCHA_HOST, + process.env.GOOGLE_RECAPTCHA_URL, + headers, + requestBody, + queryParams + ) } if (response.success) return true - else if (!response.success) return false + + return false } From 12db6ce2f4440d550b78a44688628d062c1af865 Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 27 Mar 2024 11:43:43 +0530 Subject: [PATCH 40/67] user sessions --- src/controllers/v1/account.js | 3 +- ...240326110128-create-user-sessions-table.js | 4 +- src/database/models/user-sessions.js | 4 +- src/locales/en.json | 3 +- src/services/account.js | 23 ++++++- src/services/user-sessions.js | 62 +++++++++++++++++++ 6 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 src/services/user-sessions.js diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index b5fa81738..5b0f36dd7 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -45,8 +45,9 @@ module.exports = class Account { async login(req) { const params = req.body + const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} try { - const loggedInAccount = await accountService.login(params) + const loggedInAccount = await accountService.login(params, device_info) return loggedInAccount } catch (error) { return error diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js index 663b335e1..dbe955923 100644 --- a/src/database/migrations/20240326110128-create-user-sessions-table.js +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -24,7 +24,7 @@ module.exports = { }, token: { type: Sequelize.STRING, - allowNull: false, + allowNull: true, }, device_info: { type: Sequelize.JSONB, @@ -32,7 +32,7 @@ module.exports = { }, refresh_token: { type: Sequelize.STRING, - allowNull: false, + allowNull: true, }, }) }, diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js index 00fdec943..b70ce7e43 100644 --- a/src/database/models/user-sessions.js +++ b/src/database/models/user-sessions.js @@ -23,7 +23,7 @@ module.exports = (sequelize, DataTypes) => { }, token: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, device_info: { type: DataTypes.JSONB, @@ -31,7 +31,7 @@ module.exports = (sequelize, DataTypes) => { }, refresh_token: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, }, { diff --git a/src/locales/en.json b/src/locales/en.json index 9f754e900..59bb49d8e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -117,5 +117,6 @@ "INAVLID_ORG_ROLE_REQ": "Invalid organisation request", "INCORRECT_OLD_PASSWORD": "Invalid old password", "SAME_PASSWORD_ERROR": "New password cannot be same as old password", - "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully." + "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully.", + "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully" } diff --git a/src/services/account.js b/src/services/account.js index d3fd8cc71..aaf9f18b6 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -28,6 +28,7 @@ const { removeDefaultOrgEntityTypes } = require('@generics/utils') const UserCredentialQueries = require('@database/queries/userCredential') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') +const userSessions = require('@services/user-sessions') module.exports = class AccountHelper { /** * create account @@ -358,10 +359,11 @@ module.exports = class AccountHelper { * @param {Object} bodyData -request body contains user login deatils. * @param {String} bodyData.email - user email. * @param {String} bodyData.password - user password. + * @param {Object} deviceInformation - device information * @returns {JSON} - returns susccess or failure of login details. */ - static async login(bodyData) { + static async login(bodyData, deviceInformation) { try { const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) @@ -419,6 +421,14 @@ module.exports = class AccountHelper { }) } + // create user session entry and add session_id to token data + const userSessionDetails = await userSessions.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInformation + ) + console.log('userSessionDetails : ', userSessionDetails) const tokenDetail = { data: { id: user.id, @@ -492,6 +502,17 @@ module.exports = class AccountHelper { user.email = plaintextEmailId const result = { access_token: accessToken, refresh_token: refreshToken, user } + // update user-sessions with refresh token and access token + // update filter + // const updatedUserSession = await userSessions.createUserSession( + // user.id, // userid + // "", // refresh token + // "", // Access token + // deviceInformation + // ) + + // save data in redis against session_id, write a function for this + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_IN_SUCCESSFULLY', diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js new file mode 100644 index 000000000..d0900168e --- /dev/null +++ b/src/services/user-sessions.js @@ -0,0 +1,62 @@ +/** + * name : services/user-sessions.js + * author : Vishnu + * created-date : 26-Mar-2024 + * Description : user-sessions business logic. + */ + +// Dependencies +const userSessionsQueries = require('@database/queries/user-sessions') +const httpStatusCode = require('@generics/http-status') +const responses = require('@helpers/responses') + +// create user-session +module.exports = class UserSessionsHelper { + static async createUserSession(userId, refreshToken = '', accessToken = '', deviceInfo) { + try { + const userSessionDetails = { + user_id: userId, + device_info: deviceInfo, + started_at: Date.now(), + } + if (accessToken !== '') { + userSessionDetails.token = accessToken + } + if (accessToken !== '') { + userSessionDetails.refresh_token = refreshToken + } + console.log('user sessions details : ', userSessionDetails) + // create userSession + const userSession = await userSessionsQueries.create(userSessionDetails) + + console.log('userSessions : ', userSession) + + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'USER_SESSION_CREATED_SUCCESSFULLY', + result: userSession, + }) + } catch (error) { + console.log(error) + throw error + } + } + + static async updateUserSession(filter, update, options = {}) { + try { + const result = await userSessionsQueries.update(filter, update, options) + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_UPDATED_SUCCESSFULLY', + result: result, + }) + } catch (error) { + console.log(error) + throw error + } + } +} + +// update-user session +// add entry to redis +// update user session entry in redis From 672e3b8de1b0481731f0990659ec7a183bb276d6 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Wed, 27 Mar 2024 11:49:38 +0530 Subject: [PATCH 41/67] envVariables and message changes --- src/envVariables.js | 4 ++++ src/locales/en.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/envVariables.js b/src/envVariables.js index 941d068eb..94773bdbd 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -256,6 +256,10 @@ let enviromentVariables = { optional: true, default: 3600000, }, + CHANGE_PASSWORD_TEMPLATE_CODE: { + message: 'Required change password email template code', + optional: false, + }, } let success = true diff --git a/src/locales/en.json b/src/locales/en.json index 9f754e900..ec5ae542f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -117,5 +117,5 @@ "INAVLID_ORG_ROLE_REQ": "Invalid organisation request", "INCORRECT_OLD_PASSWORD": "Invalid old password", "SAME_PASSWORD_ERROR": "New password cannot be same as old password", - "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully." + "PASSWORD_CHANGED_SUCCESSFULLY": "Your password has been changed successfully. Please log-in to continue." } From 1137e24bedd50e2a840abc7df3e3a2ceb4fbe6d8 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Wed, 27 Mar 2024 13:59:30 +0530 Subject: [PATCH 42/67] frontend requested change for recaptcha --- src/middlewares/authenticator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 5f303c345..4e9d410ea 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -67,7 +67,7 @@ module.exports = async function (req, res, next) { const isCaptchaEnabledForRoute = common.captchaEnabledAPIs.includes(req.path) if (isCaptchaEnabledForRoute) { // get the token from API - const captchaToken = req.get('captcha-token') + const captchaToken = req.get('captchaToken') // verify token if (!(await verifyCaptchaToken(captchaToken))) { throw responses.failureResponse({ From 62c9550065752b507ad2ca87fcb1f75d01c2bcb9 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Wed, 27 Mar 2024 15:44:43 +0530 Subject: [PATCH 43/67] frontend requested change for recaptcha --- src/middlewares/authenticator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 4e9d410ea..c5fe0e42c 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -67,7 +67,7 @@ module.exports = async function (req, res, next) { const isCaptchaEnabledForRoute = common.captchaEnabledAPIs.includes(req.path) if (isCaptchaEnabledForRoute) { // get the token from API - const captchaToken = req.get('captchaToken') + const captchaToken = req.get('Captcha-Token') // verify token if (!(await verifyCaptchaToken(captchaToken))) { throw responses.failureResponse({ From 567a4c76ac2178b55de75dec824f07e6afb5ff87 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Wed, 27 Mar 2024 20:34:37 +0530 Subject: [PATCH 44/67] recaptcha issue fix --- src/utils/captcha.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/captcha.js b/src/utils/captcha.js index ac3783202..263edd2a0 100644 --- a/src/utils/captcha.js +++ b/src/utils/captcha.js @@ -4,10 +4,9 @@ const requester = require('@utils/requester') exports.verifyCaptchaToken = async (token, options = {}) => { const headers = { - secret: process.env.RECAPTCHA_SECRET_KEY, - response: token, + 'Content-Type': 'application/x-www-form-urlencoded', } - const requestBody = {} + const requestBody = `secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${token}` const queryParams = {} let response if (process.env.CAPTCHA_SERVICE == 'googleRecaptcha') { From 230d3ad54fdb48b4aa3a22f3c3de162eb811c714 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Thu, 28 Mar 2024 12:46:16 +0530 Subject: [PATCH 45/67] Adding new APIs to captcha --- src/constants/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/common.js b/src/constants/common.js index 74c54835d..9724f558a 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -89,5 +89,5 @@ module.exports = { DELETED_STATUS: 'DELETED', DEFAULT_ORG_VISIBILITY: 'PUBLIC', ROLE_TYPE_NON_SYSTEM: 0, - captchaEnabledAPIs: ['/user/v1/account/login'], + captchaEnabledAPIs: ['/user/v1/account/login', '/user/v1/account/create', '/user/v1/account/resetPassword'], } From f78b051406efe22fd1a0ef04e58d8239026a63df Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Thu, 28 Mar 2024 13:43:58 +0530 Subject: [PATCH 46/67] captcha token variable updated --- src/middlewares/authenticator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index c5fe0e42c..5f303c345 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -67,7 +67,7 @@ module.exports = async function (req, res, next) { const isCaptchaEnabledForRoute = common.captchaEnabledAPIs.includes(req.path) if (isCaptchaEnabledForRoute) { // get the token from API - const captchaToken = req.get('Captcha-Token') + const captchaToken = req.get('captcha-token') // verify token if (!(await verifyCaptchaToken(captchaToken))) { throw responses.failureResponse({ From b11acb2165ac3cffccd4e2784a91e06e157e5712 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 14:56:20 +0530 Subject: [PATCH 47/67] changes pushed for internal api testing. code clean up and optimisations pending --- src/.env.sample | 3 + src/constants/common.js | 1 + src/controllers/v1/account.js | 45 +++++- ...240326110128-create-user-sessions-table.js | 17 ++- ...240328084048-update-session-permissions.js | 48 ++++++ ...28084246-update-session-role-permission.js | 139 ++++++++++++++++++ src/database/models/user-sessions.js | 6 +- src/database/queries/user-sessions.js | 2 +- src/envVariables.js | 5 + src/generics/utils.js | 24 +++ src/locales/en.json | 5 +- src/middlewares/authenticator.js | 15 ++ src/services/account.js | 135 +++++++++++++++-- src/services/user-sessions.js | 134 ++++++++++++++++- 14 files changed, 552 insertions(+), 27 deletions(-) create mode 100644 src/database/migrations/20240328084048-update-session-permissions.js create mode 100644 src/database/migrations/20240328084246-update-session-role-permission.js diff --git a/src/.env.sample b/src/.env.sample index fcb9b60bc..5ab60d59f 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -176,3 +176,6 @@ DOWNLOAD_URL_EXPIRATION_DURATION = 120000 #database url DATABASE_URL=postgres://postgres:postgres@localhost:5432/elevate-user +#allowed idle time +ALLOWED_IDLE_TIME=300000 + diff --git a/src/constants/common.js b/src/constants/common.js index ce9011075..7925c3ee3 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -59,6 +59,7 @@ module.exports = { roleAssociationName: 'user_roles', ACTIVE_STATUS: 'ACTIVE', INACTIVE_STATUS: 'INACTIVE', + EXPIRED_STATUS: 'EXPIRED', MENTOR_ROLE: 'mentor', MENTEE_ROLE: 'mentee', SESSION_MANAGER_ROLE: 'session_manager', diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 5b0f36dd7..8f23a3a9f 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -7,6 +7,7 @@ // Dependencies const accountService = require('@services/account') +const userSessionsService = require('@services/user-sessions') module.exports = class Account { /** @@ -24,8 +25,9 @@ module.exports = class Account { async create(req) { const params = req.body + const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} try { - const createdAccount = await accountService.create(params) + const createdAccount = await accountService.create(params, device_info) return createdAccount } catch (error) { return error @@ -129,7 +131,8 @@ module.exports = class Account { async resetPassword(req) { const params = req.body try { - const result = await accountService.resetPassword(params) + const deviceInfo = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const result = await accountService.resetPassword(params, deviceInfo) return result } catch (error) { return error @@ -267,4 +270,42 @@ module.exports = class Account { return error } } + + /** + * Retrieve user sessions based on the request parameters. + * @param {Object} req - The request object containing query parameters and decoded token. + * @returns {Promise} - A promise that resolves to the user session details. + */ + + async sessions(req) { + try { + const filter = req.query && req.query.status ? req.query.status.toUpperCase() : '' + const userSessionDetails = await userSessionsService.list( + req.decodedToken.id, + filter, + req.pageSize, + req.pageNo + ) + return userSessionDetails + } catch (error) { + return error + } + } + + /** + * Validate a user session based on the provided token. + * @param {Object} req - The request object containing the token in the request body. + * @param {string} req.body.token - The token to validate the user session. + * @returns {Promise} - A promise that resolves to the validation result of the user session. + */ + + async validateUserSession(req) { + try { + const token = req.body.token + const validateUserSession = await userSessionsService.validateUserSession(token) + return validateUserSession + } catch (error) { + return error + } + } } diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js index dbe955923..7e0762776 100644 --- a/src/database/migrations/20240326110128-create-user-sessions-table.js +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -15,7 +15,7 @@ module.exports = { allowNull: false, }, started_at: { - type: Sequelize.DATE, + type: Sequelize.BIGINT, allowNull: false, }, ended_at: { @@ -23,7 +23,7 @@ module.exports = { allowNull: true, }, token: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: true, }, device_info: { @@ -31,9 +31,20 @@ module.exports = { allowNull: true, }, refresh_token: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: true, }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + deleted_at: { + type: Sequelize.DATE, + }, }) }, diff --git a/src/database/migrations/20240328084048-update-session-permissions.js b/src/database/migrations/20240328084048-update-session-permissions.js new file mode 100644 index 000000000..a6f3df195 --- /dev/null +++ b/src/database/migrations/20240328084048-update-session-permissions.js @@ -0,0 +1,48 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + try { + const permissionsData = [ + { + code: 'get_user_sessions', + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + status: 'ACTIVE', + }, + { + code: 'validate_user_sessions', + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + status: 'ACTIVE', + }, + ] + + // Batch insert permissions + await queryInterface.bulkInsert( + 'permissions', + permissionsData.map((permission) => ({ + ...permission, + created_at: new Date(), + updated_at: new Date(), + })) + ) + } catch (error) { + console.error('Error in migration:', error) + throw error + } + }, + + async down(queryInterface, Sequelize) { + try { + // Rollback migration by deleting all permissions + await queryInterface.bulkDelete('permissions', null, {}) + } catch (error) { + console.error('Error in rollback migration:', error) + throw error + } + }, +} diff --git a/src/database/migrations/20240328084246-update-session-role-permission.js b/src/database/migrations/20240328084246-update-session-role-permission.js new file mode 100644 index 000000000..f86db8dd8 --- /dev/null +++ b/src/database/migrations/20240328084246-update-session-role-permission.js @@ -0,0 +1,139 @@ +'use strict' + +require('module-alias/register') +require('dotenv').config() +const common = require('@constants/common') +const Permissions = require('@database/models/index').Permission + +const getPermissionId = async (module, request_type, api_path) => { + try { + const permission = await Permissions.findOne({ + where: { module, request_type, api_path }, + }) + if (!permission) { + throw new Error( + `Permission not found for module: ${module}, request_type: ${request_type}, api_path: ${api_path}` + ) + } + return permission.id + } catch (error) { + throw new Error(`Error while fetching permission: ${error.message}`) + } +} + +module.exports = { + up: async (queryInterface, Sequelize) => { + try { + const rolePermissionsData = await Promise.all([ + { + role_title: common.MENTOR_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.ORG_ADMIN_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.USER_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.SESSION_MANAGER_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.MENTEE_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + + { + role_title: common.MENTOR_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.MENTEE_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.ORG_ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.USER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.SESSION_MANAGER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + ]) + + await queryInterface.bulkInsert( + 'role_permission_mapping', + rolePermissionsData.map((data) => ({ + ...data, + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + })) + ) + } catch (error) { + console.log(error) + console.error(`Migration error: ${error.message}`) + throw error + } + }, + + down: async (queryInterface, Sequelize) => { + try { + await queryInterface.bulkDelete('role_permission_mapping', null, {}) + } catch (error) { + console.error(`Rollback migration error: ${error.message}`) + throw error + } + }, +} diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js index b70ce7e43..50f6e88bb 100644 --- a/src/database/models/user-sessions.js +++ b/src/database/models/user-sessions.js @@ -14,7 +14,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, }, started_at: { - type: DataTypes.DATE, + type: DataTypes.BIGINT, allowNull: false, }, ended_at: { @@ -22,7 +22,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: true, }, token: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, }, device_info: { @@ -30,7 +30,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: true, }, refresh_token: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, }, }, diff --git a/src/database/queries/user-sessions.js b/src/database/queries/user-sessions.js index 599d60819..37abb8762 100644 --- a/src/database/queries/user-sessions.js +++ b/src/database/queries/user-sessions.js @@ -68,7 +68,7 @@ exports.findAll = async (filter, options = {}) => { */ exports.update = async (filter, update, options = {}) => { try { - return await await Session.update(update, { + return await await UserSessions.update(update, { where: filter, ...options, }) diff --git a/src/envVariables.js b/src/envVariables.js index 941d068eb..7a8341d85 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -256,6 +256,11 @@ let enviromentVariables = { optional: true, default: 3600000, }, + ALLOWED_IDLE_TIME: { + message: 'Require allowed idle time', + optional: true, + default: 3600, + }, } let success = true diff --git a/src/generics/utils.js b/src/generics/utils.js index 2e45d706f..843f85218 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -434,6 +434,29 @@ const getRoleTitlesFromId = (roleIds = [], roleList = []) => { }) } +const convertDurationToSeconds = (duration) => { + const timeUnits = { + s: 1, + m: 60, + h: 3600, + d: 86400, + } + + const match = /^(\d*\.?\d*)([smhd])$/.exec(duration) + if (!match) { + throw new Error('Invalid duration format') + } + + const value = parseFloat(match[1]) + const unit = match[2] + + if (!(unit in timeUnits)) { + throw new Error('Invalid duration unit') + } + + return value * timeUnits[unit] +} + module.exports = { generateToken, hashPassword, @@ -464,4 +487,5 @@ module.exports = { isValidName, generateWhereClause, getRoleTitlesFromId, + convertDurationToSeconds, } diff --git a/src/locales/en.json b/src/locales/en.json index 59bb49d8e..913f85808 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -118,5 +118,8 @@ "INCORRECT_OLD_PASSWORD": "Invalid old password", "SAME_PASSWORD_ERROR": "New password cannot be same as old password", "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully.", - "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully" + "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully", + "USER_SESSION_UPDATED_CESSFULLY": "User session updated successfully", + "USER_SESSION_VALIDATED_SUCCESSFULLY": "User session validated successfully", + "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully" } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index caed59bad..ce1b63364 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -14,6 +14,7 @@ const roleQueries = require('@database/queries/user-role') const rolePermissionMappingQueries = require('@database/queries/role-permission-mapping') const { Op } = require('sequelize') const responses = require('@helpers/responses') +const utilsHelper = require('@generics/utils') async function checkPermissions(roleTitle, requestPath, requestMethod) { const parts = requestPath.match(/[^/]+/g) @@ -95,6 +96,20 @@ module.exports = async function (req, res, next) { try { decodedToken = jwt.verify(authHeaderArray[1], process.env.ACCESS_TOKEN_SECRET) + // Get redis key for session + const sessionId = decodedToken.data.session_id.toString() + // Get data from redis + const redisData = await utilsHelper.redisGet(sessionId) + + // If data is not in redis, token is invalid + if (!redisData || redisData.accessToken !== authHeaderArray[1]) { + throw unAuthorizedResponse + } + + // Renew the TTL if allowed idle time is greater than zero + if (process.env.ALLOWED_IDLE_TIME != null) { + await utilsHelper.redisSet(sessionId, redisData, process.env.ALLOWED_IDLE_TIME) + } } catch (err) { if (err.name === 'TokenExpiredError') { throw responses.failureResponse({ diff --git a/src/services/account.js b/src/services/account.js index aaf9f18b6..ae3cbd4f9 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -43,7 +43,7 @@ module.exports = class AccountHelper { * @returns {JSON} - returns account creation details. */ - static async create(bodyData) { + static async create(bodyData, deviceInfo) { const projection = ['password', 'refresh_tokens'] try { @@ -229,10 +229,19 @@ module.exports = class AccountHelper { } ) + // create user session entry and add session_id to token data + const userSessionDetails = await userSessions.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInfo + ) + const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles: roleData, }, @@ -262,12 +271,15 @@ module.exports = class AccountHelper { process.env.ACCESS_TOKEN_SECRET, common.accessTokenExpiry ) + const refreshToken = utilsHelper.generateToken( tokenDetail, process.env.REFRESH_TOKEN_SECRET, common.refreshTokenExpiry ) + await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + let refresh_token = new Array() refresh_token.push({ token: refreshToken, @@ -365,6 +377,7 @@ module.exports = class AccountHelper { static async login(bodyData, deviceInformation) { try { + console.log('Yeaahhhhh +++= :', common.accessTokenExpiry) const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) const userCredentials = await UserCredentialQueries.findOne({ @@ -428,11 +441,12 @@ module.exports = class AccountHelper { '', // Access token deviceInformation ) - console.log('userSessionDetails : ', userSessionDetails) + console.log('userSessionDetails ****************: ', userSessionDetails.result.id) const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles: roles, }, @@ -502,16 +516,7 @@ module.exports = class AccountHelper { user.email = plaintextEmailId const result = { access_token: accessToken, refresh_token: refreshToken, user } - // update user-sessions with refresh token and access token - // update filter - // const updatedUserSession = await userSessions.createUserSession( - // user.id, // userid - // "", // refresh token - // "", // Access token - // deviceInformation - // ) - - // save data in redis against session_id, write a function for this + await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -625,6 +630,23 @@ module.exports = class AccountHelper { }) } + /* check if redis data is present*/ + // Get redis key for session + console.log(' decoed token data ><<<<<<>>>: ', decodedToken) + const sessionId = decodedToken.data.session_id.toString() + // Get data from redis + let redisData = await utilsHelper.redisGet(sessionId) + console.log('check+++hhhhhhhhh++++++137redisData :', redisData, sessionId) + + // If data is not in redis, token is invalid + if (!redisData || redisData.refreshToken !== bodyData.refresh_token) { + return responses.failureResponse({ + message: 'REFRESH_TOKEN_NOT_FOUND', + statusCode: httpStatusCode.unauthorized, + responseCode: 'CLIENT_ERROR', + }) + } + /* Generate new access token */ const accessToken = utilsHelper.generateToken( { data: decodedToken.data }, @@ -632,6 +654,28 @@ module.exports = class AccountHelper { common.accessTokenExpiry ) + let expiryTime = process.env.ALLOWED_IDLE_TIME + if (process.env.ALLOWED_IDLE_TIME == null) { + console.log('inside if..............') + expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) + } + console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) + redisData.accessToken = accessToken + const res = await utilsHelper.redisSet(sessionId, redisData, expiryTime) + console.log('response redis set ::: ', res) + + // update user-sessions with access token + let check = await userSessions.updateUserSession( + { + id: decodedToken.data.id, + }, + { + token: accessToken, + } + ) + + console.log('wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww : ', check) + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ACCESS_TOKEN_GENERATED_SUCCESSFULLY', @@ -806,7 +850,7 @@ module.exports = class AccountHelper { * @returns {JSON} - returns password reset response */ - static async resetPassword(bodyData) { + static async resetPassword(bodyData, deviceInfo) { const projection = ['location'] try { const plaintextEmailId = bodyData.email.toLowerCase() @@ -866,17 +910,30 @@ module.exports = class AccountHelper { }) } bodyData.password = utilsHelper.hashPassword(bodyData.password) + + // create user session entry and add session_id to token data + const userSessionDetails = await userSessions.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInfo + ) const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles, }, } - const accessToken = utilsHelper.generateToken(tokenDetail, process.env.ACCESS_TOKEN_SECRET, '1d') - const refreshToken = utilsHelper.generateToken(tokenDetail, process.env.REFRESH_TOKEN_SECRET, '183d') + const accessToken = (tokenDetail, process.env.ACCESS_TOKEN_SECRET, common.accessTokenExpiry) + const refreshToken = utilsHelper.generateToken( + tokenDetail, + process.env.REFRESH_TOKEN_SECRET, + common.refreshTokenExpiry + ) let currentToken = { token: refreshToken, @@ -938,6 +995,9 @@ module.exports = class AccountHelper { } user.email = plaintextEmailId + /**update a new session entry with redis insert */ + await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + const result = { access_token: accessToken, refresh_token: refreshToken, user } return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -1371,4 +1431,49 @@ module.exports = class AccountHelper { throw error } } + + static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { + try { + console.log(' ggggggggggggggggggggggggggggggggggggggggggg : ', userSessionId, accessToken, refreshToken) + // update user-sessions with refresh token and access token + let check = await userSessions.updateUserSession( + { + id: userSessionId, + }, + { + token: accessToken, + refresh_token: refreshToken, + } + ) + console.log('checkkkkkkkkkkkkkkkkkkkk+++++++++', check) + + // save data in redis against session_id, write a function for this + const redisData = { + accessToken: accessToken, + refreshToken: refreshToken, + } + /** Allowed idle time set to zero (infinity indicator here) + * set TTL of redis to accessTokenExpiry. + * Else it will be there in redis permenantly and will affect listing of user sessions + */ + + let expiryTime = process.env.ALLOWED_IDLE_TIME + if (process.env.ALLOWED_IDLE_TIME == null) { + console.log('inside if..............') + expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) + } + console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) + const redisKey = userSessionId.toString() + const res = await utilsHelper.redisSet(redisKey, redisData, expiryTime) + + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_UPDATED_CESSFULLY', + result, + }) + } catch (error) { + throw error + } + } } diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index d0900168e..71607d307 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -7,8 +7,11 @@ // Dependencies const userSessionsQueries = require('@database/queries/user-sessions') +const utilsHelper = require('@generics/utils') const httpStatusCode = require('@generics/http-status') const responses = require('@helpers/responses') +const common = require('@constants/common') +const jwt = require('jsonwebtoken') // create user-session module.exports = class UserSessionsHelper { @@ -17,7 +20,7 @@ module.exports = class UserSessionsHelper { const userSessionDetails = { user_id: userId, device_info: deviceInfo, - started_at: Date.now(), + started_at: Math.floor(new Date().getTime() / 1000), } if (accessToken !== '') { userSessionDetails.token = accessToken @@ -25,7 +28,7 @@ module.exports = class UserSessionsHelper { if (accessToken !== '') { userSessionDetails.refresh_token = refreshToken } - console.log('user sessions details : ', userSessionDetails) + console.log('user sessions details >>>>>>>>>>>>>>>>>>>>>>>%%%%%%%%%%%%%: ', userSessionDetails) // create userSession const userSession = await userSessionsQueries.create(userSessionDetails) @@ -55,6 +58,133 @@ module.exports = class UserSessionsHelper { throw error } } + + /** + * Retrieve user sessions based on user ID, status, limit, and page. + * @param {number} userId - The ID of the user. + * @param {string} status - The status of the user sessions (e.g., 'ACTIVE', ''). + * @param {number} limit - The maximum number of user sessions to retrieve per page. + * @param {number} page - The page number for pagination. + * @returns {Promise} - A promise that resolves to the user session details. + */ + + static async list(userId, status, limit, page) { + try { + const filter = { + user_id: userId, + } + const offset = (page - 1) * limit + + // If ended at is null, the status can be active. after verification with redis we can confirm + if (status === common.ACTIVE_STATUS) { + filter.ended_at = null + } + + // create userSession + const userSessions = await userSessionsQueries.findAll(filter) + const activeSessions = [] + const inActiveSessions = [] + for (const session of userSessions) { + const id = session.id.toString() // Convert ID to string + const redisData = await utilsHelper.redisGet(id) + let statusToSend = status + if (redisData === null) { + if (status === common.ACTIVE_STATUS) { + continue // Skip this element if data is not in Redis and status is active + } else { + statusToSend = common.INACTIVE_STATUS + } + } else { + statusToSend = common.ACTIVE_STATUS + } + + if (status === common.ACTIVE_STATUS && statusToSend === common.ACTIVE_STATUS) { + const responseObj = { + id: session.id, + device_info: session.device_info, + status: statusToSend, + login_time: session.started_at, + logout_time: session.ended_at, + } + activeSessions.push(responseObj) + } else if (status === '') { + const responseObj = { + id: session.id, + device_info: session.device_info, + status: statusToSend, + login_time: session.started_at, + logout_time: session.ended_at, + } + responseObj.status === common.ACTIVE_STATUS + ? activeSessions.push(responseObj) + : inActiveSessions.push(responseObj) + } + } + console.log('activeSessions : ', activeSessions) + console.log('inActiveSessions : ', inActiveSessions) + + const result = [...activeSessions, ...inActiveSessions] + + // Paginate the result array + const paginatedResult = result.slice(offset, offset + limit) + + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'USER_SESSION_FETCHED_SUCCESSFULLY', + result: { + data: paginatedResult, + count: result.length, + }, + }) + } catch (error) { + console.log(error) + throw error + } + } + + static async validateUserSession(token) { + // token validation failure message + const unAuthorizedResponse = responses.failureResponse({ + message: 'UNAUTHORIZED_REQUEST', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + + const tokenArray = token.split(' ') + + // If not bearer throw error + if (tokenArray[0] !== 'bearer') { + throw unAuthorizedResponse + } + try { + const decodedToken = jwt.verify(tokenArray[1], process.env.ACCESS_TOKEN_SECRET) + const sessionId = decodedToken.data.session_id.toString() + + const redisData = await utilsHelper.redisGet(sessionId) + + // If data is not in redis, token is invalid + if (!redisData || redisData.accessToken !== tokenArray[1]) { + throw unAuthorizedResponse + } + + // Renew the TTL if allowed idle is not infinite + if (process.env.ALLOWED_IDLE_TIME == null) { + await utilsHelper.redisSet(sessionId, redisData, process.env.ALLOWED_IDLE_TIME) + } + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_VALIDATED_SUCCESSFULLY', + result: { + data: { + user_session_active: true, + }, + }, + }) + } catch (err) { + throw unAuthorizedResponse + } + } } // update-user session From ad59c98dbe50fa0dd0ed2d8dd1021d4fe22065bf Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Thu, 28 Mar 2024 15:02:21 +0530 Subject: [PATCH 48/67] change password - logout condition --- src/services/account.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/account.js b/src/services/account.js index d3fd8cc71..a2cf6716d 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1311,13 +1311,20 @@ module.exports = class AccountHelper { } bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) - const updateParams = { password: bodyData.newPassword } + const updateParams = { password: bodyData.newPassword, refresh_tokens: [] } await userQueries.updateUser( { id: user.id, organization_id: userCredentials.organization_id }, updateParams ) await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) + + let refreshTokens = user.refresh_tokens ? user.refresh_tokens : [] + + // if (refreshTokens.length > 0) { + // await userQueries.updateUser( + // { id: user.id, organization_id: user.organization_id },{ refresh_tokens: [] }) + // } await utilsHelper.redisDel(userCredentials.email) const templateData = await notificationTemplateQueries.findOneEmailTemplate( From b7bb2c314654faf6df6b44404b44ee1963ed0850 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Thu, 28 Mar 2024 15:09:51 +0530 Subject: [PATCH 49/67] removing commented lines --- src/services/account.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/services/account.js b/src/services/account.js index a2cf6716d..6d041f040 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1319,12 +1319,6 @@ module.exports = class AccountHelper { ) await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) - let refreshTokens = user.refresh_tokens ? user.refresh_tokens : [] - - // if (refreshTokens.length > 0) { - // await userQueries.updateUser( - // { id: user.id, organization_id: user.organization_id },{ refresh_tokens: [] }) - // } await utilsHelper.redisDel(userCredentials.email) const templateData = await notificationTemplateQueries.findOneEmailTemplate( From d2cfa8e4bd3f81d63b59cca28a00b2ccdcfee9b2 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Thu, 28 Mar 2024 15:11:06 +0530 Subject: [PATCH 50/67] space --- src/services/account.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/account.js b/src/services/account.js index 6d041f040..9af41cefc 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1318,7 +1318,6 @@ module.exports = class AccountHelper { updateParams ) await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) - await utilsHelper.redisDel(userCredentials.email) const templateData = await notificationTemplateQueries.findOneEmailTemplate( From 8114f1c02d214abc52f8ab69393e5c9c9581dbdf Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 15:25:56 +0530 Subject: [PATCH 51/67] local merge check --- src/services/account.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/account.js b/src/services/account.js index ae3cbd4f9..35622856c 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -634,6 +634,7 @@ module.exports = class AccountHelper { // Get redis key for session console.log(' decoed token data ><<<<<<>>>: ', decodedToken) const sessionId = decodedToken.data.session_id.toString() + // Get data from redis let redisData = await utilsHelper.redisGet(sessionId) console.log('check+++hhhhhhhhh++++++137redisData :', redisData, sessionId) From 1d736406a25edef961d0c4a656f2d588a60ee539 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 21:22:36 +0530 Subject: [PATCH 52/67] code clean up before merge --- src/controllers/v1/account.js | 4 +- ...240326110128-create-user-sessions-table.js | 2 +- src/database/models/user-sessions.js | 2 +- src/envVariables.js | 2 +- src/locales/en.json | 3 +- src/services/account.js | 132 ++++++++++++++---- src/services/admin.js | 16 +++ src/services/user-sessions.js | 95 +++++++++++-- 8 files changed, 212 insertions(+), 44 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 8f23a3a9f..36069c8d4 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -8,7 +8,6 @@ // Dependencies const accountService = require('@services/account') const userSessionsService = require('@services/user-sessions') - module.exports = class Account { /** * create mentee account @@ -72,7 +71,8 @@ module.exports = class Account { const loggedOutAccount = await accountService.logout( req.body, req.decodedToken.id, - req.decodedToken.organization_id + req.decodedToken.organization_id, + req.decodedToken.session_id ) return loggedOutAccount } catch (error) { diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js index 7e0762776..2a558fff6 100644 --- a/src/database/migrations/20240326110128-create-user-sessions-table.js +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -19,7 +19,7 @@ module.exports = { allowNull: false, }, ended_at: { - type: Sequelize.DATE, + type: Sequelize.BIGINT, allowNull: true, }, token: { diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js index 50f6e88bb..ef7d0f957 100644 --- a/src/database/models/user-sessions.js +++ b/src/database/models/user-sessions.js @@ -18,7 +18,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, }, ended_at: { - type: DataTypes.DATE, + type: DataTypes.BIGINT, allowNull: true, }, token: { diff --git a/src/envVariables.js b/src/envVariables.js index 7a8341d85..99ad93417 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -259,7 +259,7 @@ let enviromentVariables = { ALLOWED_IDLE_TIME: { message: 'Require allowed idle time', optional: true, - default: 3600, + default: 0, }, } diff --git a/src/locales/en.json b/src/locales/en.json index 913f85808..f2d11be23 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -121,5 +121,6 @@ "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully", "USER_SESSION_UPDATED_CESSFULLY": "User session updated successfully", "USER_SESSION_VALIDATED_SUCCESSFULLY": "User session validated successfully", - "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully" + "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully", + "USER_SESSIONS_REMOVED_SUCCESSFULLY": "User sesions removed successfully" } diff --git a/src/services/account.js b/src/services/account.js index 35622856c..b3bcb0aff 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -28,7 +28,7 @@ const { removeDefaultOrgEntityTypes } = require('@generics/utils') const UserCredentialQueries = require('@database/queries/userCredential') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') -const userSessions = require('@services/user-sessions') +const userSessionsService = require('@services/user-sessions') module.exports = class AccountHelper { /** * create account @@ -40,6 +40,7 @@ module.exports = class AccountHelper { * @param {Boolean} bodyData.isAMentor - is a mentor or not . * @param {String} bodyData.email - user email. * @param {String} bodyData.password - user password. + * @param {Object} deviceInfo - Device information * @returns {JSON} - returns account creation details. */ @@ -229,8 +230,11 @@ module.exports = class AccountHelper { } ) - // create user session entry and add session_id to token data - const userSessionDetails = await userSessions.createUserSession( + /** + * create user session entry and add session_id to token data + * Entry should be created first, the session_id has to be added to token creation data + */ + const userSessionDetails = await userSessionsService.createUserSession( user.id, // userid '', // refresh token '', // Access token @@ -278,6 +282,11 @@ module.exports = class AccountHelper { common.refreshTokenExpiry ) + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) let refresh_token = new Array() @@ -377,7 +386,6 @@ module.exports = class AccountHelper { static async login(bodyData, deviceInformation) { try { - console.log('Yeaahhhhh +++= :', common.accessTokenExpiry) const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) const userCredentials = await UserCredentialQueries.findOne({ @@ -435,13 +443,13 @@ module.exports = class AccountHelper { } // create user session entry and add session_id to token data - const userSessionDetails = await userSessions.createUserSession( + const userSessionDetails = await userSessionsService.createUserSession( user.id, // userid '', // refresh token '', // Access token deviceInformation ) - console.log('userSessionDetails ****************: ', userSessionDetails.result.id) + const tokenDetail = { data: { id: user.id, @@ -516,6 +524,11 @@ module.exports = class AccountHelper { user.email = plaintextEmailId const result = { access_token: accessToken, refresh_token: refreshToken, user } + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) return responses.successResponse({ @@ -540,7 +553,7 @@ module.exports = class AccountHelper { * @returns {JSON} - returns accounts loggedout information. */ - static async logout(bodyData, user_id, organization_id) { + static async logout(bodyData, user_id, organization_id, userSessionId) { try { const user = await userQueries.findOne({ id: user_id, organization_id }) if (!user) { @@ -550,10 +563,34 @@ module.exports = class AccountHelper { responseCode: 'UNAUTHORIZED', }) } + /** + * Aquire user session_id based on the requests + */ + let userSessions = [] + if (bodyData.userSessionIds && bodyData.userSessionIds.length > 0) { + userSessions = bodyData.userSessionIds + } else { + userSessions.push(userSessionId) + } + + let tokenToRemove = [] + if (userSessions.length > 0) { + const userSessionData = await userSessionsService.findUserSession( + { + id: userSessions, + }, + { + attributes: ['refresh_token'], + } + ) + tokenToRemove = userSessionData.map(({ refresh_token }) => refresh_token) + } + + await userSessionsService.removeUserSessions(userSessions) let refreshTokens = user.refresh_tokens ? user.refresh_tokens : [] - refreshTokens = refreshTokens.filter(function (tokenData) { - return tokenData.token !== bodyData.refresh_token + refreshTokens = tokenToRemove.filter(function (tokenData) { + return !bodyData.refresh_token.includes(tokenData.token) }) /* Destroy refresh token for user */ @@ -632,12 +669,31 @@ module.exports = class AccountHelper { /* check if redis data is present*/ // Get redis key for session - console.log(' decoed token data ><<<<<<>>>: ', decodedToken) const sessionId = decodedToken.data.session_id.toString() // Get data from redis - let redisData = await utilsHelper.redisGet(sessionId) - console.log('check+++hhhhhhhhh++++++137redisData :', redisData, sessionId) + let redisData = {} + redisData = await utilsHelper.redisGet(sessionId) + + // if idle time set to infinity then db check should be done + if (!redisData && process.env.ALLOWED_IDLE_TIME == null) { + const userSessionData = await userSessionsService.findUserSession( + { + id: decodedToken.data.session_id, + }, + { + attributes: ['refresh_token'], + } + ) + if (!userSessionData) { + return responses.failureResponse({ + message: 'REFRESH_TOKEN_NOT_FOUND', + statusCode: httpStatusCode.unauthorized, + responseCode: 'CLIENT_ERROR', + }) + } + redisData.refreshToken = userSessionData[0].refresh_token + } // If data is not in redis, token is invalid if (!redisData || redisData.refreshToken !== bodyData.refresh_token) { @@ -655,18 +711,19 @@ module.exports = class AccountHelper { common.accessTokenExpiry ) + /** + * When idle tine is infinity set TTL to access token expiry + * If not redis data won't expire and timeout session will show as active in listing + */ let expiryTime = process.env.ALLOWED_IDLE_TIME if (process.env.ALLOWED_IDLE_TIME == null) { - console.log('inside if..............') expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) } - console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) redisData.accessToken = accessToken const res = await utilsHelper.redisSet(sessionId, redisData, expiryTime) - console.log('response redis set ::: ', res) // update user-sessions with access token - let check = await userSessions.updateUserSession( + let check = await userSessionsService.updateUserSession( { id: decodedToken.data.id, }, @@ -674,9 +731,6 @@ module.exports = class AccountHelper { token: accessToken, } ) - - console.log('wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww : ', check) - return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ACCESS_TOKEN_GENERATED_SUCCESSFULLY', @@ -913,7 +967,7 @@ module.exports = class AccountHelper { bodyData.password = utilsHelper.hashPassword(bodyData.password) // create user session entry and add session_id to token data - const userSessionDetails = await userSessions.createUserSession( + const userSessionDetails = await userSessionsService.createUserSession( user.id, // userid '', // refresh token '', // Access token @@ -997,6 +1051,11 @@ module.exports = class AccountHelper { user.email = plaintextEmailId /**update a new session entry with redis insert */ + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) const result = { access_token: accessToken, refresh_token: refreshToken, user } @@ -1393,7 +1452,7 @@ module.exports = class AccountHelper { } bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) - const updateParams = { password: bodyData.newPassword } + const updateParams = { password: bodyData.newPassword, refresh_tokens: [] } await userQueries.updateUser( { id: user.id, organization_id: userCredentials.organization_id }, @@ -1402,6 +1461,23 @@ module.exports = class AccountHelper { await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) await utilsHelper.redisDel(userCredentials.email) + // Find active sessions of user and remove them + const userSessionData = await userSessionsService.findUserSession( + { + user_id: userId, + ended_at: null, + }, + { + attributes: ['id'], + } + ) + const userSessionIds = userSessionData.map(({ id }) => id) + /** + * 1: Remove redis data + * 2: Update ended_at in user-sessions + */ + await userSessionsService.removeUserSessions(userSessionIds) + const templateData = await notificationTemplateQueries.findOneEmailTemplate( process.env.CHANGE_PASSWORD_TEMPLATE_CODE ) @@ -1433,11 +1509,18 @@ module.exports = class AccountHelper { } } + /** + * Update the user session with access token and refresh token, and set the data in Redis. + * @param {number} userSessionId - The ID of the user session to update. + * @param {string} accessToken - The new access token. + * @param {string} refreshToken - The new refresh token. + * @returns {Promise} - A promise that resolves to a success response after updating the user session and setting data in Redis. + * @throws {Error} - Throws an error if the update operation fails. + */ static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { try { - console.log(' ggggggggggggggggggggggggggggggggggggggggggg : ', userSessionId, accessToken, refreshToken) // update user-sessions with refresh token and access token - let check = await userSessions.updateUserSession( + let check = await userSessionsService.updateUserSession( { id: userSessionId, }, @@ -1446,7 +1529,6 @@ module.exports = class AccountHelper { refresh_token: refreshToken, } ) - console.log('checkkkkkkkkkkkkkkkkkkkk+++++++++', check) // save data in redis against session_id, write a function for this const redisData = { @@ -1460,10 +1542,8 @@ module.exports = class AccountHelper { let expiryTime = process.env.ALLOWED_IDLE_TIME if (process.env.ALLOWED_IDLE_TIME == null) { - console.log('inside if..............') expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) } - console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) const redisKey = userSessionId.toString() const res = await utilsHelper.redisSet(redisKey, redisData, expiryTime) diff --git a/src/services/admin.js b/src/services/admin.js index f367d071d..b1046864e 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -20,6 +20,7 @@ const UserCredentialQueries = require('@database/queries/userCredential') const adminService = require('../generics/materializedViews') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') +const userSessionsService = require('@services/user-sessions') module.exports = class AdminHelper { /** @@ -49,6 +50,21 @@ module.exports = class AdminHelper { await utils.redisDel(common.redisUserPrefix + userId.toString()) + /** + * Using userId get his active sessions + */ + const userSessionData = await userSessionsService.findUserSession( + { + user_id: userId, + ended_at: null, + }, + { + attributes: ['id'], + } + ) + const userSessionIds = userSessionData.map(({ id }) => id) + await userSessionsService.removeUserSessions(userSessionIds) + //code for remove user folder from cloud return responses.successResponse({ diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 71607d307..38f72d124 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -15,8 +15,21 @@ const jwt = require('jsonwebtoken') // create user-session module.exports = class UserSessionsHelper { + /** + * Create a user session. + * @param {number} userId - The ID of the user. + * @param {string} [refreshToken=''] - Optional. The refresh token associated with the session. + * @param {string} [accessToken=''] - Optional. The access token associated with the session. + * @param {Object} deviceInfo - Information about the device used for the session. + * @returns {Promise} - A promise that resolves to a success response with the created session details. + * @throws {Error} - Throws an error if any issue occurs during the process. + */ + static async createUserSession(userId, refreshToken = '', accessToken = '', deviceInfo) { try { + /** + * data for user-session creation + */ const userSessionDetails = { user_id: userId, device_info: deviceInfo, @@ -28,12 +41,10 @@ module.exports = class UserSessionsHelper { if (accessToken !== '') { userSessionDetails.refresh_token = refreshToken } - console.log('user sessions details >>>>>>>>>>>>>>>>>>>>>>>%%%%%%%%%%%%%: ', userSessionDetails) + // create userSession const userSession = await userSessionsQueries.create(userSessionDetails) - console.log('userSessions : ', userSession) - return responses.successResponse({ statusCode: httpStatusCode.created, message: 'USER_SESSION_CREATED_SUCCESSFULLY', @@ -45,6 +56,15 @@ module.exports = class UserSessionsHelper { } } + /** + * Update a user session. + * @param {Object} filter - The filter criteria to select the user session(s) to update. + * @param {Object} update - The data to be updated for the user session(s). + * @param {Object} [options={}] - Optional. Additional options for the update operation. + * @returns {Promise} - A promise that resolves to a success response with the updated session details. + * @throws {Error} - Throws an error if any issue occurs during the process. + */ + static async updateUserSession(filter, update, options = {}) { try { const result = await userSessionsQueries.update(filter, update, options) @@ -54,7 +74,6 @@ module.exports = class UserSessionsHelper { result: result, }) } catch (error) { - console.log(error) throw error } } @@ -92,7 +111,9 @@ module.exports = class UserSessionsHelper { if (status === common.ACTIVE_STATUS) { continue // Skip this element if data is not in Redis and status is active } else { - statusToSend = common.INACTIVE_STATUS + session.ended_at == null + ? (statusToSend = common.EXPIRED_STATUS) + : (statusToSend = common.INACTIVE_STATUS) } } else { statusToSend = common.ACTIVE_STATUS @@ -120,12 +141,11 @@ module.exports = class UserSessionsHelper { : inActiveSessions.push(responseObj) } } - console.log('activeSessions : ', activeSessions) - console.log('inActiveSessions : ', inActiveSessions) const result = [...activeSessions, ...inActiveSessions] // Paginate the result array + // The response is accumulated from two places. db and redis. So pagination is not possible on the fly const paginatedResult = result.slice(offset, offset + limit) return responses.successResponse({ @@ -137,11 +157,66 @@ module.exports = class UserSessionsHelper { }, }) } catch (error) { - console.log(error) throw error } } + /** + * Remove user sessions from both database and Redis. + * @param {number[]} userSessionIds - An array of user session IDs to be removed. + * @returns {Promise} - A promise that resolves to a success response upon successful removal. + */ + + static async removeUserSessions(userSessionIds) { + try { + // Delete user sessions from Redis + for (const sessionId of userSessionIds) { + await utilsHelper.redisDel(sessionId.toString()) + } + + // Update ended_at of user sessions in the database + const currentTime = Math.floor(Date.now() / 1000) // Current epoch time in seconds + const updateResult = await userSessionsQueries.update({ id: userSessionIds }, { ended_at: currentTime }) + + // Check if the update was successful + if (updateResult instanceof Error) { + throw updateResult // Throw error if update failed + } + + // Return success response + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSIONS_REMOVED_SUCCESSFULLY', + result, + }) + } catch (error) { + throw error + } + } + + /** + * Find user sessions based on the provided filter and options. + * @param {Object} filter - The filter criteria to find user sessions. + * @param {Object} [options={}] - Optional. Additional options for the query. + * @returns {Promise} - A promise that resolves to an array of user session objects. + * @throws {Error} - Throws an error if any issue occurs during the process. + */ + static async findUserSession(filter, options = {}) { + try { + return await userSessionsQueries.findAll(filter, options) + } catch (error) { + throw error + } + } + + /** + * Validate the user session token. + * @param {string} token - The token to validate. + * @returns {Promise} - A promise that resolves to a success response if the token is valid, otherwise throws an error. + * @throws {Error} - Throws an error if the token validation fails. + */ + static async validateUserSession(token) { // token validation failure message const unAuthorizedResponse = responses.failureResponse({ @@ -186,7 +261,3 @@ module.exports = class UserSessionsHelper { } } } - -// update-user session -// add entry to redis -// update user session entry in redis From 741614b75001bef8645be69feefc534291513b64 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 21:31:24 +0530 Subject: [PATCH 53/67] internal url updated --- src/constants/common.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/common.js b/src/constants/common.js index 7925c3ee3..602b33ca9 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -26,6 +26,7 @@ module.exports = { '/user/v1/account/search', '/user/v1/organization/list', '/user/v1/user-role/default', + '/user/v1/account/validateUserSession', ], notificationEmailType: 'email', accessTokenExpiry: process.env.ACCESS_TOKEN_EXPIRY, From a9ae2e75d71aedfe06aa822236c73cea8314f127 Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 29 Mar 2024 16:21:02 +0530 Subject: [PATCH 54/67] user table and related api changes --- src/controllers/v1/admin.js | 3 +- ...-token-and-login-time-column-from-users.js | 22 ++ src/database/models/users.js | 2 - src/services/account.js | 193 ++---------------- src/services/admin.js | 35 +++- src/services/user-sessions.js | 49 +++++ 6 files changed, 127 insertions(+), 177 deletions(-) create mode 100644 src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index 46f10fea5..73c432403 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -78,7 +78,8 @@ module.exports = class Admin { async login(req) { try { - const loggedInAccount = await adminService.login(req.body) + const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const loggedInAccount = await adminService.login(req.body, device_info) return loggedInAccount } catch (error) { return error diff --git a/src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js b/src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js new file mode 100644 index 000000000..c8ed62cf4 --- /dev/null +++ b/src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js @@ -0,0 +1,22 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + // Drop the materialized view + await queryInterface.sequelize.query('DROP MATERIALIZED VIEW IF EXISTS m_users') + + // Remove the columns from the users table + await queryInterface.removeColumn('users', 'last_logged_in_at') + await queryInterface.removeColumn('users', 'refresh_tokens') + }, + + down: async (queryInterface, Sequelize) => { + // Add back the columns to the users table + await queryInterface.addColumn('users', 'last_logged_in_at', { + type: Sequelize.DATE, + }) + await queryInterface.addColumn('users', 'refresh_tokens', { + type: Sequelize.ARRAY(Sequelize.JSONB), + }) + }, +} diff --git a/src/database/models/users.js b/src/database/models/users.js index 0f110e0de..aea7ed740 100644 --- a/src/database/models/users.js +++ b/src/database/models/users.js @@ -35,12 +35,10 @@ module.exports = (sequelize, DataTypes) => { defaultValue: 'ACTIVE', }, image: DataTypes.STRING, - last_logged_in_at: DataTypes.DATE, has_accepted_terms_and_conditions: { type: DataTypes.BOOLEAN, defaultValue: false, }, - refresh_tokens: DataTypes.ARRAY(DataTypes.JSONB), languages: DataTypes.ARRAY(DataTypes.STRING), preferred_language: { type: DataTypes.STRING, diff --git a/src/services/account.js b/src/services/account.js index b3bcb0aff..2fab75f0b 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -45,7 +45,7 @@ module.exports = class AccountHelper { */ static async create(bodyData, deviceInfo) { - const projection = ['password', 'refresh_tokens'] + const projection = ['password'] try { const plaintextEmailId = bodyData.email.toLowerCase() @@ -287,21 +287,11 @@ module.exports = class AccountHelper { * 1: create redis entry for the session * 2: update user-session with token and refresh_token */ - await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) - - let refresh_token = new Array() - refresh_token.push({ - token: refreshToken, - exp: new Date().getTime() + common.refreshTokenExpiryInMs, - userId: user.id, - }) - - const update = { - refresh_tokens: refresh_token, - last_logged_in_at: new Date().getTime(), - } - - await userQueries.updateUser({ id: user.id, organization_id: user.organization_id }, update) + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) await utilsHelper.redisDel(encryptedEmailId) //make the user as org admin @@ -471,33 +461,7 @@ module.exports = class AccountHelper { common.refreshTokenExpiry ) - let currentToken = { - token: refreshToken, - exp: new Date().getTime() + common.refreshTokenExpiryInMs, - userId: user.id, - } - - let userTokens = user.refresh_tokens ? user.refresh_tokens : [] - let noOfTokensToKeep = common.refreshTokenLimit - 1 - let refreshTokens = [] - - if (userTokens && userTokens.length >= common.refreshTokenLimit) { - refreshTokens = userTokens.splice(-noOfTokensToKeep) - } else { - refreshTokens = userTokens - } - - refreshTokens.push(currentToken) - - const updateParams = { - refresh_tokens: refreshTokens, - last_logged_in_at: new Date().getTime(), - } - - await userQueries.updateUser({ id: user.id, organization_id: user.organization_id }, updateParams) - delete user.password - delete user.refresh_tokens //Change to let defaultOrg = await organizationQueries.findOne( @@ -529,7 +493,11 @@ module.exports = class AccountHelper { * 1: create redis entry for the session * 2: update user-session with token and refresh_token */ - await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -573,41 +541,8 @@ module.exports = class AccountHelper { userSessions.push(userSessionId) } - let tokenToRemove = [] - if (userSessions.length > 0) { - const userSessionData = await userSessionsService.findUserSession( - { - id: userSessions, - }, - { - attributes: ['refresh_token'], - } - ) - tokenToRemove = userSessionData.map(({ refresh_token }) => refresh_token) - } - await userSessionsService.removeUserSessions(userSessions) - let refreshTokens = user.refresh_tokens ? user.refresh_tokens : [] - refreshTokens = tokenToRemove.filter(function (tokenData) { - return !bodyData.refresh_token.includes(tokenData.token) - }) - - /* Destroy refresh token for user */ - const [affectedRows, updatedData] = await userQueries.updateUser( - { id: user.id, organization_id: user.organization_id }, - { refresh_tokens: refreshTokens } - ) - - /* If user doc not updated because of stored token does not matched with bodyData.refreshToken */ - if (affectedRows == 0) { - return responses.failureResponse({ - message: 'INVALID_REFRESH_TOKEN', - statusCode: httpStatusCode.unauthorized, - responseCode: 'UNAUTHORIZED', - }) - } - return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_OUT_SUCCESSFULLY', @@ -649,24 +584,6 @@ module.exports = class AccountHelper { }) } - /* Check valid refresh token stored in db */ - if (!user.refresh_tokens.length) { - return responses.failureResponse({ - message: 'REFRESH_TOKEN_NOT_FOUND', - statusCode: httpStatusCode.unauthorized, - responseCode: 'CLIENT_ERROR', - }) - } - - const token = user.refresh_tokens.find((tokenData) => tokenData.token === bodyData.refresh_token) - if (!token) { - return responses.failureResponse({ - message: 'REFRESH_TOKEN_NOT_FOUND', - statusCode: httpStatusCode.unauthorized, - responseCode: 'CLIENT_ERROR', - }) - } - /* check if redis data is present*/ // Get redis key for session const sessionId = decodedToken.data.session_id.toString() @@ -983,38 +900,17 @@ module.exports = class AccountHelper { }, } - const accessToken = (tokenDetail, process.env.ACCESS_TOKEN_SECRET, common.accessTokenExpiry) + const accessToken = utilsHelper.generateToken( + tokenDetail, + process.env.ACCESS_TOKEN_SECRET, + common.accessTokenExpiry + ) const refreshToken = utilsHelper.generateToken( tokenDetail, process.env.REFRESH_TOKEN_SECRET, common.refreshTokenExpiry ) - let currentToken = { - token: refreshToken, - exp: new Date().getTime() + common.refreshTokenExpiryInMs, - userId: user.id, - } - - let userTokens = user.refresh_tokens ? user.refresh_tokens : [] - let noOfTokensToKeep = common.refreshTokenLimit - 1 - let refreshTokens = [] - - if (userTokens && userTokens.length >= common.refreshTokenLimit) - refreshTokens = userTokens.splice(-noOfTokensToKeep) - else refreshTokens = userTokens - - refreshTokens.push(currentToken) - const updateParams = { - refresh_tokens: refreshTokens, - lastLoggedInAt: new Date().getTime(), - password: bodyData.password, - } - - await userQueries.updateUser( - { id: user.id, organization_id: userCredentials.organization_id }, - updateParams - ) await UserCredentialQueries.updateUser( { email: encryptedEmailId, @@ -1056,7 +952,11 @@ module.exports = class AccountHelper { * 1: create redis entry for the session * 2: update user-session with token and refresh_token */ - await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) const result = { access_token: accessToken, refresh_token: refreshToken, user } return responses.successResponse({ @@ -1452,7 +1352,7 @@ module.exports = class AccountHelper { } bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) - const updateParams = { password: bodyData.newPassword, refresh_tokens: [] } + const updateParams = { password: bodyData.newPassword } await userQueries.updateUser( { id: user.id, organization_id: userCredentials.organization_id }, @@ -1508,53 +1408,4 @@ module.exports = class AccountHelper { throw error } } - - /** - * Update the user session with access token and refresh token, and set the data in Redis. - * @param {number} userSessionId - The ID of the user session to update. - * @param {string} accessToken - The new access token. - * @param {string} refreshToken - The new refresh token. - * @returns {Promise} - A promise that resolves to a success response after updating the user session and setting data in Redis. - * @throws {Error} - Throws an error if the update operation fails. - */ - static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { - try { - // update user-sessions with refresh token and access token - let check = await userSessionsService.updateUserSession( - { - id: userSessionId, - }, - { - token: accessToken, - refresh_token: refreshToken, - } - ) - - // save data in redis against session_id, write a function for this - const redisData = { - accessToken: accessToken, - refreshToken: refreshToken, - } - /** Allowed idle time set to zero (infinity indicator here) - * set TTL of redis to accessTokenExpiry. - * Else it will be there in redis permenantly and will affect listing of user sessions - */ - - let expiryTime = process.env.ALLOWED_IDLE_TIME - if (process.env.ALLOWED_IDLE_TIME == null) { - expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) - } - const redisKey = userSessionId.toString() - const res = await utilsHelper.redisSet(redisKey, redisData, expiryTime) - - const result = {} - return responses.successResponse({ - statusCode: httpStatusCode.ok, - message: 'USER_SESSION_UPDATED_CESSFULLY', - result, - }) - } catch (error) { - throw error - } - } } diff --git a/src/services/admin.js b/src/services/admin.js index b1046864e..8f9a07fd1 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -152,9 +152,10 @@ module.exports = class AdminHelper { * @param {Object} bodyData - user login data. * @param {string} bodyData.email - email. * @param {string} bodyData.password - email. + * @param {string} deviceInformation - device information. * @returns {JSON} - returns login response */ - static async login(bodyData) { + static async login(bodyData, deviceInformation) { try { const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) @@ -203,10 +204,19 @@ module.exports = class AdminHelper { }) } + // create user session entry and add session_id to token data + const userSessionDetails = await userSessionsService.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInformation + ) + const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles: roles, }, @@ -214,8 +224,27 @@ module.exports = class AdminHelper { user.user_roles = roles - const accessToken = utils.generateToken(tokenDetail, process.env.ACCESS_TOKEN_SECRET, '1d') - const refreshToken = utils.generateToken(tokenDetail, process.env.REFRESH_TOKEN_SECRET, '183d') + const accessToken = utils.generateToken( + tokenDetail, + process.env.ACCESS_TOKEN_SECRET, + common.accessTokenExpiry + ) + const refreshToken = utils.generateToken( + tokenDetail, + process.env.REFRESH_TOKEN_SECRET, + common.refreshTokenExpiry + ) + + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) delete user.password const result = { access_token: accessToken, refresh_token: refreshToken, user } diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 38f72d124..eba027e5b 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -260,4 +260,53 @@ module.exports = class UserSessionsHelper { throw unAuthorizedResponse } } + + /** + * Update the user session with access token and refresh token, and set the data in Redis. + * @param {number} userSessionId - The ID of the user session to update. + * @param {string} accessToken - The new access token. + * @param {string} refreshToken - The new refresh token. + * @returns {Promise} - A promise that resolves to a success response after updating the user session and setting data in Redis. + * @throws {Error} - Throws an error if the update operation fails. + */ + static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { + try { + // update user-sessions with refresh token and access token + await this.updateUserSession( + { + id: userSessionId, + }, + { + token: accessToken, + refresh_token: refreshToken, + } + ) + + // save data in redis against session_id, write a function for this + const redisData = { + accessToken: accessToken, + refreshToken: refreshToken, + } + /** Allowed idle time set to zero (infinity indicator here) + * set TTL of redis to accessTokenExpiry. + * Else it will be there in redis permenantly and will affect listing of user sessions + */ + + let expiryTime = process.env.ALLOWED_IDLE_TIME + if (process.env.ALLOWED_IDLE_TIME == null) { + expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) + } + const redisKey = userSessionId.toString() + await utilsHelper.redisSet(redisKey, redisData, expiryTime) + + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_UPDATED_CESSFULLY', + result, + }) + } catch (error) { + throw error + } + } } From 49497e1adafc9a51af9125802fd5ec9aadd03a9f Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 29 Mar 2024 17:31:43 +0530 Subject: [PATCH 55/67] configurable active session check added --- src/.env.sample | 3 +++ src/envVariables.js | 5 +++++ src/locales/en.json | 3 ++- src/services/account.js | 11 +++++++++++ src/services/user-sessions.js | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/.env.sample b/src/.env.sample index 27d48ca61..88f9ff893 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -181,3 +181,6 @@ ALLOWED_IDLE_TIME=300000 # Expiry time for the signed urls SIGNED_URL_EXPIRY_IN_MILLISECONDS = 120000 + +# Allowed active sessions +ALLOWED_ACTIVE_SESSIONS = 5 diff --git a/src/envVariables.js b/src/envVariables.js index bd547ff43..a6c24db4d 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -293,6 +293,11 @@ let enviromentVariables = { optional: true, default: 3600000, }, + ALLOWED_ACTIVE_SESSIONS: { + message: 'Require allowed active sessions', + optional: true, + default: 0, + }, } let success = true diff --git a/src/locales/en.json b/src/locales/en.json index e02111d3d..7eda9ffad 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -122,5 +122,6 @@ "USER_SESSION_VALIDATED_SUCCESSFULLY": "User session validated successfully", "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully", "USER_SESSIONS_REMOVED_SUCCESSFULLY": "User sesions removed successfully", - "PASSWORD_CHANGED_SUCCESSFULLY": "Your password has been changed successfully. Please log-in to continue." + "PASSWORD_CHANGED_SUCCESSFULLY": "Your password has been changed successfully. Please log-in to continue.", + "ACTIVE_SESSION_LIMIT_EXCEEDED": "Sorry! Your allowed active session limi exceeded. Please log-off from other sessions to continue" } diff --git a/src/services/account.js b/src/services/account.js index 2fab75f0b..dc47ac01c 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -404,6 +404,17 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) } + // check if login is allowed or not + if (process.env.ALLOWED_ACTIVE_SESSIONS != null) { + const activeSessionCount = await userSessionsService.activeUserSessionCounts(user.id) + if (activeSessionCount >= process.env.ALLOWED_ACTIVE_SESSIONS) { + return responses.failureResponse({ + message: 'ACTIVE_SESSION_LIMIT_EXCEEDED', + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + }) + } + } let roles = await roleQueries.findAll( { id: user.roles, status: common.ACTIVE_STATUS }, diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index eba027e5b..6b227c6ee 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -309,4 +309,38 @@ module.exports = class UserSessionsHelper { throw error } } + + /** + * Retrieve the count of active user sessions for a given userId. + * @param {number} userId - The ID of the user for which to retrieve active sessions. + * @returns {Promise} - A Promise that resolves to the count of active user sessions. + * @throws {Error} - If an error occurs while retrieving the count of active user sessions. + */ + static async activeUserSessionCounts(userId) { + try { + // Define filter criteria + const filterQuery = { + user_id: userId, + ended_at: null, + } + + // Fetch user sessions based on filter criteria + const userSessions = await userSessionsQueries.findAll(filterQuery) + + // Initialize count of active sessions + let activeSession = 0 + + // Loop through user sessions and check if each session exists in Redis + for (const session of userSessions) { + const id = session.id.toString() + const redisData = await utilsHelper.redisGet(id) + if (redisData !== null) { + activeSession++ + } + } + return activeSession + } catch (error) { + throw error + } + } } From 3663260210e6c617523c24257fec046d85e9a301 Mon Sep 17 00:00:00 2001 From: vishnu Date: Sat, 30 Mar 2024 17:58:50 +0530 Subject: [PATCH 56/67] parse device info --- src/services/user-sessions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 6b227c6ee..d259bb032 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -122,7 +122,7 @@ module.exports = class UserSessionsHelper { if (status === common.ACTIVE_STATUS && statusToSend === common.ACTIVE_STATUS) { const responseObj = { id: session.id, - device_info: session.device_info, + device_info: JSON.parse(session.device_info), status: statusToSend, login_time: session.started_at, logout_time: session.ended_at, @@ -131,7 +131,7 @@ module.exports = class UserSessionsHelper { } else if (status === '') { const responseObj = { id: session.id, - device_info: session.device_info, + device_info: JSON.parse(session.device_info), status: statusToSend, login_time: session.started_at, logout_time: session.ended_at, From ff9266628aabaaca036aa5f58b65edc9118d5b89 Mon Sep 17 00:00:00 2001 From: vishnu Date: Sat, 30 Mar 2024 19:48:23 +0530 Subject: [PATCH 57/67] device info --- src/controllers/v1/account.js | 6 +++--- src/services/user-sessions.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 36069c8d4..071aca529 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -24,7 +24,7 @@ module.exports = class Account { async create(req) { const params = req.body - const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const device_info = req.headers && req.headers['device-info'] ? JSON.parse(req.headers['device-info']) : {} try { const createdAccount = await accountService.create(params, device_info) return createdAccount @@ -46,7 +46,7 @@ module.exports = class Account { async login(req) { const params = req.body - const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const device_info = req.headers && req.headers['device-info'] ? JSON.parse(req.headers['device-info']) : {} try { const loggedInAccount = await accountService.login(params, device_info) return loggedInAccount @@ -131,7 +131,7 @@ module.exports = class Account { async resetPassword(req) { const params = req.body try { - const deviceInfo = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const deviceInfo = req.headers && req.headers['device-info'] ? JSON.parse(req.headers['device-info']) : {} const result = await accountService.resetPassword(params, deviceInfo) return result } catch (error) { diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index d259bb032..6b227c6ee 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -122,7 +122,7 @@ module.exports = class UserSessionsHelper { if (status === common.ACTIVE_STATUS && statusToSend === common.ACTIVE_STATUS) { const responseObj = { id: session.id, - device_info: JSON.parse(session.device_info), + device_info: session.device_info, status: statusToSend, login_time: session.started_at, logout_time: session.ended_at, @@ -131,7 +131,7 @@ module.exports = class UserSessionsHelper { } else if (status === '') { const responseObj = { id: session.id, - device_info: JSON.parse(session.device_info), + device_info: session.device_info, status: statusToSend, login_time: session.started_at, logout_time: session.ended_at, From d75e009b12aef84933599b626bf3c93c268d93dc Mon Sep 17 00:00:00 2001 From: vishnu Date: Mon, 1 Apr 2024 10:25:16 +0530 Subject: [PATCH 58/67] remove unwanted keys --- src/services/admin.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/admin.js b/src/services/admin.js index 8f9a07fd1..42a5276d7 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -596,11 +596,9 @@ function _removeUserKeys() { 'gender', 'about', 'share_link', - 'last_logged_in_at', 'preferred_language', 'location', 'languages', - 'refresh_tokens', 'image', 'roles', ] From fe1b3c322226c3363d92527fec91b0c0ff9245ad Mon Sep 17 00:00:00 2001 From: Nevil Date: Mon, 1 Apr 2024 14:18:50 +0530 Subject: [PATCH 59/67] updated validation regex --- src/validators/v1/organization.js | 6 ++---- src/validators/v1/user.js | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index 9a0bcf838..2f30bd1ee 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -21,8 +21,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') - .not() - .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .matches(/^[a-zA-Z0-9\-.,]+$/) .withMessage('invalid description') req.checkBody('domains').trim().notEmpty().withMessage('domains field is empty') }, @@ -43,8 +42,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') - .not() - .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .matches(/^[a-zA-Z0-9\-.,]+$/) .withMessage('invalid description') }, diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js index a0ceb2c2f..806fa8375 100644 --- a/src/validators/v1/user.js +++ b/src/validators/v1/user.js @@ -38,8 +38,7 @@ module.exports = { .trim() .notEmpty() .withMessage('about field is empty') - .not() - .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .matches(/^[a-zA-Z0-9\-.,]+$/) .withMessage('invalid about') req.checkBody('has_accepted_terms_and_conditions') From dc5d7f68cc758824ab3c413de4c7b044919371b3 Mon Sep 17 00:00:00 2001 From: Nevil Date: Mon, 1 Apr 2024 14:24:31 +0530 Subject: [PATCH 60/67] added whitespace --- src/validators/v1/organization.js | 4 ++-- src/validators/v1/user.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index 2f30bd1ee..dfb6b6f2d 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -21,7 +21,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') - .matches(/^[a-zA-Z0-9\-.,]+$/) + .matches(/^[a-zA-Z0-9\-.,\s]+$/) .withMessage('invalid description') req.checkBody('domains').trim().notEmpty().withMessage('domains field is empty') }, @@ -42,7 +42,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') - .matches(/^[a-zA-Z0-9\-.,]+$/) + .matches(/^[a-zA-Z0-9\-.,\s]+$/) .withMessage('invalid description') }, diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js index 806fa8375..91c49f40b 100644 --- a/src/validators/v1/user.js +++ b/src/validators/v1/user.js @@ -38,7 +38,7 @@ module.exports = { .trim() .notEmpty() .withMessage('about field is empty') - .matches(/^[a-zA-Z0-9\-.,]+$/) + .matches(/^[a-zA-Z0-9\-.,\s]+$/) .withMessage('invalid about') req.checkBody('has_accepted_terms_and_conditions') From 31c9d9dc9775629da29c36bf31f1223a9c87ff40 Mon Sep 17 00:00:00 2001 From: vishnu Date: Mon, 1 Apr 2024 15:17:35 +0530 Subject: [PATCH 61/67] update TTL if idle time is provided --- src/services/user-sessions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 6b227c6ee..64b60a058 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -243,7 +243,7 @@ module.exports = class UserSessionsHelper { } // Renew the TTL if allowed idle is not infinite - if (process.env.ALLOWED_IDLE_TIME == null) { + if (process.env.ALLOWED_IDLE_TIME != null) { await utilsHelper.redisSet(sessionId, redisData, process.env.ALLOWED_IDLE_TIME) } From 71b8ba460de291795acd74e76d852b4ac359a5b8 Mon Sep 17 00:00:00 2001 From: vishnu Date: Mon, 1 Apr 2024 18:28:12 +0530 Subject: [PATCH 62/67] DC issue fix --- ...r-id-primary-key-in-user-sessions-table.js | 27 +++++++++++++++++++ src/database/models/user-sessions.js | 1 + 2 files changed, 28 insertions(+) create mode 100644 src/database/migrations/20240401123752-make-user-id-primary-key-in-user-sessions-table.js diff --git a/src/database/migrations/20240401123752-make-user-id-primary-key-in-user-sessions-table.js b/src/database/migrations/20240401123752-make-user-id-primary-key-in-user-sessions-table.js new file mode 100644 index 000000000..6786b39a9 --- /dev/null +++ b/src/database/migrations/20240401123752-make-user-id-primary-key-in-user-sessions-table.js @@ -0,0 +1,27 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + // Remove the primary key constraint from 'id' + await queryInterface.removeConstraint('user_sessions', 'user_sessions_pkey') + + // Add a unique constraint for the combination of 'id' and 'user_id' + await queryInterface.addConstraint('user_sessions', { + fields: ['id', 'user_id'], + type: 'primary key', + name: 'user_sessions_pkey', + }) + }, + + async down(queryInterface, Sequelize) { + // Remove the unique constraint for the combination of 'id' and 'user_id' + await queryInterface.removeConstraint('user_sessions', 'user_sessions_pkey') + + // Add back the primary key constraint on 'id' + await queryInterface.addConstraint('user_sessions', { + type: 'primary key', + fields: ['id'], + name: 'user_sessions_pkey', + }) + }, +} diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js index ef7d0f957..7b961e137 100644 --- a/src/database/models/user-sessions.js +++ b/src/database/models/user-sessions.js @@ -12,6 +12,7 @@ module.exports = (sequelize, DataTypes) => { user_id: { type: DataTypes.INTEGER, allowNull: false, + primaryKey: true, }, started_at: { type: DataTypes.BIGINT, From f98a1cc5403fa3618231b2abdab32f8f7d0ada5f Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 3 Apr 2024 14:25:28 +0530 Subject: [PATCH 63/67] 1310 fix sending correct error message --- src/locales/en.json | 3 ++- src/middlewares/authenticator.js | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 7eda9ffad..c43bdb5fb 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -123,5 +123,6 @@ "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully", "USER_SESSIONS_REMOVED_SUCCESSFULLY": "User sesions removed successfully", "PASSWORD_CHANGED_SUCCESSFULLY": "Your password has been changed successfully. Please log-in to continue.", - "ACTIVE_SESSION_LIMIT_EXCEEDED": "Sorry! Your allowed active session limi exceeded. Please log-off from other sessions to continue" + "ACTIVE_SESSION_LIMIT_EXCEEDED": "Sorry! Your allowed active session limi exceeded. Please log-off from other sessions to continue", + "USER_SESSION_NOT_FOUND": "User session not found" } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index c80443f22..0515ca050 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -124,7 +124,11 @@ module.exports = async function (req, res, next) { // If data is not in redis, token is invalid if (!redisData || redisData.accessToken !== authHeaderArray[1]) { - throw unAuthorizedResponse + throw responses.failureResponse({ + message: 'USER_SESSION_NOT_FOUND', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) } // Renew the TTL if allowed idle time is greater than zero @@ -132,7 +136,7 @@ module.exports = async function (req, res, next) { await utilsHelper.redisSet(sessionId, redisData, process.env.ALLOWED_IDLE_TIME) } } catch (err) { - if (err.name === 'TokenExpiredError') { + if (err.name === 'TokenExpiredError' || err.message === 'USER_SESSION_NOT_FOUND') { throw responses.failureResponse({ message: 'ACCESS_TOKEN_EXPIRED', statusCode: httpStatusCode.unauthorized, @@ -140,7 +144,6 @@ module.exports = async function (req, res, next) { }) } else throw unAuthorizedResponse } - if (!decodedToken) throw unAuthorizedResponse //check for admin user From ddd4c1f8786d6043629460465a455872e982115d Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Wed, 3 Apr 2024 19:31:18 +0530 Subject: [PATCH 64/67] Captcha and otp security changes --- src/constants/common.js | 2 +- src/services/account.js | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/constants/common.js b/src/constants/common.js index 5170a5d68..29d1935cd 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -91,5 +91,5 @@ module.exports = { DELETED_STATUS: 'DELETED', DEFAULT_ORG_VISIBILITY: 'PUBLIC', ROLE_TYPE_NON_SYSTEM: 0, - captchaEnabledAPIs: ['/user/v1/account/login', '/user/v1/account/create', '/user/v1/account/resetPassword'], + captchaEnabledAPIs: ['/user/v1/account/login', '/user/v1/account/generateOtp', '/user/v1/account/registrationOtp'], } diff --git a/src/services/account.js b/src/services/account.js index ef1d6b84b..4d1de6a92 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -838,6 +838,15 @@ module.exports = class AccountHelper { try { const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) + + const redisData = await utilsHelper.redisGet(encryptedEmailId) + if (!redisData || redisData.otp != bodyData.otp) { + return responses.failureResponse({ + message: 'RESET_OTP_INVALID', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } const userCredentials = await UserCredentialQueries.findOne({ email: encryptedEmailId, password: { @@ -876,14 +885,6 @@ module.exports = class AccountHelper { } user.user_roles = roles - const redisData = await utilsHelper.redisGet(encryptedEmailId) - if (!redisData || redisData.otp != bodyData.otp) { - return responses.failureResponse({ - message: 'RESET_OTP_INVALID', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } const isPasswordSame = bcryptJs.compareSync(bodyData.password, userCredentials.password) if (isPasswordSame) { return responses.failureResponse({ From 67126702ebdafd187c6c9749bd854009401e3c20 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 4 Apr 2024 09:25:35 +0530 Subject: [PATCH 65/67] changes added to keep current session on top of first page --- src/controllers/v1/account.js | 3 ++- src/services/user-sessions.js | 23 +++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 071aca529..7382ea30e 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -284,7 +284,8 @@ module.exports = class Account { req.decodedToken.id, filter, req.pageSize, - req.pageNo + req.pageNo, + req.decodedToken.session_id ) return userSessionDetails } catch (error) { diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 64b60a058..ad9a2bebc 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -84,10 +84,11 @@ module.exports = class UserSessionsHelper { * @param {string} status - The status of the user sessions (e.g., 'ACTIVE', ''). * @param {number} limit - The maximum number of user sessions to retrieve per page. * @param {number} page - The page number for pagination. + * @param {number} currentSessionId - The id of current session. * @returns {Promise} - A promise that resolves to the user session details. */ - static async list(userId, status, limit, page) { + static async list(userId, status, limit, page, currentSessionId) { try { const filter = { user_id: userId, @@ -103,6 +104,7 @@ module.exports = class UserSessionsHelper { const userSessions = await userSessionsQueries.findAll(filter) const activeSessions = [] const inActiveSessions = [] + const currentSession = [] for (const session of userSessions) { const id = session.id.toString() // Convert ID to string const redisData = await utilsHelper.redisGet(id) @@ -127,7 +129,11 @@ module.exports = class UserSessionsHelper { login_time: session.started_at, logout_time: session.ended_at, } - activeSessions.push(responseObj) + if (responseObj.id == currentSessionId) { + currentSession.push(responseObj) + } else { + activeSessions.push(responseObj) + } } else if (status === '') { const responseObj = { id: session.id, @@ -136,13 +142,18 @@ module.exports = class UserSessionsHelper { login_time: session.started_at, logout_time: session.ended_at, } - responseObj.status === common.ACTIVE_STATUS - ? activeSessions.push(responseObj) - : inActiveSessions.push(responseObj) + // get current session data + if (responseObj.id == currentSessionId) { + currentSession.push(responseObj) + } else { + responseObj.status === common.ACTIVE_STATUS + ? activeSessions.push(responseObj) + : inActiveSessions.push(responseObj) + } } } - const result = [...activeSessions, ...inActiveSessions] + const result = [...currentSession, ...activeSessions, ...inActiveSessions] // Paginate the result array // The response is accumulated from two places. db and redis. So pagination is not possible on the fly From ef218ce7e1521b0512c3483d5db8cb39ecc4a8d4 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 4 Apr 2024 10:50:32 +0530 Subject: [PATCH 66/67] add period filter --- src/controllers/v1/account.js | 3 ++- src/services/user-sessions.js | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 7382ea30e..b49cdc363 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -285,7 +285,8 @@ module.exports = class Account { filter, req.pageSize, req.pageNo, - req.decodedToken.session_id + req.decodedToken.session_id, + req.query && req.query.period ? req.query.period : '' ) return userSessionDetails } catch (error) { diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index ad9a2bebc..7f00468fd 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -12,6 +12,8 @@ const httpStatusCode = require('@generics/http-status') const responses = require('@helpers/responses') const common = require('@constants/common') const jwt = require('jsonwebtoken') +const moment = require('moment') +const { Op } = require('sequelize') // create user-session module.exports = class UserSessionsHelper { @@ -88,7 +90,7 @@ module.exports = class UserSessionsHelper { * @returns {Promise} - A promise that resolves to the user session details. */ - static async list(userId, status, limit, page, currentSessionId) { + static async list(userId, status, limit, page, currentSessionId, period = '') { try { const filter = { user_id: userId, @@ -100,6 +102,14 @@ module.exports = class UserSessionsHelper { filter.ended_at = null } + // If front end passes a period + if (period != '') { + const periodInSeconds = await utilsHelper.convertDurationToSeconds(period) + const currentTimeEpoch = moment().unix() + const threshold = currentTimeEpoch - periodInSeconds + filter.started_at = { [Op.gte]: threshold } + } + // create userSession const userSessions = await userSessionsQueries.findAll(filter) const activeSessions = [] From 7afcdd865a84126a0a7b3f66ae3eb328e0d545b2 Mon Sep 17 00:00:00 2001 From: Nevil Date: Fri, 5 Apr 2024 14:46:26 +0530 Subject: [PATCH 67/67] added additional validation in /requestOrgRole --- src/validators/v1/organization.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index dfb6b6f2d..87e0c6fbc 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -50,6 +50,20 @@ module.exports = { req.body = filterRequestBody(req.body, organization.requestOrgRole) req.checkBody('role').notEmpty().withMessage('role field is empty') req.checkBody('form_data').notEmpty().withMessage('form_data field is empty') + req.checkBody('form_data.about') + .optional() + .trim() + .notEmpty() + .withMessage('about field is empty') + .matches(/^[a-zA-Z0-9\-.,\s]+$/) + .withMessage('invalid about') + req.checkBody('form_data.experience') + .optional() + .trim() + .notEmpty() + .withMessage('form_data.experience field is empty') + .isNumeric() + .withMessage('invalid form_data.experience') }, read: (req) => {