Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added: image and user profile update integration with communication service #1035

Open
wants to merge 7 commits into
base: release-3.2.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 219 additions & 1 deletion src/api-doc/MentorED-Mentoring.postman_collection.json

Large diffs are not rendered by default.

1,342 changes: 1,209 additions & 133 deletions src/api-doc/api-doc.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/constants/endpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ module.exports = {
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',
}
27 changes: 27 additions & 0 deletions src/controllers/v1/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,33 @@ module.exports = class Mentees {
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<Object>} - The mentor's profile details.
*/
async details(req) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nevil-mathew why this API needed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we only have API to get mentor details, this API allows getting any user details. Will update the JS Doc to mention that.

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.
// /**
Expand Down
1 change: 1 addition & 0 deletions src/database/queries/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ exports.getConnectionsDetails = async (
is_mentor,
area_of_expertise,
education_qualification,
image,
custom_entity_text::JSONB AS custom_entity_text,
meta::JSONB AS meta
`
Expand Down
6 changes: 4 additions & 2 deletions src/database/queries/mentorExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ 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)) {
Expand All @@ -64,6 +63,8 @@ module.exports = class MentorExtensionQueries {
true
)
}
} else {
delete data.meta
}

const result = unscoped
Expand Down Expand Up @@ -100,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
Expand Down
4 changes: 3 additions & 1 deletion src/database/queries/userExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ module.exports = class MenteeExtensionQueries {
true
)
}
} else {
delete data.meta
}

return await MenteeExtension.update(data, {
Expand Down Expand Up @@ -170,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
Expand Down
81 changes: 78 additions & 3 deletions src/helpers/communications.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ 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
Expand Down Expand Up @@ -44,6 +47,74 @@ exports.logout = async (userId) => {
}
}

/**
* 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<void>} 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<void>} 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<void>} 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
Expand All @@ -60,7 +131,7 @@ exports.create = async (userId, name, email, image) => {

if (signup.result.user_id) {
// Update the user's metadata with the communication service user ID
const [updateCount, updatedUser] = await userExtensionQueries.updateMenteeExtension(
await userExtensionQueries.updateMenteeExtension(
userId,
{ meta: { communications_user_id: signup.result.user_id } },
{
Expand Down Expand Up @@ -95,7 +166,7 @@ exports.createChatRoom = async (recipientUserId, initiatorUserId, initialMessage
let userDetails = await userExtensionQueries.getUsersByUserIds(
[initiatorUserId, recipientUserId],
{
attributes: ['name', 'user_id', 'email', 'meta'],
attributes: ['name', 'user_id', 'email', 'meta', 'image'],
},
true
)
Expand All @@ -105,7 +176,11 @@ exports.createChatRoom = async (recipientUserId, initiatorUserId, initialMessage
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)
await this.create(user.user_id, user.name, user.email, 'https://picsum.photos/200/200')
let userImage
if (user?.image) {
userImage = (await userRequests.getDownloadableUrl(user.image))?.result
}
await this.create(user.user_id, user.name, user.email, userImage)
}
}

Expand Down
123 changes: 123 additions & 0 deletions src/helpers/saasUserAccessibility.js
Original file line number Diff line number Diff line change
@@ -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,
}
48 changes: 46 additions & 2 deletions src/requests/communications.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ apiClient.interceptors.response.use(
exports.signup = async ({ userId, name, email, image }) => {
try {
const url = apiEndpoints.COMMUNICATION_SIGNUP
const body = { user_id: userId, name, email, image_url: image }

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) {
Expand Down Expand Up @@ -113,3 +115,45 @@ exports.createChatRoom = async ({ userIds, initialMessage }) => {
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<Object>} 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<Object>} 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
}
}
Loading