diff --git a/interface-routes/elevate-mentoring.json b/interface-routes/elevate-mentoring.json index afa1ce97e..4ea883e12 100644 --- a/interface-routes/elevate-mentoring.json +++ b/interface-routes/elevate-mentoring.json @@ -1367,6 +1367,32 @@ } ] }, + { + "sourceRoute": "/mentoring/v1/profile/getCommunicationToken", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, + { + "sourceRoute": "/mentoring/v1/profile/logout", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, { "sourceRoute": "/mentoring/v1/notification/template", "type": "POST", @@ -2133,44 +2159,122 @@ ] }, { - "sourceRoute": "/interface/v1/mentors/details/:id", - "type": "GET", - "priority": "MUST_HAVE", - "inSequence": true, - "orchestrated": true, - "responseMessage": "Profile fetched successfully.", - "targetPackages": [ - { - "basePackageName": "mentoring", - "packageName": "elevate-mentoring", - "targetBody": [], - "responseBody": [ + "sourceRoute": "/mentoring/v1/connections/initiate", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, + { + "sourceRoute": "/mentoring/v1/connections/pending", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, + { + "sourceRoute": "/mentoring/v1/connections/accept", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, + { + "sourceRoute": "/mentoring/v1/connections/reject", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, + { + "sourceRoute": "/mentoring/v1/connections/getInfo", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, + { + "sourceRoute": "/mentoring/v1/connections/list", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring" + } + ] + }, + { + "sourceRoute": "/interface/v1/mentors/details/:id", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": true, + "orchestrated": true, + "responseMessage": "Profile fetched successfully.", + "targetPackages": [ + { + "basePackageName": "mentoring", + "packageName": "elevate-mentoring", + "targetBody": [], + "responseBody": [ { "sourceField": "user_roles", "targetField": "mentoring.user_roles[]" } ] - }, - { - "basePackageName": "user", - "packageName": "elevate-user", - "targetBody": [], - "responseBody": [] - } - ] - }, - { - "sourceRoute": "/interface/v1/profile/read", - "type": "GET", - "priority": "MUST_HAVE", - "inSequence": true, - "orchestrated": true, - "responseMessage": "Profile fetched successfully.", - "targetPackages": [ - { - "basePackageName": "user", - "packageName": "elevate-user", - "targetBody": [], + }, + { + "basePackageName": "user", + "packageName": "elevate-user", + "targetBody": [], + "responseBody": [] + } + ] + }, + { + "sourceRoute": "/interface/v1/profile/read", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": true, + "orchestrated": true, + "responseMessage": "Profile fetched successfully.", + "targetPackages": [ + { + "basePackageName": "user", + "packageName": "elevate-user", + "targetBody": [], "responseBody": [ { "sourceField": "permissions", @@ -2181,11 +2285,11 @@ "targetField": "user_roles[]" } ] - }, + }, { - "basePackageName": "mentoring", - "packageName": "elevate-mentoring", - "targetBody": [], + "basePackageName": "mentoring", + "packageName": "elevate-mentoring", + "targetBody": [], "responseBody": [ { "sourceField": "permissions", @@ -2196,8 +2300,8 @@ "targetField": "mentoring.user_roles[]" } ] - } - ] - } + } + ] + } ] } diff --git a/src/.env.sample b/src/.env.sample index 45e8b86fb..9c30db19e 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -168,3 +168,12 @@ ADMIN_TOKEN_HEADER_NAME='admin-auth-token' # The header name used to pass the organization ID in HTTP requests. This helps identify the organization associated with the request. ORG_ID_HEADER_NAME='organization-id' + +# Flag to enable/disable chat chapabilities +ENABLE_CHAT=true + +#If chat is enables +COMMUNICATION_SERVICE_HOST=http://localhost:3123 + +# Base URL for the Communication Service +COMMUNICATION_SERVICE_BASE_URL='/communications' diff --git a/src/api-doc/MentorED-Mentoring.postman_collection.json b/src/api-doc/MentorED-Mentoring.postman_collection.json index e9f5d51a4..824a03c41 100644 --- a/src/api-doc/MentorED-Mentoring.postman_collection.json +++ b/src/api-doc/MentorED-Mentoring.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "a0142733-af4d-4ff9-bbd7-7afd375fc495", + "_postman_id": "a27653df-0e4d-46c7-9b5d-f3bd56458dbf", "name": "MentorED-Mentoring", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "21498549" @@ -2187,6 +2187,25 @@ } }, "response": [] + }, + { + "name": "User details", + "request": { + "method": "GET", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/profile/details/34", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "profile", "details", "34"] + } + }, + "response": [] } ] }, @@ -4507,6 +4526,205 @@ ] } ] + }, + { + "name": "Connections", + "item": [ + { + "name": "Send Request", + "request": { + "method": "POST", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"user_id\": \"2\",\n \"message\": \"Hey i would like to connect with you.\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/connections/initiate", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "connections", "initiate"] + }, + "description": "### Create Default Rule\n\nThis endpoint allows the creation of a default rule for mentoring.\n\n#### Request Body\n\n- `type` (string): The type of the rule.\n- `target_field` (string): The target field for the rule.\n- `is_target_from_sessions_mentor` (boolean): Indicates if the target is from sessions mentor.\n- `requester_field` (string): The requester field for the rule.\n- `field_configs` (null): Field configurations (if any).\n- `matching_operator` (string): The matching operator for the rule.\n- `requester_roles` (array of strings): The roles of the requester.\n- `role_config.exclude` (boolean): Indicates if the role should be excluded.\n \n\n#### Response\n\nThe response is in JSON format with the following schema:\n\n``` json\n{\n \"type\": \"object\",\n \"properties\": {\n \"responseCode\": {\"type\": \"string\"},\n \"message\": {\"type\": \"string\"},\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"created_at\": {\"type\": \"string\"},\n \"updated_at\": {\"type\": \"string\"},\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"target_field\": {\"type\": \"string\"},\n \"is_target_from_sessions_mentor\": {\"type\": \"boolean\"},\n \"requester_field\": {\"type\": \"string\"},\n \"field_configs\": {\"type\": \"null\"},\n \"matching_operator\": {\"type\": \"string\"},\n \"requester_roles\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"}\n },\n \"role_config\": {\n \"type\": \"object\",\n \"properties\": {\n \"exclude\": {\"type\": \"boolean\"}\n }\n },\n \"created_by\": {\"type\": \"integer\"},\n \"updated_by\": {\"type\": \"integer\"},\n \"organization_id\": {\"type\": \"integer\"},\n \"deleted_at\": {\"type\": [\"string\", \"null\"]}\n }\n },\n \"meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"formsVersion\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"version\": {\"type\": \"integer\"}\n }\n }\n },\n \"correlation\": {\"type\": \"string\"},\n \"meetingPlatform\": {\"type\": \"string\"}\n }\n }\n }\n}\n\n ```" + }, + "response": [] + }, + { + "name": "Pending Requests", + "request": { + "method": "GET", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/connections/pending?page=1&limit=100", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "connections", "pending"], + "query": [ + { + "key": "page", + "value": "1", + "description": "Default: 1" + }, + { + "key": "limit", + "value": "100", + "description": "Default: 100" + } + ] + }, + "description": "### Create Default Rule\n\nThis endpoint allows the creation of a default rule for mentoring.\n\n#### Request Body\n\n- `type` (string): The type of the rule.\n- `target_field` (string): The target field for the rule.\n- `is_target_from_sessions_mentor` (boolean): Indicates if the target is from sessions mentor.\n- `requester_field` (string): The requester field for the rule.\n- `field_configs` (null): Field configurations (if any).\n- `matching_operator` (string): The matching operator for the rule.\n- `requester_roles` (array of strings): The roles of the requester.\n- `role_config.exclude` (boolean): Indicates if the role should be excluded.\n \n\n#### Response\n\nThe response is in JSON format with the following schema:\n\n``` json\n{\n \"type\": \"object\",\n \"properties\": {\n \"responseCode\": {\"type\": \"string\"},\n \"message\": {\"type\": \"string\"},\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"created_at\": {\"type\": \"string\"},\n \"updated_at\": {\"type\": \"string\"},\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"target_field\": {\"type\": \"string\"},\n \"is_target_from_sessions_mentor\": {\"type\": \"boolean\"},\n \"requester_field\": {\"type\": \"string\"},\n \"field_configs\": {\"type\": \"null\"},\n \"matching_operator\": {\"type\": \"string\"},\n \"requester_roles\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"}\n },\n \"role_config\": {\n \"type\": \"object\",\n \"properties\": {\n \"exclude\": {\"type\": \"boolean\"}\n }\n },\n \"created_by\": {\"type\": \"integer\"},\n \"updated_by\": {\"type\": \"integer\"},\n \"organization_id\": {\"type\": \"integer\"},\n \"deleted_at\": {\"type\": [\"string\", \"null\"]}\n }\n },\n \"meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"formsVersion\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"version\": {\"type\": \"integer\"}\n }\n }\n },\n \"correlation\": {\"type\": \"string\"},\n \"meetingPlatform\": {\"type\": \"string\"}\n }\n }\n }\n}\n\n ```" + }, + "response": [] + }, + { + "name": "Connected users", + "request": { + "method": "GET", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/connections/list?page=1&limit=20", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "connections", "list"], + "query": [ + { + "key": "page", + "value": "1", + "description": "Default: 1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "search", + "value": "YXNhc2Q=", + "disabled": true + }, + { + "key": "organization_ids", + "value": "1,2", + "disabled": true + }, + { + "key": "area_of_expertise", + "value": "educational_leadership", + "disabled": true + } + ] + }, + "description": "### Create Default Rule\n\nThis endpoint allows the creation of a default rule for mentoring.\n\n#### Request Body\n\n- `type` (string): The type of the rule.\n- `target_field` (string): The target field for the rule.\n- `is_target_from_sessions_mentor` (boolean): Indicates if the target is from sessions mentor.\n- `requester_field` (string): The requester field for the rule.\n- `field_configs` (null): Field configurations (if any).\n- `matching_operator` (string): The matching operator for the rule.\n- `requester_roles` (array of strings): The roles of the requester.\n- `role_config.exclude` (boolean): Indicates if the role should be excluded.\n \n\n#### Response\n\nThe response is in JSON format with the following schema:\n\n``` json\n{\n \"type\": \"object\",\n \"properties\": {\n \"responseCode\": {\"type\": \"string\"},\n \"message\": {\"type\": \"string\"},\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"created_at\": {\"type\": \"string\"},\n \"updated_at\": {\"type\": \"string\"},\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"target_field\": {\"type\": \"string\"},\n \"is_target_from_sessions_mentor\": {\"type\": \"boolean\"},\n \"requester_field\": {\"type\": \"string\"},\n \"field_configs\": {\"type\": \"null\"},\n \"matching_operator\": {\"type\": \"string\"},\n \"requester_roles\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"}\n },\n \"role_config\": {\n \"type\": \"object\",\n \"properties\": {\n \"exclude\": {\"type\": \"boolean\"}\n }\n },\n \"created_by\": {\"type\": \"integer\"},\n \"updated_by\": {\"type\": \"integer\"},\n \"organization_id\": {\"type\": \"integer\"},\n \"deleted_at\": {\"type\": [\"string\", \"null\"]}\n }\n },\n \"meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"formsVersion\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"version\": {\"type\": \"integer\"}\n }\n }\n },\n \"correlation\": {\"type\": \"string\"},\n \"meetingPlatform\": {\"type\": \"string\"}\n }\n }\n }\n}\n\n ```" + }, + "response": [] + }, + { + "name": "Get Connection Info", + "request": { + "method": "POST", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"user_id\": \"34\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/connections/getInfo", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "connections", "getInfo"] + }, + "description": "### Create Default Rule\n\nThis endpoint allows the creation of a default rule for mentoring.\n\n#### Request Body\n\n- `type` (string): The type of the rule.\n- `target_field` (string): The target field for the rule.\n- `is_target_from_sessions_mentor` (boolean): Indicates if the target is from sessions mentor.\n- `requester_field` (string): The requester field for the rule.\n- `field_configs` (null): Field configurations (if any).\n- `matching_operator` (string): The matching operator for the rule.\n- `requester_roles` (array of strings): The roles of the requester.\n- `role_config.exclude` (boolean): Indicates if the role should be excluded.\n \n\n#### Response\n\nThe response is in JSON format with the following schema:\n\n``` json\n{\n \"type\": \"object\",\n \"properties\": {\n \"responseCode\": {\"type\": \"string\"},\n \"message\": {\"type\": \"string\"},\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"created_at\": {\"type\": \"string\"},\n \"updated_at\": {\"type\": \"string\"},\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"target_field\": {\"type\": \"string\"},\n \"is_target_from_sessions_mentor\": {\"type\": \"boolean\"},\n \"requester_field\": {\"type\": \"string\"},\n \"field_configs\": {\"type\": \"null\"},\n \"matching_operator\": {\"type\": \"string\"},\n \"requester_roles\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"}\n },\n \"role_config\": {\n \"type\": \"object\",\n \"properties\": {\n \"exclude\": {\"type\": \"boolean\"}\n }\n },\n \"created_by\": {\"type\": \"integer\"},\n \"updated_by\": {\"type\": \"integer\"},\n \"organization_id\": {\"type\": \"integer\"},\n \"deleted_at\": {\"type\": [\"string\", \"null\"]}\n }\n },\n \"meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"formsVersion\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"version\": {\"type\": \"integer\"}\n }\n }\n },\n \"correlation\": {\"type\": \"string\"},\n \"meetingPlatform\": {\"type\": \"string\"}\n }\n }\n }\n}\n\n ```" + }, + "response": [] + }, + { + "name": "Accept Request", + "request": { + "method": "POST", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"user_id\": \"1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/connections/accept", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "connections", "accept"] + }, + "description": "### Create Default Rule\n\nThis endpoint allows the creation of a default rule for mentoring.\n\n#### Request Body\n\n- `type` (string): The type of the rule.\n- `target_field` (string): The target field for the rule.\n- `is_target_from_sessions_mentor` (boolean): Indicates if the target is from sessions mentor.\n- `requester_field` (string): The requester field for the rule.\n- `field_configs` (null): Field configurations (if any).\n- `matching_operator` (string): The matching operator for the rule.\n- `requester_roles` (array of strings): The roles of the requester.\n- `role_config.exclude` (boolean): Indicates if the role should be excluded.\n \n\n#### Response\n\nThe response is in JSON format with the following schema:\n\n``` json\n{\n \"type\": \"object\",\n \"properties\": {\n \"responseCode\": {\"type\": \"string\"},\n \"message\": {\"type\": \"string\"},\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"created_at\": {\"type\": \"string\"},\n \"updated_at\": {\"type\": \"string\"},\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"target_field\": {\"type\": \"string\"},\n \"is_target_from_sessions_mentor\": {\"type\": \"boolean\"},\n \"requester_field\": {\"type\": \"string\"},\n \"field_configs\": {\"type\": \"null\"},\n \"matching_operator\": {\"type\": \"string\"},\n \"requester_roles\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"}\n },\n \"role_config\": {\n \"type\": \"object\",\n \"properties\": {\n \"exclude\": {\"type\": \"boolean\"}\n }\n },\n \"created_by\": {\"type\": \"integer\"},\n \"updated_by\": {\"type\": \"integer\"},\n \"organization_id\": {\"type\": \"integer\"},\n \"deleted_at\": {\"type\": [\"string\", \"null\"]}\n }\n },\n \"meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"formsVersion\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"version\": {\"type\": \"integer\"}\n }\n }\n },\n \"correlation\": {\"type\": \"string\"},\n \"meetingPlatform\": {\"type\": \"string\"}\n }\n }\n }\n}\n\n ```" + }, + "response": [] + }, + { + "name": "Reject Request", + "request": { + "method": "POST", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"user_id\": \"1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/connections/reject", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "connections", "reject"] + }, + "description": "### Create Default Rule\n\nThis endpoint allows the creation of a default rule for mentoring.\n\n#### Request Body\n\n- `type` (string): The type of the rule.\n- `target_field` (string): The target field for the rule.\n- `is_target_from_sessions_mentor` (boolean): Indicates if the target is from sessions mentor.\n- `requester_field` (string): The requester field for the rule.\n- `field_configs` (null): Field configurations (if any).\n- `matching_operator` (string): The matching operator for the rule.\n- `requester_roles` (array of strings): The roles of the requester.\n- `role_config.exclude` (boolean): Indicates if the role should be excluded.\n \n\n#### Response\n\nThe response is in JSON format with the following schema:\n\n``` json\n{\n \"type\": \"object\",\n \"properties\": {\n \"responseCode\": {\"type\": \"string\"},\n \"message\": {\"type\": \"string\"},\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"created_at\": {\"type\": \"string\"},\n \"updated_at\": {\"type\": \"string\"},\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"target_field\": {\"type\": \"string\"},\n \"is_target_from_sessions_mentor\": {\"type\": \"boolean\"},\n \"requester_field\": {\"type\": \"string\"},\n \"field_configs\": {\"type\": \"null\"},\n \"matching_operator\": {\"type\": \"string\"},\n \"requester_roles\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"}\n },\n \"role_config\": {\n \"type\": \"object\",\n \"properties\": {\n \"exclude\": {\"type\": \"boolean\"}\n }\n },\n \"created_by\": {\"type\": \"integer\"},\n \"updated_by\": {\"type\": \"integer\"},\n \"organization_id\": {\"type\": \"integer\"},\n \"deleted_at\": {\"type\": [\"string\", \"null\"]}\n }\n },\n \"meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"formsVersion\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\"type\": \"integer\"},\n \"type\": {\"type\": \"string\"},\n \"version\": {\"type\": \"integer\"}\n }\n }\n },\n \"correlation\": {\"type\": \"string\"},\n \"meetingPlatform\": {\"type\": \"string\"}\n }\n }\n }\n}\n\n ```" + }, + "response": [] + } + ] } ] } diff --git a/src/api-doc/api-doc.yaml b/src/api-doc/api-doc.yaml index 42ca15662..3d73648de 100644 --- a/src/api-doc/api-doc.yaml +++ b/src/api-doc/api-doc.yaml @@ -4826,7 +4826,7 @@ paths: /mentoring/v1/profile/create: post: summary: Create details - tags: + tags: &ref_13 - Profile description: |- This API is associated with the profile API @@ -4834,7 +4834,8 @@ paths: - It is mandatory to provide values for parameters marked with `required` - Mandatory parameter cannot be empty or null parameters: - - name: x-auth-token + - &ref_14 + name: x-auth-token required: true in: header description: >- @@ -5009,7 +5010,7 @@ paths: formsVersion: [] correlation: 4bc32289-a646-40c5-8229-adfa3a92faa6 meeting_platform: BBB - '400': + '400': &ref_15 description: Bad Request. Mentor Not Found content: application.json: @@ -5103,7 +5104,7 @@ paths: configs: notification: true visibility: public - parameters: [] + parameters: &ref_16 [] /mentoring/v1/profile/update: post: summary: Update details @@ -9379,142 +9380,1212 @@ paths: meta: correlation: 0e57651e-4f49-4b9c-b264-6c1b3107c904 message: The default rule was not found. -components: - schemas: - form: - createFormRequest: - type: object - required: - - type - - subType - - action - - data - properties: - type: - type: string - example: profile - subType: - type: string - example: profileForm - action: - type: string - example: profileFields - data: - type: object - properties: - templateName: - type: string - example: defaultTemplate - fields: + /mentoring/v1/connections/initiate: + post: + summary: Initiate connection with a user + description: '' + operationId: '' + tags: &ref_8 + - Connections + parameters: [] + responses: + '200': + description: OK + content: + application/json: + schema: type: object properties: - controls: - type: array - items: - type: object - properties: - name: - type: string - example: name - label: - type: string - example: name - value: - type: string - example: '' - class: - type: string - example: ion-margin - type: - type: string - example: text - position: - type: string - example: floating - validators: + responseCode: + type: string + message: + type: string + result: + type: object + properties: + created_at: + type: string + updated_at: + type: string + id: + type: number + user_id: + type: string + friend_id: + type: string + status: + type: string + created_by: + type: string + updated_by: + type: string + meta: + type: object + properties: + message: + type: string + deleted_at: + type: 'null' + meta: + type: object + properties: + formsVersion: + type: array + items: type: object properties: - required: - type: boolean - example: true - minLength: + id: type: number - example: 10 - createForm200Response: - description: Created - type: object - properties: - responseCode: - type: string - example: OK - message: - type: string - example: Form created successfully - result: - type: array - example: [] - createForm400Response: - description: Bad Request. Form Already Exist - type: object - properties: - responseCode: - type: string - example: CLIENT_ERROR - message: - type: string - example: Form already exists - error: - type: array - items: - type: string - example: [] - updateFormRequest: - type: object - required: - - type - - subType - - action - - data - properties: - type: - type: string - example: profile - subType: - type: string - example: profileForm - action: - type: string - example: profileFields - data: - type: object - properties: - templateName: - type: string - example: defaultTemplate - fields: + type: + type: string + version: + type: number + required: + - id + - type + - version + correlation: + type: string + meetingPlatform: + type: string + examples: + Connection initiated: + value: + responseCode: OK + message: Your connection request has been sent successfully! + result: + created_at: '2024-12-05T09:22:55.298Z' + updated_at: '2024-12-05T09:22:55.298Z' + id: 19 + user_id: '3' + friend_id: '35' + status: REQUESTED + created_by: '3' + updated_by: '3' + meta: + message: Hey i would like to connect with you. + deleted_at: null + meta: + formsVersion: + - id: 4 + type: termsAndConditions + version: 0 + - id: 6 + type: helpVideos + version: 0 + - id: 7 + type: platformApp + version: 0 + - id: 8 + type: helpApp + version: 0 + - id: 9 + type: sampleCsvDownload + version: 0 + - id: 1 + type: session + version: 1 + - id: 2 + type: editProfile + version: 0 + - id: 10 + type: mentorQuestionnaire + version: 0 + - id: 5 + type: faq + version: 1 + - id: 11 + type: termsAndConditions + version: 1 + - id: 3 + type: session + version: 3 + - id: 12 + type: test + version: 0 + correlation: e368b950-b475-4d9d-a73d-58d02f4e5fee + meetingPlatform: BBB + requestBody: + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + message: + type: string + examples: + Initiate Connection: + value: + user_id: '2' + message: Hey i would like to connect with you. + /mentoring/v1/connections/pending: + get: + summary: Get all pending requests + description: '' + operationId: '' + tags: *ref_8 + parameters: &ref_10 + - in: query + name: page + description: '' + schema: &ref_9 + type: number + - in: query + name: limit + description: '' + schema: *ref_9 + responses: + '200': + description: OK + content: + application/json: + schema: type: object properties: - controls: - type: array - items: - type: object - properties: - name: - type: string - example: name - label: - type: string - example: name - value: - type: string - example: '' - class: - type: string - example: ion-margin - type: - type: string - example: text - position: + responseCode: + type: string + message: + type: string + result: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: number + user_id: + type: string + friend_id: + type: string + status: + type: string + meta: + type: object + properties: + message: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: 'null' + updated_by: + type: string + created_by: + type: string + user_details: + type: object + properties: + name: + type: string + user_id: + type: string + mentee_visibility: + type: string + organization_id: + type: string + designation: + type: 'null' + area_of_expertise: + type: array + items: + type: object + properties: + value: + type: string + label: + type: string + required: + - value + - label + education_qualification: + type: string + experience: + type: 'null' + is_mentor: + type: boolean + image: + type: string + communications_user_id: + type: string + count: + type: number + meta: + type: object + properties: + formsVersion: + type: array + items: + type: object + properties: + id: + type: number + type: + type: string + version: + type: number + required: + - id + - type + - version + correlation: + type: string + meetingPlatform: + type: string + examples: + Pending requests: + value: + responseCode: OK + message: Successfully retrieved your connection list! + result: + data: + - id: 19 + user_id: '3' + friend_id: '35' + status: REQUESTED + meta: + message: Hey i would like to connect with you. + created_at: '2024-12-05T09:22:55.298Z' + updated_at: '2024-12-05T09:22:55.298Z' + deleted_at: null + updated_by: '3' + created_by: '3' + user_details: + name: 'Nika ' + user_id: '35' + mentee_visibility: CURRENT + organization_id: '1' + designation: null + area_of_expertise: + - value: educational_leadership + label: Educational leadership + - value: sqaa + label: SQAA + education_qualification: MBA + experience: null + is_mentor: false + image: >- + https://storage.googleapis.com/mentoring-dev-storage-private/users/266-1733229493508-logo_3_photoroom_png?GoogleAccessId=sl-mentoring-dev-storage%40sl-dev-project.iam.gserviceaccount.com&Expires=1733445969&Signature=t4pmaTMz2fmLynvelWUJHWD54ZEdhGBiQ6uFbtzxz1Z49mLGf48q4gKFW7l%2BNI4tTWAZxB9ZA3r6ZkahkEpT99FV1VQBdSvkTNJManyz%2FE62ybhQb9%2FF3U5474xICciXDBUoA%2Bipv8Bt50j9KtvHzj1nvYBILvkR7QH%2BycGXp%2FZCDcABcaKXWVO8iwgjBVtkWuD%2Bpjwf2%2BOAy2kAR4FtCaM8WivI21%2Bnqn7byMm69AcJ7vODWQWwZU7BPlR384XAAjK4Su7XP3b7gz8kYpvLdCGHVVH%2BEpUFuwlZx4I4mHvPF2IoiHEFPmrZdj7zBf06BFmYe%2B%2BheQubIgCRB7uo9g%3D%3D + communications_user_id: z5t2YLpYfAMbyaZ2n + count: 1 + meta: + formsVersion: + - id: 4 + type: termsAndConditions + version: 0 + - id: 6 + type: helpVideos + version: 0 + - id: 7 + type: platformApp + version: 0 + - id: 8 + type: helpApp + version: 0 + - id: 9 + type: sampleCsvDownload + version: 0 + - id: 1 + type: session + version: 1 + - id: 2 + type: editProfile + version: 0 + - id: 10 + type: mentorQuestionnaire + version: 0 + - id: 5 + type: faq + version: 1 + - id: 11 + type: termsAndConditions + version: 1 + - id: 3 + type: session + version: 3 + - id: 12 + type: test + version: 0 + correlation: f0774609-4446-4f26-93da-a54ef0cd96e0 + meetingPlatform: BBB + /mentoring/v1/connections/list: + get: + summary: Get all active connections + description: '' + operationId: '' + tags: *ref_8 + parameters: *ref_10 + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + responseCode: + type: string + message: + type: string + result: + type: object + properties: + data: + type: array + items: + type: object + properties: + name: + type: string + user_id: + type: string + mentee_visibility: + type: string + organization_id: + type: string + designation: + type: 'null' + experience: + type: 'null' + is_mentor: + type: boolean + area_of_expertise: + type: array + items: + type: object + properties: + value: + type: string + label: + type: string + required: + - value + - label + education_qualification: + type: string + image: + type: string + communications_user_id: + type: string + count: + type: number + meta: + type: object + properties: + formsVersion: + type: array + items: + type: object + properties: + id: + type: number + type: + type: string + version: + type: number + required: + - id + - type + - version + correlation: + type: string + meetingPlatform: + type: string + examples: + Connected users: + value: + responseCode: OK + message: Connections retrieved successfully. + result: + data: + - name: 'Nika ' + user_id: '35' + mentee_visibility: CURRENT + organization_id: '1' + designation: null + experience: null + is_mentor: false + area_of_expertise: + - value: educational_leadership + label: Educational leadership + - value: sqaa + label: SQAA + education_qualification: MBA + image: >- + https://storage.googleapis.com/mentoring-dev-storage-private/users/266-1733229493508-logo_3_photoroom_png?GoogleAccessId=sl-mentoring-dev-storage%40sl-dev-project.iam.gserviceaccount.com&Expires=1733446482&Signature=h%2FpjjGBGqU%2FBVNNMmcAv7%2FZ0MdXA7248Z7B8GLysxzA0GBEFRjECvnlTbOhrpKAwpoE7JbNzlyCnX0YnCt8tthBnOpgxgi2MNbwlMg92EiJ7sljh3PDh5%2FUxuS%2F7NU5nt7oL7oJ0gX0xBz4AksicR6xX25BshSbY%2BLv5u9aGqWEsj8vwcNONutuJXkBCbWhFic7KWbQQ90AbuLM5jRfKRgNUQ5xx5SysOeF3VylqpSgLz%2F3evhrXd7sWgibtJwIb5xtQfg7zwvhjFIT9MNx1n2Hsy5AYco4cYCRMgXiV3i2mlPCmfiZDCd1qgjwDdAjEKAdBxOvrPuA%2B5r9SBbS8Gw%3D%3D + communications_user_id: z5t2YLpYfAMbyaZ2n + count: 1 + meta: + formsVersion: + - id: 4 + type: termsAndConditions + version: 0 + - id: 6 + type: helpVideos + version: 0 + - id: 7 + type: platformApp + version: 0 + - id: 8 + type: helpApp + version: 0 + - id: 9 + type: sampleCsvDownload + version: 0 + - id: 1 + type: session + version: 1 + - id: 2 + type: editProfile + version: 0 + - id: 10 + type: mentorQuestionnaire + version: 0 + - id: 5 + type: faq + version: 1 + - id: 11 + type: termsAndConditions + version: 1 + - id: 3 + type: session + version: 3 + - id: 12 + type: test + version: 0 + correlation: 07d7e0ae-3d98-4407-b66f-520d67376a6a + meetingPlatform: BBB + /mentoring/v1/connections/getInfo: + post: + summary: Get connection info + description: '' + operationId: '' + tags: *ref_8 + parameters: &ref_11 [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + responseCode: + type: string + message: + type: string + result: + type: object + properties: + id: + type: number + user_id: + type: string + friend_id: + type: string + status: + type: string + meta: + type: object + properties: + message: + type: string + room_id: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: 'null' + updated_by: + type: string + created_by: + type: string + user_details: + type: object + properties: + name: + type: string + user_id: + type: string + mentee_visibility: + type: string + organization_id: + type: string + designation: + type: 'null' + area_of_expertise: + type: array + items: + type: object + properties: + value: + type: string + label: + type: string + required: + - value + - label + education_qualification: + type: string + is_mentor: + type: boolean + experience: + type: 'null' + image: + type: string + communications_user_id: + type: string + meta: + type: object + properties: + formsVersion: + type: array + items: + type: object + properties: + id: + type: number + type: + type: string + version: + type: number + required: + - id + - type + - version + correlation: + type: string + meetingPlatform: + type: string + examples: + Connection info: + value: + responseCode: OK + message: Connection details fetched successfully. + result: + id: 128 + user_id: '36' + friend_id: '35' + status: ACCEPTED + meta: + message: Hey i would like to connect with you. + room_id: LwtqPenpfQFaw6cC7z5t2YLpYfAMbyaZ2n + created_at: '2024-12-03T14:30:01.483Z' + updated_at: '2024-12-03T14:30:02.674Z' + deleted_at: null + updated_by: '35' + created_by: '36' + user_details: + name: 'Nika ' + user_id: '35' + mentee_visibility: CURRENT + organization_id: '1' + designation: null + area_of_expertise: + - value: educational_leadership + label: Educational leadership + - value: sqaa + label: SQAA + education_qualification: MBA + is_mentor: false + experience: null + image: >- + https://storage.googleapis.com/266-1733229493508-logo_3_photoroom_png + communications_user_id: z5t2YLpYfAMbyaZ2n + meta: + formsVersion: + - id: 4 + type: termsAndConditions + version: 0 + - id: 6 + type: helpVideos + version: 0 + - id: 7 + type: platformApp + version: 0 + - id: 8 + type: helpApp + version: 0 + - id: 9 + type: sampleCsvDownload + version: 0 + - id: 1 + type: session + version: 1 + - id: 2 + type: editProfile + version: 0 + - id: 10 + type: mentorQuestionnaire + version: 0 + - id: 5 + type: faq + version: 1 + - id: 11 + type: termsAndConditions + version: 1 + - id: 3 + type: session + version: 3 + - id: 12 + type: test + version: 0 + correlation: 26c90de8-8b82-4e40-af7e-5efd12539788 + meetingPlatform: BBB + requestBody: &ref_12 + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + examples: + example: + value: + user_id: '35' + /mentoring/v1/connections/reject: + post: + summary: Reject Connection + description: '' + operationId: '' + tags: *ref_8 + parameters: *ref_11 + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + responseCode: + type: string + message: + type: string + result: + type: array + items: + type: string + meta: + type: object + properties: + formsVersion: + type: array + items: + type: object + properties: + id: + type: number + type: + type: string + version: + type: number + required: + - id + - type + - version + correlation: + type: string + meetingPlatform: + type: string + examples: + Reject connection: + value: + responseCode: OK + message: Your connection request has been rejected. + result: [] + meta: + formsVersion: + - id: 4 + type: termsAndConditions + version: 0 + - id: 6 + type: helpVideos + version: 0 + - id: 7 + type: platformApp + version: 0 + - id: 8 + type: helpApp + version: 0 + - id: 9 + type: sampleCsvDownload + version: 0 + - id: 1 + type: session + version: 1 + - id: 2 + type: editProfile + version: 0 + - id: 10 + type: mentorQuestionnaire + version: 0 + - id: 5 + type: faq + version: 1 + - id: 11 + type: termsAndConditions + version: 1 + - id: 3 + type: session + version: 3 + - id: 12 + type: test + version: 0 + correlation: 45c04985-4ae0-4ddb-8047-c9eab7976993 + meetingPlatform: BBB + requestBody: *ref_12 + /mentoring/v1/connections/accept: + post: + summary: Accept connection + description: '' + operationId: '' + tags: *ref_8 + parameters: *ref_11 + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + responseCode: + type: string + message: + type: string + result: + type: object + properties: + id: + type: number + user_id: + type: string + friend_id: + type: string + status: + type: string + meta: + type: object + properties: + message: + type: string + room_id: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: 'null' + updated_by: + type: string + created_by: + type: string + meta: + type: object + properties: + formsVersion: + type: array + items: + type: object + properties: + id: + type: number + type: + type: string + version: + type: number + required: + - id + - type + - version + correlation: + type: string + meetingPlatform: + type: string + examples: + Accept connection: + value: + responseCode: OK + message: Your connection request has been approved. + result: + id: 131 + user_id: '37' + friend_id: '36' + status: ACCEPTED + meta: + message: Hey i would like to connect with you. + room_id: CjHN4R7TLT5Bvjz8uLwtqPenpfQFaw6cC7 + created_at: '2024-12-05T10:01:45.593Z' + updated_at: '2024-12-05T10:01:47.310Z' + deleted_at: null + updated_by: '37' + created_by: '36' + meta: + formsVersion: + - id: 4 + type: termsAndConditions + version: 0 + - id: 6 + type: helpVideos + version: 0 + - id: 7 + type: platformApp + version: 0 + - id: 8 + type: helpApp + version: 0 + - id: 9 + type: sampleCsvDownload + version: 0 + - id: 1 + type: session + version: 1 + - id: 2 + type: editProfile + version: 0 + - id: 10 + type: mentorQuestionnaire + version: 0 + - id: 5 + type: faq + version: 1 + - id: 11 + type: termsAndConditions + version: 1 + - id: 3 + type: session + version: 3 + - id: 12 + type: test + version: 0 + correlation: 32c8a7af-70fb-474a-b115-505686bf9e0b + meetingPlatform: BBB + requestBody: *ref_12 + /mentoring/v1/profile/details: + get: + summary: Get user profile details + tags: *ref_13 + description: |- + This API is associated with the profile API + - Endpoint for listing session `/mentoring/v1/profile/create/` + - It is mandatory to provide values for parameters marked with `required` + - Mandatory parameter cannot be empty or null + parameters: + - *ref_14 + - in: path + name: id + description: ID of the user + schema: + type: string + required: true + responses: + '200': + description: ok + content: + application.json: + schema: + type: object + properties: + responseCode: + type: string + message: + type: string + result: + type: object + properties: + designation: + type: 'null' + area_of_expertise: + type: array + items: + type: object + properties: + value: + type: string + label: + type: string + required: + - value + - label + education_qualification: + type: string + rating: + type: 'null' + stats: + type: 'null' + tags: + type: 'null' + configs: + type: 'null' + external_session_visibility: + type: string + experience: + type: 'null' + organization_id: + type: string + external_mentee_visibility: + type: string + mentee_visibility: + type: string + external_mentor_visibility: + type: string + mentor_visibility: + type: string + is_mentor: + type: boolean + created_at: + type: string + updated_at: + type: string + deleted_at: + type: 'null' + communications_user_id: + type: string + profile_mandatory_fields: + type: array + items: + type: string + meta: + type: object + properties: + formsVersion: + type: array + items: + type: object + properties: + id: + type: number + type: + type: string + version: + type: number + required: + - id + - type + - version + correlation: + type: string + meetingPlatform: + type: string + examples: + example1: + value: + responseCode: OK + message: Profile fetched successfully. + result: + designation: null + area_of_expertise: + - value: educational_leadership + label: Educational leadership + - value: sqaa + label: SQAA + education_qualification: MBA + rating: null + stats: null + tags: null + configs: null + external_session_visibility: CURRENT + experience: null + organization_id: '1' + external_mentee_visibility: CURRENT + mentee_visibility: CURRENT + external_mentor_visibility: CURRENT + mentor_visibility: CURRENT + is_mentor: true + created_at: '2024-12-03T11:28:46.912Z' + updated_at: '2024-12-03T14:48:44.239Z' + deleted_at: null + communications_user_id: LwtqPenpfQFaw6cC7 + profile_mandatory_fields: [] + meta: + formsVersion: + - id: 4 + type: termsAndConditions + version: 0 + - id: 6 + type: helpVideos + version: 0 + - id: 7 + type: platformApp + version: 0 + - id: 8 + type: helpApp + version: 0 + - id: 9 + type: sampleCsvDownload + version: 0 + - id: 1 + type: session + version: 1 + - id: 2 + type: editProfile + version: 0 + - id: 10 + type: mentorQuestionnaire + version: 0 + - id: 5 + type: faq + version: 1 + - id: 11 + type: termsAndConditions + version: 1 + - id: 3 + type: session + version: 3 + - id: 12 + type: test + version: 0 + correlation: 53bc9d00-3b1b-4032-a2e9-7538d7f1a3d6 + meetingPlatform: BBB + '400': *ref_15 + parameters: *ref_16 +components: + schemas: + form: + createFormRequest: + type: object + required: + - type + - subType + - action + - data + properties: + type: + type: string + example: profile + subType: + type: string + example: profileForm + action: + type: string + example: profileFields + data: + type: object + properties: + templateName: + type: string + example: defaultTemplate + fields: + type: object + properties: + controls: + type: array + items: + type: object + properties: + name: + type: string + example: name + label: + type: string + example: name + value: + type: string + example: '' + class: + type: string + example: ion-margin + type: + type: string + example: text + position: + type: string + example: floating + validators: + type: object + properties: + required: + type: boolean + example: true + minLength: + type: number + example: 10 + createForm200Response: + description: Created + type: object + properties: + responseCode: + type: string + example: OK + message: + type: string + example: Form created successfully + result: + type: array + example: [] + createForm400Response: + description: Bad Request. Form Already Exist + type: object + properties: + responseCode: + type: string + example: CLIENT_ERROR + message: + type: string + example: Form already exists + error: + type: array + items: + type: string + example: [] + updateFormRequest: + type: object + required: + - type + - subType + - action + - data + properties: + type: + type: string + example: profile + subType: + type: string + example: profileForm + action: + type: string + example: profileFields + data: + type: object + properties: + templateName: + type: string + example: defaultTemplate + fields: + type: object + properties: + controls: + type: array + items: + type: object + properties: + name: + type: string + example: name + label: + type: string + example: name + value: + type: string + example: '' + class: + type: string + example: ion-margin + type: + type: string + example: text + position: type: string example: floating validators: @@ -12053,3 +13124,8 @@ tags: externalDocs: description: '' url: '' + - name: Connections + description: '' + externalDocs: + description: '' + url: '' diff --git a/src/configs/postgres.js b/src/configs/postgres.js index 5b1336abc..c9f2525c2 100644 --- a/src/configs/postgres.js +++ b/src/configs/postgres.js @@ -1,5 +1,15 @@ +require('module-alias/register') require('dotenv').config() +let environmentData = require('../envVariables')() + +if (!environmentData.success) { + logger.error('Server could not start . Not all environment variable is provided', { + triggerNotification: true, + }) + process.exit() +} + const defaultOrgId = process.env.DEFAULT_ORG_ID.toString() || (() => { diff --git a/src/constants/common.js b/src/constants/common.js index b9ddb9084..629341cd9 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -209,4 +209,14 @@ module.exports = { ], }, FALSE: 'false', + CONNECTIONS_STATUS: { + ACCEPTED: 'ACCEPTED', + REJECTED: 'REJECTED', + PENDING: 'PENDING', + REQUESTED: 'REQUESTED', + BLOCKED: 'BLOCKED', + }, + COMMUNICATION: { + UNAUTHORIZED: 'Unauthorized', + }, } diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index a0b45b85e..5cb24393e 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -18,5 +18,12 @@ module.exports = { REMOVE_SCHEDULED_JOB: 'jobs/remove', // Remove scheduled job endpoint ORGANIZATION_LIST: 'v1/organization/list', VALIDATE_SESSIONS: 'v1/account/validateUserSession', + //Communication apis + COMMUNICATION_SIGNUP: 'v1/communication/signup', + COMMUNICATION_LOGIN: 'v1/communication/login', + COMMUNICATION_LOGOUT: 'v1/communication/logout', + COMMUNICATION_CREATE_CHAT_ROOM: 'v1/communication/createRoom', + COMMUNICATION_UPDATE_AVATAR: 'v1/communication/updateAvatar', + COMMUNICATION_UPDATE_USER: 'v1/communication/updateUser', DOWNLOAD_IMAGE_URL: 'v1/cloud-services/file/getDownloadableUrl', } diff --git a/src/controllers/v1/connections.js b/src/controllers/v1/connections.js new file mode 100644 index 000000000..458b1ac06 --- /dev/null +++ b/src/controllers/v1/connections.js @@ -0,0 +1,118 @@ +const connectionsService = require('@services/connections') + +module.exports = class Connection { + /** + * Get information about a connection between the authenticated user and another user. + * @param {Object} req - The request object. + * @param {Object} req.body - The body of the request. + * @param {string} req.body.user_id - The ID of the user to get connection info for. + * @param {Object} req.decodedToken - The decoded token containing authenticated user info. + * @param {string} req.decodedToken.id - The ID of the authenticated user. + * @returns {Promise} The connection information. + * @throws Will throw an error if the request fails. + */ + async getInfo(req) { + try { + return await connectionsService.getInfo(req.body.user_id, req.decodedToken.id) + } catch (error) { + throw error + } + } + + /** + * Initiate a connection request between the authenticated user and another user. + * @param {Object} req - The request object. + * @param {Object} req.body - The body of the request. + * @param {Object} req.decodedToken - The decoded token containing authenticated user info. + * @param {string} req.decodedToken.id - The ID of the authenticated user. + * @returns {Promise} The response from the connection initiation. + * @throws Will throw an error if the request fails. + */ + async initiate(req) { + try { + return await connectionsService.initiate(req.body, req.decodedToken.id) + } catch (error) { + throw error + } + } + + /** + * Get a list of pending connection requests for the authenticated user. + * @param {Object} req - The request object. + * @param {Object} req.decodedToken - The decoded token containing authenticated user info. + * @param {string} req.decodedToken.id - The ID of the authenticated user. + * @param {number} req.pageNo - The page number for pagination. + * @param {number} req.pageSize - The number of items per page. + * @returns {Promise} The list of pending connection requests. + * @throws Will throw an error if the request fails. + */ + async pending(req) { + try { + return await connectionsService.pending(req.decodedToken.id, req.pageNo, req.pageSize) + } catch (error) { + throw error + } + } + + /** + * Accept a connection request for the authenticated user. + * @param {Object} req - The request object. + * @param {Object} req.body - The body of the request. + * @param {Object} req.decodedToken - The decoded token containing authenticated user info. + * @param {string} req.decodedToken.id - The ID of the authenticated user. + * @returns {Promise} The response from accepting the connection. + * @throws Will throw an error if the request fails. + */ + async accept(req) { + try { + return await connectionsService.accept(req.body, req.decodedToken.id) + } catch (error) { + throw error + } + } + + /** + * Reject a connection request for the authenticated user. + * @param {Object} req - The request object. + * @param {Object} req.body - The body of the request. + * @param {Object} req.decodedToken - The decoded token containing authenticated user info. + * @param {string} req.decodedToken.id - The ID of the authenticated user. + * @returns {Promise} The response from rejecting the connection. + * @throws Will throw an error if the request fails. + */ + async reject(req) { + try { + return await connectionsService.reject(req.body, req.decodedToken.id) + } catch (error) { + throw error + } + } + + /** + * Get a list of connections for the authenticated user. + * @param {Object} req - The request object. + * @param {number} req.pageNo - The page number for pagination. + * @param {number} req.pageSize - The number of items per page. + * @param {string} req.searchText - The search text for filtering connections. + * @param {Object} req.query - Additional query parameters for filtering. + * @param {Object} req.decodedToken - The decoded token containing authenticated user info. + * @param {string} req.decodedToken.id - The ID of the authenticated user. + * @param {string} req.decodedToken.organization_id - The organization ID of the authenticated user. + * @returns {Promise} The list of connections. + * @throws Will throw an error if the request fails. + */ + async list(req) { + try { + return await connectionsService.list( + req.pageNo, + req.pageSize, + req.searchText, + req.query, + req.decodedToken.id, + req.decodedToken.organization_id + ) + } catch (error) { + throw error + } + } +} diff --git a/src/controllers/v1/profile.js b/src/controllers/v1/profile.js index 68cc90222..81088e9e2 100644 --- a/src/controllers/v1/profile.js +++ b/src/controllers/v1/profile.js @@ -124,6 +124,72 @@ module.exports = class Mentees { } } + /** + * Get mentor or mentee extension by user ID. + * @method + * @name getExtension + * @param {Object} req - Request data. + * @param {String} req.params.id - User ID of the user. + * @returns {Promise} - user extension details. + */ + async getCommunicationToken(req) { + try { + return await menteesService.getCommunicationToken(req.decodedToken.id) // params since read will be public for mentees + } catch (error) { + return error + } + } + + /** + * Logs out a mentee by terminating their session. + * + * This function retrieves the mentee's ID from the decoded token in the request + * and calls the `logout` method in `menteesService` to handle session termination. + * Any errors during the process are caught and returned. + * + * @async + * @function logout + * @param {Object} req - The request object containing authentication details. + * @param {Object} req.decodedToken - The decoded token from the authenticated request. + * @param {string} req.decodedToken.id - The ID of the mentee extracted from the decoded token. + * @returns {Promise<*>} Returns a promise that resolves with the result of `menteesService.logout` if successful, + * or the caught error if an error occurs. + */ + async logout(req) { + try { + return await menteesService.logout(req.decodedToken.id) // Params since read will be public for mentees + } catch (error) { + return error + } + } + /** + * Fetches user profile details. + * @method + * @name details + * @param {Object} req - Request object. + * @param {Object} req.params - Route parameters. + * @param {String} req.params.id - The mentor's ID. + * @param {Object} req.decodedToken - Decoded token from authentication. + * @param {String} req.decodedToken.id - The user's ID. + * @param {String} req.decodedToken.organization_id - The user's organization ID. + * @param {Array} req.decodedToken.roles - The user's roles. + * @param {Boolean} isAMentor - Indicates whether the user is a mentor. + * @returns {Promise} - The mentor's profile details. + */ + async details(req) { + try { + return await menteesService.details( + req.params.id, + req.decodedToken.organization_id, + req.decodedToken.id, + isAMentor(req.decodedToken.roles), + req.decodedToken.roles + ) + } catch (error) { + return error + } + } + //To be enabled when delete flow is needed. // /** // * Delete a mentee extension by user ID. diff --git a/src/database/migrations/20241004102326-create-connections-table.js b/src/database/migrations/20241004102326-create-connections-table.js new file mode 100644 index 000000000..3a734658a --- /dev/null +++ b/src/database/migrations/20241004102326-create-connections-table.js @@ -0,0 +1,76 @@ +'use strict' +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('connections', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + user_id: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + friend_id: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + status: { + type: Sequelize.STRING, + allowNull: false, + }, + meta: { + type: Sequelize.JSON, + }, + + updated_by: { + type: Sequelize.STRING, + allowNull: false, + }, + created_by: { + type: Sequelize.STRING, + allowNull: false, + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + }, + deleted_at: { + type: Sequelize.DATE, + allowNull: true, + }, + }) + await queryInterface.addIndex('connections', ['user_id', 'friend_id'], { + unique: true, + name: 'unique_user_id_friend_id_connections', + where: { + deleted_at: null, + }, + }) + await queryInterface.addIndex('connections', ['friend_id'], { + name: 'index_friend_id_connections', + }) + await queryInterface.addIndex('connections', ['status'], { + name: 'index_status_connections', + }) + await queryInterface.addIndex('connections', ['created_by'], { + name: 'index_created_by_connections', + }) + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeIndex('connections', 'index_friend_id_connections') + await queryInterface.removeIndex('connections', 'index_status_connections') + await queryInterface.removeIndex('connections', 'unique_user_id_friend_id_connections') + await queryInterface.removeIndex('connections', 'index_created_by_connections') + + await queryInterface.dropTable('connections') + }, +} diff --git a/src/database/migrations/20241004102426-create-connection-requests-table.js b/src/database/migrations/20241004102426-create-connection-requests-table.js new file mode 100644 index 000000000..8176024c6 --- /dev/null +++ b/src/database/migrations/20241004102426-create-connection-requests-table.js @@ -0,0 +1,77 @@ +'use strict' +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('connection_requests', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + user_id: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + friend_id: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + status: { + type: Sequelize.STRING, + allowNull: false, + }, + meta: { + type: Sequelize.JSON, + }, + + updated_by: { + type: Sequelize.STRING, + allowNull: false, + }, + created_by: { + type: Sequelize.STRING, + allowNull: false, + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + }, + deleted_at: { + type: Sequelize.DATE, + allowNull: true, + }, + }) + await queryInterface.addIndex('connection_requests', ['user_id', 'friend_id'], { + unique: true, + name: 'unique_user_id_friend_id_connection_requests', + where: { + deleted_at: null, + }, + }) + + await queryInterface.addIndex('connection_requests', ['friend_id'], { + name: 'index_friend_id_connection_requests', + }) + await queryInterface.addIndex('connection_requests', ['status'], { + name: 'index_status_connection_requests', + }) + await queryInterface.addIndex('connection_requests', ['created_by'], { + name: 'index_created_by_connection_requests', + }) + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeIndex('connection_requests', 'unique_user_id_friend_id_connection_requests') + await queryInterface.removeIndex('connection_requests', 'index_friend_id_connections') + await queryInterface.removeIndex('connection_requests', 'index_status_connection_requests') + await queryInterface.removeIndex('connection_requests', 'index_created_by_connection_requests') + + await queryInterface.dropTable('connection_requests') + }, +} diff --git a/src/database/migrations/20241014122046-connections_modules.js b/src/database/migrations/20241014122046-connections_modules.js new file mode 100644 index 000000000..004f0f387 --- /dev/null +++ b/src/database/migrations/20241014122046-connections_modules.js @@ -0,0 +1,15 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const modulesData = [{ code: 'connections', status: 'ACTIVE', created_at: new Date(), updated_at: new Date() }] + + // Insert the data into the 'modules' table + await queryInterface.bulkInsert('modules', modulesData) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('modules', null, {}) + }, +} diff --git a/src/database/migrations/20241014122050-connections_permissions.js b/src/database/migrations/20241014122050-connections_permissions.js new file mode 100644 index 000000000..ba0fff340 --- /dev/null +++ b/src/database/migrations/20241014122050-connections_permissions.js @@ -0,0 +1,27 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + try { + const permissionsData = [ + { + code: 'connections_permissions', + module: 'connections', + request_type: ['POST', 'GET'], + api_path: '/mentoring/v1/connections/*', + status: 'ACTIVE', + created_at: new Date(), + updated_at: new Date(), + }, + ] + await queryInterface.bulkInsert('permissions', permissionsData) + } catch (error) { + console.log(error) + } + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('permissions', null, {}) + }, +} diff --git a/src/database/migrations/20241014122054-connections_role_permissions.js b/src/database/migrations/20241014122054-connections_role_permissions.js new file mode 100644 index 000000000..3e10454c2 --- /dev/null +++ b/src/database/migrations/20241014122054-connections_role_permissions.js @@ -0,0 +1,86 @@ +'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 error + } + return permission.id + } catch (error) { + throw error + } +} + +module.exports = { + async up(queryInterface, Sequelize) { + try { + const rolePermissionsData = [ + { + role_title: common.ORG_ADMIN_ROLE, + permission_id: await getPermissionId('connections', ['POST', 'GET'], '/mentoring/v1/connections/*'), + module: 'connections', + request_type: ['POST', 'GET'], + api_path: '/mentoring/v1/connections/*', + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('connections', ['POST', 'GET'], '/mentoring/v1/connections/*'), + module: 'connections', + request_type: ['POST', 'GET'], + api_path: '/mentoring/v1/connections/*', + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + }, + { + role_title: common.MENTOR_ROLE, + permission_id: await getPermissionId('connections', ['POST', 'GET'], '/mentoring/v1/connections/*'), + module: 'connections', + request_type: ['POST', 'GET'], + api_path: '/mentoring/v1/connections/*', + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + }, + { + role_title: common.MENTEE_ROLE, + permission_id: await getPermissionId('connections', ['POST', 'GET'], '/mentoring/v1/connections/*'), + module: 'connections', + request_type: ['POST', 'GET'], + api_path: '/mentoring/v1/connections/*', + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + }, + { + role_title: common.USER_ROLE, + permission_id: await getPermissionId('connections', ['POST', 'GET'], '/mentoring/v1/connections/*'), + module: 'connections', + request_type: ['POST', 'GET'], + api_path: '/mentoring/v1/connections/*', + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + }, + ] + await queryInterface.bulkInsert('role_permission_mapping', rolePermissionsData) + } catch (error) { + console.error(error) + } + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('role_permission_mapping', null, {}) + }, +} diff --git a/src/database/migrations/20241114175723-add-settings-to-user-extension.js b/src/database/migrations/20241114175723-add-settings-to-user-extension.js new file mode 100644 index 000000000..886d9e9bc --- /dev/null +++ b/src/database/migrations/20241114175723-add-settings-to-user-extension.js @@ -0,0 +1,24 @@ +'use strict' + +const defaultChatEnabled = process.env.ENABLE_CHAT === 'true' + +module.exports = { + up: async (queryInterface, Sequelize) => { + // Add the new column without a default value + await queryInterface.addColumn('user_extensions', 'settings', { + type: Sequelize.JSONB, + allowNull: true, + }) + + // Update existing rows to set chat_enabled based on the environment variable + await queryInterface.sequelize.query(` + UPDATE user_extensions + SET settings = JSON_BUILD_OBJECT('chat_enabled', ${defaultChatEnabled}) + WHERE settings IS NULL; + `) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('user_extensions', 'settings') + }, +} diff --git a/src/database/models/connection.js b/src/database/models/connection.js new file mode 100644 index 000000000..b66b5751d --- /dev/null +++ b/src/database/models/connection.js @@ -0,0 +1,57 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const Connection = sequelize.define( + 'Connection', + { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + }, + friend_id: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + }, + status: { + type: DataTypes.STRING, + allowNull: false, + }, + meta: { + type: DataTypes.JSON, + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + deleted_at: { + type: DataTypes.DATE, + }, + updated_by: { + type: DataTypes.STRING, + }, + created_by: { + type: DataTypes.STRING, + }, + }, + { + sequelize, + modelName: 'Connection', + tableName: 'connections', + freezeTableName: true, + paranoid: true, + } + ) + + return Connection +} diff --git a/src/database/models/connectionRequest.js b/src/database/models/connectionRequest.js new file mode 100644 index 000000000..7cbebc05c --- /dev/null +++ b/src/database/models/connectionRequest.js @@ -0,0 +1,57 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const ConnectionRequest = sequelize.define( + 'ConnectionRequest', + { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + }, + friend_id: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + }, + status: { + type: DataTypes.STRING, + allowNull: false, + }, + meta: { + type: DataTypes.JSON, + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + deleted_at: { + type: DataTypes.DATE, + }, + updated_by: { + type: DataTypes.STRING, + }, + created_by: { + type: DataTypes.STRING, + }, + }, + { + sequelize, + modelName: 'ConnectionRequest', + tableName: 'connection_requests', + freezeTableName: true, + paranoid: true, + } + ) + + return ConnectionRequest +} diff --git a/src/database/models/userExtension.js b/src/database/models/userExtension.js index ff414ba3f..147a95aeb 100644 --- a/src/database/models/userExtension.js +++ b/src/database/models/userExtension.js @@ -1,6 +1,8 @@ 'use strict' const Sequelize = require('sequelize') const Op = Sequelize.Op +const defaultChatEnabled = process.env.ENABLE_CHAT === 'true' + module.exports = (sequelize, DataTypes) => { const UserExtension = sequelize.define( 'UserExtension', @@ -80,6 +82,11 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, defaultValue: false, }, + settings: { + type: DataTypes.JSONB, + allowNull: false, + defaultValue: { chat_enabled: defaultChatEnabled }, + }, image: { type: DataTypes.STRING, }, diff --git a/src/database/queries/connection.js b/src/database/queries/connection.js new file mode 100644 index 000000000..0405a290a --- /dev/null +++ b/src/database/queries/connection.js @@ -0,0 +1,382 @@ +'use strict' +const Connection = require('../models/index').Connection +const ConnectionRequest = require('../models/index').ConnectionRequest + +const { Op } = require('sequelize') +const sequelize = require('@database/models/index').sequelize + +const common = require('@constants/common') +const MenteeExtension = require('@database/models/index').UserExtension +const { QueryTypes } = require('sequelize') + +exports.addFriendRequest = async (userId, friendId, message) => { + try { + const result = await sequelize.transaction(async (t) => { + const friendRequestData = [ + { + user_id: userId, + friend_id: friendId, + status: common.CONNECTIONS_STATUS.REQUESTED, + created_by: userId, + updated_by: userId, + meta: { + message, + }, + }, + { + user_id: friendId, + friend_id: userId, + status: common.CONNECTIONS_STATUS.REQUESTED, + created_by: userId, + updated_by: userId, + meta: { + message, + }, + }, + ] + + const requests = await ConnectionRequest.bulkCreate(friendRequestData, { transaction: t }) + + return requests[0].get({ plain: true }) + }) + + return result + } catch (error) { + throw error + } +} + +exports.getPendingRequests = async (userId, page, pageSize) => { + try { + // This will retrieve send and received request + + const result = await ConnectionRequest.findAndCountAll({ + where: { + user_id: userId, + status: common.CONNECTIONS_STATUS.REQUESTED, + }, + raw: true, + limit: pageSize, + offset: (page - 1) * pageSize, + }) + return result + } catch (error) { + throw error + } +} + +exports.getRejectedRequest = async (userId, friendId) => { + try { + const result = await ConnectionRequest.findOne({ + where: { + user_id: userId, + friend_id: friendId, + status: common.CONNECTIONS_STATUS.REJECTED, + created_by: friendId, + }, + paranoid: false, + order: [['deleted_at', 'DESC']], // Order by the deleted_at field in descending order to get the latest + raw: true, + }) + return result + } catch (error) { + console.log(error) + throw error + } +} + +exports.approveRequest = async (userId, friendId, meta) => { + try { + const requests = await sequelize.transaction(async (t) => { + const deletedCount = await ConnectionRequest.destroy({ + where: { + [Op.or]: [ + { user_id: userId, friend_id: friendId }, + { user_id: friendId, friend_id: userId }, + ], + status: common.CONNECTIONS_STATUS.REQUESTED, + created_by: friendId, + }, + individualHooks: true, + transaction: t, + }) + if (deletedCount != 2) { + throw new Error('Error while deleting from "ConnectionRequest"') + } + + const friendRequestData = [ + { + user_id: userId, + friend_id: friendId, + status: common.CONNECTIONS_STATUS.ACCEPTED, + created_by: friendId, + updated_by: userId, + meta, + }, + { + user_id: friendId, + friend_id: userId, + status: common.CONNECTIONS_STATUS.ACCEPTED, + created_by: friendId, + updated_by: userId, + meta, + }, + ] + + const requests = await Connection.bulkCreate(friendRequestData, { + transaction: t, + }) + + return requests + }) + + return requests + } catch (error) { + throw error + } +} + +exports.rejectRequest = async (userId, friendId) => { + try { + const updateData = { + status: common.CONNECTIONS_STATUS.REJECTED, + updated_by: userId, + deleted_at: Date.now(), + } + + return await ConnectionRequest.update(updateData, { + where: { + status: common.CONNECTIONS_STATUS.REQUESTED, + [Op.or]: [ + { user_id: userId, friend_id: friendId }, + { user_id: friendId, friend_id: userId }, + ], + created_by: friendId, + }, + individualHooks: true, + }) + } catch (error) { + throw error + } +} +exports.findOneRequest = async (userId, friendId) => { + try { + const connectionRequest = await ConnectionRequest.findOne({ + where: { + [Op.or]: [ + { user_id: userId, friend_id: friendId }, + { user_id: friendId, friend_id: userId }, + ], + status: common.CONNECTIONS_STATUS.REQUESTED, + created_by: friendId, + }, + raw: true, + }) + + return connectionRequest + } catch (error) { + throw error + } +} + +exports.checkPendingRequest = async (userId, friendId) => { + try { + const result = await ConnectionRequest.findOne({ + where: { + user_id: userId, + friend_id: friendId, + status: common.CONNECTIONS_STATUS.REQUESTED, + }, + raw: true, + }) + return result + } catch (error) { + throw error + } +} + +exports.getSentAndReceivedRequests = async (userId) => { + try { + const result = await Connection.findAll({ + where: { + [Op.or]: [{ user_id: userId }, { friend_id: userId }], + status: common.CONNECTIONS_STATUS.REQUESTED, + }, + raw: true, + }) + return result + } catch (error) { + throw error + } +} + +exports.getConnection = async (userId, friendId) => { + try { + const result = await Connection.findOne({ + where: { + user_id: userId, + friend_id: friendId, + status: { + [Op.or]: [common.CONNECTIONS_STATUS.ACCEPTED, common.CONNECTIONS_STATUS.BLOCKED], + }, + }, + raw: true, + }) + return result + } catch (error) { + throw error + } +} + +exports.getConnectionsByUserIds = async (userId, friendIds, projection) => { + try { + const defaultProjection = ['user_id', 'friend_id'] + + const result = await Connection.findAll({ + where: { + user_id: userId, + friend_id: { + [Op.in]: friendIds, + }, + status: common.CONNECTIONS_STATUS.ACCEPTED, + }, + attributes: projection || defaultProjection, + raw: true, + }) + return result + } catch (error) { + throw error + } +} + +exports.getConnectionsDetails = async ( + page, + limit, + filter, + searchText = '', + userId, + organizationIds = [], + roles = [] +) => { + try { + let additionalFilter = '' + let orgFilter = '' + let filterClause = '' + let rolesFilter = '' + + if (searchText) { + additionalFilter = `AND name ILIKE :search` + } + + if (organizationIds.length > 0) { + orgFilter = `AND organization_id IN (:organizationIds)` + } + + if (filter?.query?.length > 0) { + filterClause = filter.query.startsWith('AND') ? filter.query : 'AND ' + filter.query + } + + // Add the roles filter + if (roles.includes('mentor') && roles.includes('mentee')) { + // Show both mentors and mentees, no additional filter needed + } else if (roles.includes('mentor')) { + rolesFilter = `AND is_mentor = true` + } else if (roles.includes('mentee')) { + rolesFilter = `AND is_mentor = false` + } + + const userFilterClause = `user_id IN (SELECT friend_id FROM ${Connection.tableName} WHERE user_id = :userId)` + + const projectionClause = ` + name, + user_id, + mentee_visibility, + organization_id, + designation, + experience, + is_mentor, + area_of_expertise, + education_qualification, + image, + custom_entity_text::JSONB AS custom_entity_text, + meta::JSONB AS meta + ` + + let query = ` + SELECT ${projectionClause} + FROM ${common.materializedViewsPrefix + MenteeExtension.tableName} + WHERE ${userFilterClause} + ${orgFilter} + ${filterClause} + ${rolesFilter} + ${additionalFilter} + ` + + const replacements = { + ...filter?.replacements, + search: `%${searchText}%`, + userId, + organizationIds, + } + + if (page !== null && limit !== null) { + query += ` + OFFSET :offset + LIMIT :limit; + ` + replacements.offset = limit * (page - 1) + replacements.limit = limit + } + + const connectedUsers = await sequelize.query(query, { + type: QueryTypes.SELECT, + replacements: replacements, + }) + + const countQuery = ` + SELECT count(*) AS "count" + FROM ${common.materializedViewsPrefix + MenteeExtension.tableName} + WHERE ${userFilterClause} + ${filterClause} + ${rolesFilter} + ${orgFilter} + ${additionalFilter}; + ` + const count = await sequelize.query(countQuery, { + type: QueryTypes.SELECT, + replacements: replacements, + }) + + return { + data: connectedUsers, + count: Number(count[0].count), + } + } catch (error) { + throw error + } +} + +exports.updateConnection = async (userId, friendId, updateBody) => { + try { + const [rowsUpdated, updatedConnections] = await Connection.update(updateBody, { + where: { + [Op.or]: [ + { user_id: userId, friend_id: friendId }, + { user_id: friendId, friend_id: userId }, + ], + status: common.CONNECTIONS_STATUS.ACCEPTED, + }, + returning: true, + raw: true, + }) + + // Find and return the specific row + const targetConnection = updatedConnections.find( + (connection) => connection.user_id === userId && connection.friend_id === friendId + ) + + return targetConnection + } catch (error) { + throw error + } +} diff --git a/src/database/queries/mentorExtension.js b/src/database/queries/mentorExtension.js index 02e876397..8479ec8e0 100644 --- a/src/database/queries/mentorExtension.js +++ b/src/database/queries/mentorExtension.js @@ -52,6 +52,20 @@ module.exports = class MentorExtensionQueries { } const whereClause = _.isEmpty(customFilter) ? { user_id: userId } : customFilter + // If `meta` is included in `data`, use `jsonb_set` to merge changes safely + if (data.meta) { + for (const [key, value] of Object.entries(data.meta)) { + data.meta = Sequelize.fn( + 'jsonb_set', + Sequelize.fn('COALESCE', Sequelize.col('meta'), '{}'), // Initializes `meta` if null + `{${key}}`, + JSON.stringify(value), + true + ) + } + } else { + delete data.meta + } const result = unscoped ? await MentorExtension.unscoped().update(data, { @@ -87,7 +101,8 @@ module.exports = class MentorExtensionQueries { } else { mentor = await MentorExtension.findOne(queryOptions) } - if (mentor.email) { + + if (mentor?.email) { mentor.email = await emailEncryption.decrypt(mentor.email.toLowerCase()) } return mentor diff --git a/src/database/queries/userExtension.js b/src/database/queries/userExtension.js index f720b1a83..b00a94bde 100644 --- a/src/database/queries/userExtension.js +++ b/src/database/queries/userExtension.js @@ -37,6 +37,22 @@ module.exports = class MenteeExtensionQueries { delete data['user_id'] } const whereClause = _.isEmpty(customFilter) ? { user_id: userId } : customFilter + + // If `meta` is included in `data`, use `jsonb_set` to merge changes safely + if (data.meta) { + for (const [key, value] of Object.entries(data.meta)) { + data.meta = Sequelize.fn( + 'jsonb_set', + Sequelize.fn('COALESCE', Sequelize.col('meta'), '{}'), // Initializes `meta` if null + `{${key}}`, + JSON.stringify(value), + true + ) + } + } else { + delete data.meta + } + return await MenteeExtension.update(data, { where: whereClause, ...options, @@ -156,7 +172,7 @@ module.exports = class MenteeExtensionQueries { } else { mentee = await MenteeExtension.findOne(queryOptions) } - if (mentee.email) { + if (mentee?.email) { mentee.email = await emailEncryption.decrypt(mentee.email.toLowerCase()) } return mentee diff --git a/src/distributionColumns.psql b/src/distributionColumns.psql index d84ff4b47..decfa30c3 100644 --- a/src/distributionColumns.psql +++ b/src/distributionColumns.psql @@ -15,4 +15,5 @@ SELECT create_distributed_table('session_enrollments', 'mentee_id'); SELECT create_distributed_table('session_ownerships', 'mentor_id'); SELECT create_distributed_table('sessions', 'id'); SELECT create_distributed_table('user_extensions', 'user_id'); - +SELECT create_distributed_table('connection_requests', 'user_id'); +SELECT create_distributed_table('connections', 'user_id'); \ No newline at end of file diff --git a/src/envVariables.js b/src/envVariables.js index 6b00c946b..1e238948e 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -343,6 +343,21 @@ let enviromentVariables = { optional: true, default: 'CURRENT', }, + ENABLE_CHAT: { + message: 'Enable or Disable Chat Capabilities', + optional: true, + default: false, + }, + COMMUNICATION_SERVICE_HOST: { + message: 'Communication service host', + optional: process.env.ENABLE_CHAT === 'true' ? false : true, + default: false, + }, + COMMUNICATION_SERVICE_BASE_URL: { + message: 'Base URL for the Communication Service', + optional: true, + default: '/communications/', + }, } let success = true diff --git a/src/helpers/communications.js b/src/helpers/communications.js new file mode 100644 index 000000000..b90375693 --- /dev/null +++ b/src/helpers/communications.js @@ -0,0 +1,197 @@ +'use strict' +const communicationRequests = require('@requests/communications') +const userExtensionQueries = require('@database/queries/userExtension') +const emailEncryption = require('@utils/emailEncryption') +const common = require('@constants/common') +const utils = require('@generics/utils') +const userRequests = require('@requests/user') + +/** + * Logs in a user and retrieves authentication token and user ID. + * @async + * @param {string} userId - Unique identifier of the user. + * @returns {Promise} An object containing auth_token and user_id if login is successful. + * @throws Will throw an error if the login request fails for reasons other than unauthorized access. + */ +exports.login = async (userId) => { + try { + const login = await communicationRequests.login({ userId }) + return { + auth_token: login.result.auth_token, + user_id: login.result.user_id, + } + } catch (error) { + if (error.message === common.COMMUNICATION.UNAUTHORIZED) { + console.error('Error: Unauthorized access during login. Please check your tokens.') + } + throw error + } +} + +/** + * Logs out a user from the communication service. + * @async + * @param {string} userId - Unique identifier of the user. + * @returns {Promise} The status of the logout operation. + * @throws Will throw an error if the logout request fails for reasons other than unauthorized access. + */ +exports.logout = async (userId) => { + try { + const logout = await communicationRequests.logout({ userId }) + return logout.result.status + } catch (error) { + if (error.message === common.COMMUNICATION.UNAUTHORIZED) { + console.error('Error: Unauthorized access during logout. Please check your tokens.') + } + throw error + } +} + +/** + * Updates a user's avatar. + * @async + * @param {string} userId - Unique identifier of the user. + * @param {string} imageUrl - New avatar URL for the user. + * @returns {Promise} Resolves if the update is successful. + * @throws Will throw an error if the updateAvatar request fails. + */ +exports.updateAvatar = async (userId, imageUrl) => { + try { + await communicationRequests.updateAvatar(userId, imageUrl) + } catch (error) { + console.error(`Error updating avatar for user ${userId}:`, error.message) + throw error + } +} + +/** + * Updates a user's name. + * @async + * @param {string} userId - Unique identifier of the user. + * @param {string} name - New name for the user. + * @returns {Promise} Resolves if the update is successful. + * @throws Will throw an error if the updateUser request fails. + */ +exports.updateUser = async (userId, name) => { + try { + await communicationRequests.updateUser(userId, name) + } catch (error) { + console.error(`Error updating user ${userId}:`, error.message) + throw error + } +} + +/** + * Creates or updates a user in the communication service. + * Optimized to handle updates for avatar and name if the user already exists. + * @async + * @param {Object} userData - Data for the user. + * @param {string} userData.userId - Unique identifier of the user. + * @param {string} userData.name - Name of the user. + * @param {string} userData.email - Email of the user. + * @param {string} userData.image - URL of the user's profile image. + * @returns {Promise} Resolves if creation or updates are successful. + * @throws Will throw an error if any request fails. + */ +exports.createOrUpdateUser = async ({ userId, name, email, image }) => { + try { + const user = await userExtensionQueries.getUserById(userId, { + attributes: ['meta'], + }) + + if (user && user.meta?.communications_user_id) { + // Update user information if already exists in the communication service + await Promise.all([ + image ? this.updateAvatar(userId, image) : Promise.resolve(), + name ? this.updateUser(userId, name) : Promise.resolve(), + ]) + } else { + // Create new user in the communication service + await this.create(userId, name, email, image) + } + } catch (error) { + console.error('Error in createOrUpdateUser:', error.message) + throw error + } +} + +/** + * Creates a new user in the communication system, then updates the user's metadata. + * @async + * @param {string} userId - Unique identifier of the user. + * @param {string} name - Name of the user. + * @param {string} email - Email of the user. + * @param {string} image - URL of the user's profile image. + * @returns {Promise} An object containing the user_id from the communication service. + * @throws Will throw an error if the signup request fails for reasons other than unauthorized access. + */ +exports.create = async (userId, name, email, image) => { + try { + const signup = await communicationRequests.signup({ userId, name, email, image }) + + if (signup.result.user_id) { + // Update the user's metadata with the communication service user ID + await userExtensionQueries.updateMenteeExtension( + userId, + { meta: { communications_user_id: signup.result.user_id } }, + { + returning: true, + raw: true, + } + ) + } + return { + user_id: signup.result.user_id, + } + } catch (error) { + if (error.message === common.COMMUNICATION.UNAUTHORIZED) { + console.error('Error: Unauthorized access during signup. Please check your tokens.') + } + throw error + } +} + +/** + * Creates a chat room between two users. If a user lacks a communications ID, it creates one. + * @async + * @param {string} recipientUserId - The ID of the user to receive the chat room invite. + * @param {string} initiatorUserId - The ID of the user initiating the chat room. + * @param {string} initialMessage - An initial message to be sent in the chat room. + * @returns {Promise} The response from the communication service upon creating the chat room. + * @throws Will throw an error if the request to create a chat room fails. + */ +exports.createChatRoom = async (recipientUserId, initiatorUserId, initialMessage) => { + try { + // Retrieve user details, ensuring each has a `communications_user_id` + let userDetails = await userExtensionQueries.getUsersByUserIds( + [initiatorUserId, recipientUserId], + { + attributes: ['name', 'user_id', 'email', 'meta', 'image'], + }, + true + ) + + // Loop through users to ensure they have a `communications_user_id` + for (const user of userDetails) { + if (!user.meta || !user.meta.communications_user_id) { + // Decrypt email and create user in communication service if `communications_user_id` is missing + user.email = await emailEncryption.decrypt(user.email) + let userImage + if (user?.image) { + userImage = (await userRequests.getDownloadableUrl(user.image))?.result + } + await this.create(user.user_id, user.name, user.email, userImage) + } + } + + // Create the chat room after ensuring all users have `communications_user_id` + const chatRoom = await communicationRequests.createChatRoom({ + userIds: [initiatorUserId, recipientUserId], + initialMessage: initialMessage, + }) + return chatRoom + } catch (error) { + console.error('Create Room Failed:', error) + throw error + } +} diff --git a/src/helpers/getDefaultOrgId.js b/src/helpers/getDefaultOrgId.js index f0cec6291..c3e0831bd 100644 --- a/src/helpers/getDefaultOrgId.js +++ b/src/helpers/getDefaultOrgId.js @@ -1,7 +1,11 @@ 'use strict' const userRequests = require('@requests/user') + exports.getDefaultOrgId = async () => { try { + let defaultOrgId = process.env.DEFAULT_ORG_ID + if (defaultOrgId) return defaultOrgId.toString() + let defaultOrgDetails = await userRequests.fetchOrgDetails({ organizationCode: process.env.DEFAULT_ORGANISATION_CODE, }) diff --git a/src/helpers/saasUserAccessibility.js b/src/helpers/saasUserAccessibility.js new file mode 100644 index 000000000..74ba30232 --- /dev/null +++ b/src/helpers/saasUserAccessibility.js @@ -0,0 +1,123 @@ +const menteeQueries = require('@database/queries/userExtension') +const responses = require('@helpers/responses') +const common = require('@constants/common') +const httpStatusCode = require('@generics/http-status') + +/** + * @description - Check if users are accessible based on the SaaS policy. + * @method + * @name checkIfUserIsAccessible + * @param {Number} userId - User ID. + * @param {Object|Array} userData - User data (single object or array). + * @returns {Boolean|Array} - Boolean (for a single user) or array of objects with user_id and isAccessible flag (for multiple users). + */ +async function checkIfUserIsAccessible(userId, userData) { + try { + // Ensure userData is always processed as an array + const users = Array.isArray(userData) ? userData : [userData] + + // Fetch policy details + const userPolicyDetails = await menteeQueries.getMenteeExtension(userId, [ + 'external_mentor_visibility', + 'external_mentee_visibility', + 'organization_id', + ]) + if (!userPolicyDetails || Object.keys(userPolicyDetails).length === 0) { + return responses.failureResponse({ + statusCode: httpStatusCode.NOT_FOUND, + message: 'USER_EXTENSION_NOT_FOUND', + responseCode: 'CLIENT_ERROR', + }) + } + + const { organization_id, external_mentor_visibility, external_mentee_visibility } = userPolicyDetails + + // Ensure data for accessibility evaluation + if (!organization_id) { + return false // If no organization_id is found, return false for accessibility + } + + // For single user, return boolean indicating accessibility + if (users.length === 1) { + const user = users[0] + const isMentor = user.is_mentor + const visibilityKey = isMentor ? external_mentor_visibility : external_mentee_visibility + const roleVisibilityKey = isMentor ? 'mentor_visibility' : 'mentee_visibility' + + let isAccessible = false + + switch (visibilityKey) { + case common.CURRENT: + isAccessible = user.organization_id === organization_id + break + + case common.ASSOCIATED: + isAccessible = + (user.visible_to_organizations.includes(organization_id) && + user[roleVisibilityKey] !== common.CURRENT) || + user.organization_id === organization_id + break + + case common.ALL: + isAccessible = + (user.visible_to_organizations.includes(organization_id) && + user[roleVisibilityKey] !== common.CURRENT) || + user[roleVisibilityKey] === common.ALL || + user.organization_id === organization_id + break + + default: + break + } + + return isAccessible + } + + // For multiple users, return an array with each user's accessibility status + const accessibleUsers = users.map((user) => { + const isMentor = user.is_mentor + const visibilityKey = isMentor ? external_mentor_visibility : external_mentee_visibility + const roleVisibilityKey = isMentor ? 'mentor_visibility' : 'mentee_visibility' + + let isAccessible = false + + switch (visibilityKey) { + case common.CURRENT: + isAccessible = user.organization_id === organization_id + break + + case common.ASSOCIATED: + isAccessible = + (user.visible_to_organizations.includes(organization_id) && + user[roleVisibilityKey] !== common.CURRENT) || + user.organization_id === organization_id + break + + case common.ALL: + isAccessible = + (user.visible_to_organizations.includes(organization_id) && + user[roleVisibilityKey] !== common.CURRENT) || + user[roleVisibilityKey] === common.ALL || + user.organization_id === organization_id + break + + default: + break + } + + return { + user_id: user.id, + isAccessible: isAccessible, + } + }) + + return accessibleUsers + } catch (error) { + throw error // Return error if something goes wrong + } +} + +// Export the function to be used in other parts of the app +module.exports = { + checkIfUserIsAccessible, +} diff --git a/src/locales/en.json b/src/locales/en.json index cc15ceadf..fc3382b96 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -190,6 +190,20 @@ "UNAUTHORIZED_REQUEST": "You are not authorized to access this resource. Please ensure that you have the correct permissions or authentication credentials.", "NO_RESULTS_FOUND": "No results found", "ENTITY_TYPE_AND_ENTITES_DELETED_SUCCESSFULLY": "Entity types and Entities deleted successfully.", + "CONNECTION_REQUEST_SEND_SUCCESSFULLY": "Your connection request has been sent successfully!", + "CONNECTION_REQUEST_EXISTS": "You already have a connection request with this user.", + "CONNECTION_LIST": "Successfully retrieved your connection list!", + "CONNECTION_REQUEST_APPROVED": "Your connection request has been approved.", + "CONNECTION_REQUEST_REJECTED": "Your connection request has been rejected.", + "CONNECTION_REQUEST_NOT_FOUND": "Oops! We couldn't find that connection request.", + "CONNECTION_EXITS": "You are already connected with this user.", + "CONNECTION_NOT_FOUND": "Connection not found.", + "CONNECTION_DETAILS": "Connection details fetched successfully.", + "CONNECTED_USERS_FETCHED": "Connections retrieved successfully.", + "CONNECTION_REQUEST_NOT_FOUND_OR_ALREADY_PROCESSED": "The connection request could not be found or has already been processed.", + "COMMUNICATION_TOKEN_FETCHED_SUCCESSFULLY": "Communication token retrieved successfully.", + "COMMUNICATION_TOKEN_NOT_FOUND": "Communication token could not be located.", + "USER_LOGGED_OUT": "User has been logged out successfully.", "USER_DETAILS_FETCHED_SUCCESSFULLY": "User details fetched successfully.", "ORGANIZATION_FETCHED_SUCCESSFULLY": "Organization fetched successfully." } diff --git a/src/requests/communications.js b/src/requests/communications.js new file mode 100644 index 000000000..73b89fce8 --- /dev/null +++ b/src/requests/communications.js @@ -0,0 +1,159 @@ +// File: communications.js + +const axios = require('axios') +const apiEndpoints = require('@constants/endpoints') + +const baseUrl = process.env.COMMUNICATION_SERVICE_HOST + process.env.COMMUNICATION_SERVICE_BASE_URL +const internalAccessToken = process.env.INTERNAL_ACCESS_TOKEN + +// Create Axios instance with default configurations for base URL and headers +const apiClient = axios.create({ + baseURL: baseUrl, + headers: { + internal_access_token: internalAccessToken, + }, +}) + +// Axios response interceptor to handle specific HTTP errors centrally +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response && error.response.status === 401) { + console.error('Unauthorized: 401 error') + return Promise.reject(new Error('unauthorized')) + } + return Promise.reject(error) + } +) + +/** + * Signs up a new user with the communication service. + * @async + * @param {Object} params - Parameters for signup. + * @param {string} params.userId - The unique identifier for the user. + * @param {string} params.name - The name of the user. + * @param {string} params.email - The email of the user. + * @param {string} params.image - URL for the user's profile image. + * @returns {Promise} The response data from the signup request. + * @throws Will throw an error if the signup request fails. + */ +exports.signup = async ({ userId, name, email, image }) => { + try { + const url = apiEndpoints.COMMUNICATION_SIGNUP + const body = { user_id: userId, name, email } + if (image) { + body.image_url = image + } + const response = await apiClient.post(url, body) + return response.data + } catch (err) { + console.error('Signup error:', err.message) + throw err + } +} + +/** + * Logs in a user with the communication service. + * @async + * @param {Object} params - Parameters for login. + * @param {string} params.userId - The unique identifier for the user. + * @returns {Promise} The response data from the login request. + * @throws Will throw an error if the login request fails. + */ +exports.login = async ({ userId }) => { + try { + const url = apiEndpoints.COMMUNICATION_LOGIN + const body = { user_id: userId } + + const response = await apiClient.post(url, body) + return response.data + } catch (err) { + console.error('Login error:', err.message) + throw err + } +} + +/** + * Logs out a user from the communication service. + * @async + * @param {Object} params - Parameters for logout. + * @param {string} params.userId - The unique identifier for the user. + * @returns {Promise} The response data from the logout request. + * @throws Will throw an error if the logout request fails. + */ +exports.logout = async ({ userId }) => { + try { + const url = apiEndpoints.COMMUNICATION_LOGOUT + const body = { user_id: userId } + + const response = await apiClient.post(url, body) + return response.data + } catch (err) { + console.error('Logout error:', err.message) + throw err + } +} + +/** + * Creates a chat room with an optional initial message. + * @async + * @param {Object} params - Parameters for creating a chat room. + * @param {Array} params.userIds - Array of user IDs to be added to the chat room. + * @param {string} [params.initialMessage] - An optional initial message for the chat room. + * @returns {Promise} The response data from the create chat room request. + * @throws Will throw an error if the request fails. + */ +exports.createChatRoom = async ({ userIds, initialMessage }) => { + try { + const url = apiEndpoints.COMMUNICATION_CREATE_CHAT_ROOM + const body = { usernames: userIds, initial_message: initialMessage } + + const response = await apiClient.post(url, body) + return response.data + } catch (err) { + console.error('Create Chat Room error:', err.message) + throw err + } +} + +/** + * Updates a user's avatar in the communication service. + * @async + * @param {string} userId - The unique identifier for the user. + * @param {string} imageUrl - The new avatar URL. + * @returns {Promise} The response data from the update avatar request. + * @throws Will throw an error if the request fails. + */ +exports.updateAvatar = async (userId, imageUrl) => { + try { + const url = apiEndpoints.COMMUNICATION_UPDATE_AVATAR + const body = { user_id: userId, image_url: imageUrl } + + const response = await apiClient.post(url, body) + return response.data + } catch (err) { + console.error('Update Avatar error:', err.message) + throw err + } +} + +/** + * Updates a user's details in the communication service. + * @async + * @param {string} userId - The unique identifier for the user. + * @param {string} name - The new name of the user. + * @returns {Promise} The response data from the update user request. + * @throws Will throw an error if the request fails. + */ +exports.updateUser = async (userId, name) => { + try { + const url = apiEndpoints.COMMUNICATION_UPDATE_USER + const body = { user_id: userId, name } + + const response = await apiClient.post(url, body) + return response.data + } catch (err) { + console.error('Update User error:', err.message) + throw err + } +} diff --git a/src/services/connections.js b/src/services/connections.js new file mode 100644 index 000000000..3bd12aa5e --- /dev/null +++ b/src/services/connections.js @@ -0,0 +1,449 @@ +const httpStatusCode = require('@generics/http-status') +const connectionQueries = require('@database/queries/connection') +const responses = require('@helpers/responses') +const userExtensionQueries = require('@database/queries/userExtension') +const { UniqueConstraintError } = require('sequelize') +const common = require('@constants/common') +const entityTypeService = require('@services/entity-type') +const entityTypeQueries = require('@database/queries/entityType') +const { Op } = require('sequelize') +const { getDefaultOrgId } = require('@helpers/getDefaultOrgId') +const { removeDefaultOrgEntityTypes } = require('@generics/utils') +const utils = require('@generics/utils') +const communicationHelper = require('@helpers/communications') +const userRequests = require('@requests/user') + +module.exports = class ConnectionHelper { + /** + * Check if a connection request already exists between two users. + * @param {string} userId - The ID of the user making the request. + * @param {string} targetUserId - The ID of the target user. + * @returns {Promise} The connection request if it exists, otherwise a failure response. + */ + static async checkConnectionRequestExists(userId, targetUserId) { + const connectionRequest = await connectionQueries.findOneRequest(userId, targetUserId) + if (!connectionRequest) { + return false + } + return connectionRequest + } + + /** + * Initiates a connection request between two users. + * @param {Object} bodyData - The request body containing user information. + * @param {string} bodyData.user_id - The ID of the target user. + * @param {string} userId - The ID of the user initiating the request. + * @returns {Promise} A success or failure response. + */ + static async initiate(bodyData, userId) { + try { + // Check if the target user exists + const userExists = await userExtensionQueries.getMenteeExtension(bodyData.user_id) + if (!userExists) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_found, + message: 'USER_NOT_FOUND', + }) + } + + // Check if a connection already exists between the users + const connectionExists = await connectionQueries.getConnection(userId, bodyData.user_id) + if (connectionExists?.status == common.CONNECTIONS_STATUS.BLOCKED) { + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_NOT_FOUND', + }) + } + + if (connectionExists) { + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'CONNECTION_EXITS', + }) + } + + // Create a new connection request + const friendRequestResult = await connectionQueries.addFriendRequest( + userId, + bodyData.user_id, + bodyData.message + ) + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'CONNECTION_REQUEST_SEND_SUCCESSFULLY', + result: friendRequestResult, + }) + } catch (error) { + if (error instanceof UniqueConstraintError) { + return responses.failureResponse({ + message: 'CONNECTION_REQUEST_EXISTS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + console.error(error) + throw error + } + } + + /** + * Get information about the connection between two users. + * @param {string} friendId - The ID of the friend or target user. + * @param {string} userId - The ID of the authenticated user. + * @returns {Promise} The connection details or appropriate error. + */ + static async getInfo(friendId, userId) { + try { + let connection = await connectionQueries.getConnection(userId, friendId) + + if (!connection) { + // If no connection is found, check for pending requests + connection = await connectionQueries.checkPendingRequest(userId, friendId) + } + + if (!connection) { + // If still no connection, check for the deleted request + connection = await connectionQueries.getRejectedRequest(userId, friendId) + } + + const defaultOrgId = await getDefaultOrgId() + if (!defaultOrgId) { + return responses.failureResponse({ + message: 'DEFAULT_ORG_ID_NOT_SET', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + const [userExtensionsModelName, userDetails] = await Promise.all([ + userExtensionQueries.getModelName(), + userExtensionQueries.getMenteeExtension(friendId, [ + 'name', + 'user_id', + 'mentee_visibility', + 'organization_id', + 'designation', + 'area_of_expertise', + 'education_qualification', + 'custom_entity_text', + 'meta', + 'is_mentor', + 'experience', + 'image', + ]), + ]) + + if (connection?.status === common.CONNECTIONS_STATUS.BLOCKED || !userDetails) { + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_NOT_FOUND', + }) + } + userDetails.image = (await userRequests.getDownloadableUrl(userDetails.image))?.result + + // Fetch entity types associated with the user + let entityTypes = await entityTypeQueries.findUserEntityTypesAndEntities({ + status: 'ACTIVE', + organization_id: { + [Op.in]: [userDetails.organization_id, defaultOrgId], + }, + model_names: { [Op.contains]: [userExtensionsModelName] }, + }) + const validationData = removeDefaultOrgEntityTypes(entityTypes, userDetails.organization_id) + const processedUserDetails = utils.processDbResponse(userDetails, validationData) + + if (!connection) { + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'CONNECTION_NOT_FOUND', + result: { user_details: processedUserDetails }, + }) + } + + connection.user_details = processedUserDetails + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'CONNECTION_DETAILS', + result: connection, + }) + } catch (error) { + console.error(error) + throw error + } + } + + /** + * Get a list of pending connection requests for a user. + * @param {string} userId - The ID of the user. + * @param {number} pageNo - The page number for pagination. + * @param {number} pageSize - The number of records per page. + * @returns {Promise} The list of pending connection requests. + */ + static async pending(userId, pageNo, pageSize) { + try { + const connections = await connectionQueries.getPendingRequests(userId, pageNo, pageSize) + + if (connections.count == 0 || connections.rows.length == 0) { + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'CONNECTION_LIST', + result: { + data: [], + count: connections.count, + }, + }) + } + + // Map friend details by user IDs + const friendIds = connections.rows.map((connection) => connection.friend_id) + let friendDetails = await userExtensionQueries.getUsersByUserIds(friendIds, { + attributes: [ + 'name', + 'user_id', + 'mentee_visibility', + 'organization_id', + 'designation', + 'area_of_expertise', + 'education_qualification', + 'custom_entity_text', + 'meta', + 'experience', + 'is_mentor', + 'image', + ], + }) + + const userExtensionsModelName = await userExtensionQueries.getModelName() + + const uniqueOrgIds = [...new Set(friendDetails.map((obj) => obj.organization_id))] + friendDetails = await entityTypeService.processEntityTypesToAddValueLabels( + friendDetails, + uniqueOrgIds, + userExtensionsModelName, + 'organization_id' + ) + + const friendDetailsMap = friendDetails.reduce((acc, friend) => { + acc[friend.user_id] = friend + return acc + }, {}) + + let connectionsWithDetails = connections.rows.map((connection) => { + return { + ...connection, + user_details: friendDetailsMap[connection.friend_id] || null, + } + }) + + const userIds = connectionsWithDetails.map((item) => item.friend_id) + const userDetails = await userRequests.getListOfUserDetails(userIds, true) + const userDetailsMap = new Map(userDetails.result.map((userDetail) => [String(userDetail.id), userDetail])) + connectionsWithDetails = connectionsWithDetails.filter((connectionsWithDetail) => { + const user_id = String(connectionsWithDetail.friend_id) + + if (userDetailsMap.has(user_id)) { + const userDetail = userDetailsMap.get(user_id) + connectionsWithDetail.user_details.image = userDetail.image + return true + } + return false + }) + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'CONNECTION_LIST', + result: { data: connectionsWithDetails, count: connections.count }, + }) + } catch (error) { + console.error(error) + throw error + } + } + + /** + * Accept a pending connection request. + * @param {Object} bodyData - The body data containing the target user ID. + * @param {string} bodyData.user_id - The ID of the target user. + * @param {string} userId - The ID of the authenticated user. + * @returns {Promise} A success response indicating the request was accepted. + */ + static async accept(bodyData, userId) { + try { + const connectionRequest = await this.checkConnectionRequestExists(userId, bodyData.user_id) + if (!connectionRequest) + return responses.failureResponse({ + message: 'CONNECTION_REQUEST_NOT_FOUND_OR_ALREADY_PROCESSED', + statusCode: httpStatusCode.not_found, + responseCode: 'CLIENT_ERROR', + }) + + await connectionQueries.approveRequest(userId, bodyData.user_id, connectionRequest.meta) + + const userDetails = await userExtensionQueries.getUsersByUserIds( + [userId, bodyData.user_id], + { + attributes: ['settings', 'user_id'], + }, + true + ) + let chatRoom + // Create room only if both users have enable chat option + if ( + userDetails.length === 2 && + userDetails[0]?.settings?.chat_enabled === true && + userDetails[1]?.settings?.chat_enabled === true + ) { + chatRoom = await communicationHelper.createChatRoom( + userId, + bodyData.user_id, + connectionRequest.meta.message + ) + } + + // Update connection meta with room_id if chatRoom was created + const metaUpdate = chatRoom + ? { ...connectionRequest.meta, room_id: chatRoom.result.room.room_id } + : connectionRequest.meta + + const updateConnection = await connectionQueries.updateConnection(userId, bodyData.user_id, { + meta: metaUpdate, + }) + + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'CONNECTION_REQUEST_APPROVED', + result: updateConnection, + }) + } catch (error) { + console.error(error) + throw error + } + } + + /** + * Reject a pending connection request. + * @param {Object} bodyData - The body data containing the target user ID. + * @param {string} bodyData.user_id - The ID of the target user. + * @param {string} userId - The ID of the authenticated user. + * @returns {Promise} A success response indicating the request was rejected. + */ + static async reject(bodyData, userId) { + try { + const connectionRequest = await this.checkConnectionRequestExists(userId, bodyData.user_id) + if (!connectionRequest) + return responses.failureResponse({ + message: 'CONNECTION_REQUEST_NOT_FOUND_OR_ALREADY_PROCESSED', + statusCode: httpStatusCode.not_found, + responseCode: 'CLIENT_ERROR', + }) + + const [rejectedCount, rejectedData] = await connectionQueries.rejectRequest(userId, bodyData.user_id) + + if (rejectedCount == 0) { + return responses.failureResponse({ + message: 'CONNECTION_REQUEST_NOT_FOUND_OR_ALREADY_PROCESSED', + statusCode: httpStatusCode.not_found, + responseCode: 'CLIENT_ERROR', + }) + } + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'CONNECTION_REQUEST_REJECTED', + }) + } catch (error) { + console.error(error) + throw error + } + } + + /** + * Fetch a list of connections based on query parameters and filters. + * @param {number} pageNo - The page number for pagination. + * @param {number} pageSize - The number of records per page. + * @param {string} searchText - The search text to filter results. + * @param {Object} queryParams - The query parameters for filtering. + * @param {string} userId - The ID of the authenticated user. + * @param {string} orgId - The organization ID for filtering. + * @returns {Promise} A list of filtered connections. + */ + static async list(pageNo, pageSize, searchText, queryParams, userId, orgId) { + try { + let organizationIds = [] + + if (queryParams.organization_ids) { + organizationIds = queryParams.organization_ids.split(',') + } + + const query = utils.processQueryParametersWithExclusions(queryParams) + const userExtensionsModelName = await userExtensionQueries.getModelName() + + // Fetch validation data for filtering connections (excluding roles) + const validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ + status: 'ACTIVE', + allow_filtering: true, + model_names: { [Op.contains]: [userExtensionsModelName] }, + }) + + const filteredQuery = utils.validateAndBuildFilters(query, validationData, userExtensionsModelName) + + let roles = [] + if (queryParams.roles) { + roles = queryParams.roles.split(',') + } + + let extensionDetails = await connectionQueries.getConnectionsDetails( + pageNo, + pageSize, + filteredQuery, + searchText, + userId, + organizationIds, + roles + ) + + if (extensionDetails.count === 0 || extensionDetails.data.length === 0) { + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'CONNECTED_USERS_FETCHED', + result: { + data: [], + count: extensionDetails.count, + }, + }) + } + + if (extensionDetails.data.length > 0) { + const uniqueOrgIds = [...new Set(extensionDetails.data.map((obj) => obj.organization_id))] + + extensionDetails.data = await entityTypeService.processEntityTypesToAddValueLabels( + extensionDetails.data, + uniqueOrgIds, + userExtensionsModelName, + 'organization_id' + ) + } + const userIds = extensionDetails.data.map((item) => item.user_id) + const userDetails = await userRequests.getListOfUserDetails(userIds, true) + const userDetailsMap = new Map(userDetails.result.map((userDetail) => [String(userDetail.id), userDetail])) + extensionDetails.data = extensionDetails.data.filter((extensionDetail) => { + const user_id = String(extensionDetail.user_id) + if (userDetailsMap.has(user_id)) { + const userDetail = userDetailsMap.get(user_id) + extensionDetail.image = userDetail.image + return true + } + return false + }) + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'CONNECTED_USERS_FETCHED', + result: extensionDetails, + }) + } catch (error) { + console.error('Error in list function:', error) + throw error + } + } +} diff --git a/src/services/entity-type.js b/src/services/entity-type.js index 84c2e0fe0..caee3dd85 100644 --- a/src/services/entity-type.js +++ b/src/services/entity-type.js @@ -193,11 +193,12 @@ module.exports = class EntityHelper { * @name processEntityTypesToAddValueLabels * @param {Array} responseData - data to modify * @param {Array} orgIds - org ids - * @param {String} modelName - model name which the entity search is assocoated to. + * @param {String} modelName - model name which the entity search is associated to. * @param {String} orgIdKey - In responseData which key represents org id + * @param {ARRAY} entityType - Array of entity types value * @returns {JSON} - modified response data */ - static async processEntityTypesToAddValueLabels(responseData, orgIds, modelName, orgIdKey) { + static async processEntityTypesToAddValueLabels(responseData, orgIds, modelName, orgIdKey, entityType) { try { const defaultOrgId = await getDefaultOrgId() if (!defaultOrgId) @@ -221,7 +222,7 @@ module.exports = class EntityHelper { [Op.contains]: Array.isArray(modelName) ? modelName : [modelName], }, } - + if (entityType) filter.value = entityType // get entityTypes with entities data let entityTypesWithEntities = await entityTypeQueries.findUserEntityTypesAndEntities(filter) entityTypesWithEntities = JSON.parse(JSON.stringify(entityTypesWithEntities)) diff --git a/src/services/mentees.js b/src/services/mentees.js index af8cbf137..8566df0ba 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -26,11 +26,13 @@ const { getEnrolledMentees } = require('@helpers/getEnrolledMentees') const responses = require('@helpers/responses') const permissions = require('@helpers/getPermissions') const { buildSearchFilter } = require('@helpers/search') -const { defaultRulesFilter } = require('@helpers/defaultRules') +const { defaultRulesFilter, validateDefaultRulesFilter } = require('@helpers/defaultRules') const searchConfig = require('@configs/search.json') const emailEncryption = require('@utils/emailEncryption') +const communicationHelper = require('@helpers/communications') const menteeExtensionQueries = require('@database/queries/userExtension') +const { checkIfUserIsAccessible } = require('@helpers/saasUserAccessibility') module.exports = class MenteesHelper { /** @@ -81,6 +83,22 @@ module.exports = class MenteesHelper { const profileMandatoryFields = await utils.validateProfileData(processDbResponse, validationData) menteeDetails.data.result.profile_mandatory_fields = profileMandatoryFields + let communications = null + + if (mentee?.meta?.communications_user_id) { + try { + const chat = await communicationHelper.login(id) + communications = chat + } catch (error) { + console.error('Failed to log in to communication service:', error) + } + } + + processDbResponse.meta = { + ...processDbResponse.meta, + communications, + } + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'PROFILE_FTECHED_SUCCESSFULLY', @@ -752,16 +770,19 @@ module.exports = class MenteesHelper { * Update a mentee extension. * @method * @name updateMenteeExtension - * @param {String} userId - User ID of the mentee. * @param {Object} data - Updated mentee extension data excluding user_id. + * @param {String} userId - User ID of the mentee. + * @param {String} orgId - Organization ID for validation. * @returns {Promise} - Updated mentee extension details. */ static async updateMenteeExtension(data, userId, orgId) { try { + // Encrypt email if provided if (data.email) data.email = emailEncryption.encrypt(data.email.toLowerCase()) - let skipValidation = data.skipValidation ? data.skipValidation : false - // Remove certain data in case it is getting passed + let skipValidation = data.skipValidation || false + + // Remove unnecessary data keys const dataToRemove = [ 'user_id', 'mentor_visibility', @@ -771,31 +792,34 @@ module.exports = class MenteesHelper { 'external_mentee_visibility', 'mentee_visibility', ] + dataToRemove.forEach((key) => delete data[key]) - dataToRemove.forEach((key) => { - if (data[key]) { - delete data[key] - } - }) + // Fetch current mentee extension data + const currentUser = await menteeQueries.getMenteeExtension(userId) + if (!currentUser) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_found, + message: 'MENTEE_EXTENSION_NOT_FOUND', + }) + } + // Perform validation const defaultOrgId = await getDefaultOrgId() - if (!defaultOrgId) + if (!defaultOrgId) { return responses.failureResponse({ message: 'DEFAULT_ORG_ID_NOT_SET', statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) + } const userExtensionsModelName = await menteeQueries.getModelName() const filter = { status: 'ACTIVE', - organization_id: { - [Op.in]: [orgId, defaultOrgId], - }, + organization_id: { [Op.in]: [orgId, defaultOrgId] }, model_names: { [Op.contains]: [userExtensionsModelName] }, } let entityTypes = await entityTypeQueries.findUserEntityTypesAndEntities(filter) - //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) let res = utils.validateInput(data, validationData, userExtensionsModelName, skipValidation) if (!res.success) { @@ -807,10 +831,11 @@ module.exports = class MenteesHelper { }) } + // Restructure the data let userExtensionModel = await menteeQueries.getColumns() - data = utils.restructureBody(data, validationData, userExtensionModel) + // Handle organization update logic if organization data is provided if (data?.organization?.id) { //Do a org policy update for the user only if the data object explicitly includes an //organization.id. This is added for the users/update workflow where @@ -834,14 +859,29 @@ module.exports = class MenteesHelper { new Set([...userOrgDetails.data.result.related_orgs, data.organization.id]) ) } + + // Update the database const [updateCount, updatedUser] = await menteeQueries.updateMenteeExtension(userId, data, { returning: true, raw: true, }) + if (currentUser?.meta?.communications_user_id) { + const promises = [] + if (data.name && data.name !== currentUser.name) { + promises.push(communicationHelper.updateUser(userId, data.name)) + } + + if (data.image && data.image !== currentUser.image) { + const downloadableUrl = (await userRequests.getDownloadableUrl(data.image))?.result + promises.push(communicationHelper.updateAvatar(userId, downloadableUrl)) + } + + await Promise.all(promises) + } + if (updateCount === 0) { const fallbackUpdatedUser = await menteeQueries.getMenteeExtension(userId) - console.log(fallbackUpdatedUser) if (!fallbackUpdatedUser) { return responses.failureResponse({ statusCode: httpStatusCode.not_found, @@ -857,8 +897,8 @@ module.exports = class MenteesHelper { }) } + // Return updated data const processDbResponse = utils.processDbResponse(updatedUser[0], validationData) - return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'MENTEE_EXTENSION_UPDATED', @@ -1535,6 +1575,188 @@ module.exports = class MenteesHelper { return error } } + + /** + * Retrieves a communication token for the logged in user. + * + * This asynchronous method logs in a user using their unique identifier (`id`) + * to obtain a communication token and other relevant details, then returns a + * standardized success response with the token, user ID, and metadata. + * + * @async + * @function getCommunicationToken + * @param {string} id - The unique identifier of the user for whom the communication token is to be retrieved. + * @returns {Promise} A promise that resolves to an object containing the response code, message, result data, + * and additional metadata. + * + * @throws {Error} If the communicationHelper login process fails, this method may throw an error. + * + * @example + * const response = await getCommunicationToken(123); + * console.log(response); + * // { + * // responseCode: "OK", + * // message: "Communication token fetched successfully!", + * // result: { + * // auth_token: "_GTFHENH422lGlLcgYQfu2GnnWO8bg6zY8ZHrXkcNmN", + * // user_id: "Q9hz3jbPXkk3fXQoL" + * // }, + * // meta: { + * // correlation: "69893cb9-8b0c-44f9-945e-1bff2174af0d", + * // meetingPlatform: "BBB" + * // } + * // } + */ + static async getCommunicationToken(id) { + try { + const token = await communicationHelper.login(id) + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'COMMUNICATION_TOKEN_FETCHED_SUCCESSFULLY', + result: token, + }) + } catch (error) { + if (error.message == 'unauthorized') { + return responses.failureResponse({ + statusCode: httpStatusCode.not_found, + message: 'COMMUNICATION_TOKEN_NOT_FOUND', + responseCode: 'CLIENT_ERROR', + }) + } + } + } + + /** + * Logs out a user by invoking the `communicationHelper.logout` function and + * returns a success response upon successful logout. If the logout fails due to + * an unauthorized error, returns a failure response indicating that the communication + * token was not found. + * + * @async + * @function logout + * @param {string} id - The ID of the user to be logged out. + * @returns {Promise} Resolves with a success response object if the logout is successful, + * or a failure response object if an unauthorized error occurs. + * + * @throws {Error} If an error other than 'unauthorized' occurs, it will not be caught here and may be handled upstream. + */ + static async logout(id) { + try { + const response = await communicationHelper.logout(id) + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_LOGGED_OUT', + result: response, + }) + } catch (error) { + if (error.message === 'unauthorized') { + return responses.failureResponse({ + statusCode: httpStatusCode.not_found, + message: 'COMMUNICATION_TOKEN_NOT_FOUND', + responseCode: 'CLIENT_ERROR', + }) + } + throw error // rethrow other errors to be handled by a higher-level error handler + } + } + + static async details(id, orgId, userId = '', isAMentor = '', roles = '') { + try { + let requestedUserExtension = await menteeQueries.getMenteeExtension(id) + + if (!requestedUserExtension || (!isAMentor && requestedUserExtension.is_mentor == false)) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_found, + message: 'USER_NOT_FOUND', + responseCode: 'CLIENT_ERROR', + }) + } + if (requestedUserExtension.is_mentor == true) { + // Get mentor visibility and org id + const validateDefaultRules = await validateDefaultRulesFilter({ + ruleType: common.DEFAULT_RULES.MENTOR_TYPE, + requesterId: userId, + roles: roles, + requesterOrganizationId: orgId, + data: requestedUserExtension, + }) + if (validateDefaultRules.error && validateDefaultRules.error.missingField) { + return responses.failureResponse({ + message: 'PROFILE_NOT_UPDATED', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + if (!validateDefaultRules) { + return responses.failureResponse({ + message: 'USER_NOT_FOUND', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + } + // Check for accessibility for reading shared mentor profile + const isAccessible = await checkIfUserIsAccessible(userId, requestedUserExtension) + + // Throw access error + if (!isAccessible) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_found, + message: 'PROFILE_RESTRICTED', + }) + } + + let mentorExtension + if (requestedUserExtension) mentorExtension = requestedUserExtension + else mentorExtension = await mentorQueries.getMentorExtension(id) + + mentorExtension = utils.deleteProperties(mentorExtension, [ + 'user_id', + 'visible_to_organizations', + 'image', + 'email', + 'phone', + 'settings', + ]) + + const defaultOrgId = await getDefaultOrgId() + if (!defaultOrgId) + return responses.failureResponse({ + message: 'DEFAULT_ORG_ID_NOT_SET', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + const menteeExtensionsModelName = await menteeQueries.getModelName() + + let entityTypes = await entityTypeQueries.findUserEntityTypesAndEntities({ + status: 'ACTIVE', + organization_id: { + [Op.in]: [orgId, defaultOrgId], + }, + model_names: { [Op.contains]: [menteeExtensionsModelName] }, + }) + + // validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) + const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) + const processDbResponse = utils.processDbResponse(mentorExtension, validationData) + + const profileMandatoryFields = await utils.validateProfileData(processDbResponse, validationData) + processDbResponse.profile_mandatory_fields = profileMandatoryFields + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'PROFILE_FTECHED_SUCCESSFULLY', + result: { + ...processDbResponse, + }, + }) + } catch (error) { + console.error(error) + return error + } + } } function convertEntitiesForFilter(entityTypes) { diff --git a/src/services/mentors.js b/src/services/mentors.js index f4a596de5..69b42f244 100644 --- a/src/services/mentors.js +++ b/src/services/mentors.js @@ -25,7 +25,8 @@ const { buildSearchFilter } = require('@helpers/search') const searchConfig = require('@configs/search.json') const emailEncryption = require('@utils/emailEncryption') const { defaultRulesFilter, validateDefaultRulesFilter } = require('@helpers/defaultRules') - +const connectionQueries = require('@database/queries/connection') +const communicationHelper = require('@helpers/communications') module.exports = class MentorsHelper { /** * upcomingSessions. @@ -431,6 +432,14 @@ module.exports = class MentorsHelper { */ static async updateMentorExtension(data, userId, orgId) { try { + // Fetch current mentee extension data + const currentUser = await mentorQueries.getMentorExtension(userId) + if (!currentUser) { + return responses.failureResponse({ + statusCode: httpStatusCode.not_found, + message: 'MENTOR_EXTENSION_NOT_FOUND', + }) + } if (data.email) data.email = emailEncryption.encrypt(data.email.toLowerCase()) let skipValidation = data.skipValidation ? data.skipValidation : false // Remove certain data in case it is getting passed @@ -504,12 +513,26 @@ module.exports = class MentorsHelper { new Set([...userOrgDetails.data.result.related_orgs, data.organization.id]) ) } - console.log('UPDATED MENTOR EXTENSIONS: ', data) + const [updateCount, updatedMentor] = await mentorQueries.updateMentorExtension(userId, data, { returning: true, raw: true, }) + if (currentUser?.meta?.communications_user_id) { + const promises = [] + if (data.name && data.name !== currentUser.name) { + promises.push(communicationHelper.updateUser(userId, data.name)) + } + + if (data.image && data.image !== currentUser.image) { + const downloadableUrl = (await userRequests.getDownloadableUrl(data.image))?.result + promises.push(communicationHelper.updateAvatar(userId, downloadableUrl)) + } + + await Promise.all(promises) + } + if (updateCount === 0) { const fallbackUpdatedUser = await mentorQueries.getMentorExtension(userId) if (!fallbackUpdatedUser) { @@ -710,6 +733,21 @@ module.exports = class MentorsHelper { const profileMandatoryFields = await utils.validateProfileData(processDbResponse, validationData) mentorProfile.profile_mandatory_fields = profileMandatoryFields + let communications = null + + if (mentorExtension?.meta?.communications_user_id) { + try { + const chat = await communicationHelper.login(id) + communications = chat + } catch (error) { + console.error('Failed to log in to communication service:', error) + } + } + processDbResponse.meta = { + ...processDbResponse.meta, + communications, + } + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'PROFILE_FTECHED_SUCCESSFULLY', @@ -927,6 +965,9 @@ module.exports = class MentorsHelper { const userDetails = await userRequests.getListOfUserDetails(mentorIds, true, false) + const connectedUsers = await connectionQueries.getConnectionsByUserIds(userId, mentorIds) + const connectedMentorIds = new Set(connectedUsers.map((connectedUser) => connectedUser.friend_id)) + if (extensionDetails.data.length > 0) { const uniqueOrgIds = [...new Set(extensionDetails.data.map((obj) => obj.organization_id))] extensionDetails.data = await entityTypeService.processEntityTypesToAddValueLabels( @@ -944,10 +985,12 @@ module.exports = class MentorsHelper { extensionDetails.data = extensionDetails.data .map((extensionDetail) => { const user_id = `${extensionDetail.user_id}` + const isConnected = connectedMentorIds.has(extensionDetail.user_id) + if (userDetailsMap.has(user_id)) { let userDetail = userDetailsMap.get(user_id) // Merge userDetail with extensionDetail, prioritize extensionDetail properties - userDetail = { ...userDetail, ...extensionDetail } + userDetail = { ...userDetail, ...extensionDetail, is_connected: isConnected } delete userDetail.user_id delete userDetail.mentor_visibility delete userDetail.mentee_visibility diff --git a/src/validators/v1/connections.js b/src/validators/v1/connections.js new file mode 100644 index 000000000..bd815530a --- /dev/null +++ b/src/validators/v1/connections.js @@ -0,0 +1,43 @@ +module.exports = { + initiate: (req) => { + req.checkBody('user_id') + .notEmpty() + .withMessage('user_id is required') + .isString() + .withMessage('user_id must be a string') + + req.checkBody('message') + .notEmpty() + .withMessage('message is required') + .isString() + .withMessage('message must be a string') + }, + + pending: (req) => {}, + + list: (req) => {}, + + getInfo: (req) => { + req.checkBody('user_id') + .notEmpty() + .withMessage('user_id is required') + .isString() + .withMessage('user_id must be a string') + }, + + accept: (req) => { + req.checkBody('user_id') + .notEmpty() + .withMessage('user_id is required') + .isString() + .withMessage('user_id must be a string') + }, + + reject: (req) => { + req.checkBody('user_id') + .notEmpty() + .withMessage('user_id is required') + .isString() + .withMessage('user_id must be a string') + }, +}