From 16cbb357b216e2e6c580496329c6fd0f6627ffb0 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" <contato@luizrrodrigues.com.br> Date: Mon, 13 Dec 2021 09:09:54 -0300 Subject: [PATCH 1/3] Revert "Merge pull request #325 from topcoder-platform/revert-322-feat/traits-v5-upgrade" This reverts commit 83a7c5c575250beac24792320eb8c8ad676dab5d, reversing changes made to 4a658562739b7821008f48e6c1498011dc64208e. --- __tests__/actions/profile.js | 4 -- docs/services.members.md | 40 +++--------------- package.json | 2 +- src/actions/profile.js | 4 +- src/services/members.js | 78 +++++++----------------------------- src/services/user-traits.js | 44 +++++++++----------- src/utils/tc.js | 3 +- 7 files changed, 44 insertions(+), 131 deletions(-) diff --git a/__tests__/actions/profile.js b/__tests__/actions/profile.js index b4d1848b..4ea366a3 100644 --- a/__tests__/actions/profile.js +++ b/__tests__/actions/profile.js @@ -18,8 +18,6 @@ const linkedAccounts = [{ // Mock services const mockMembersService = { - getPresignedUrl: jest.fn().mockReturnValue(Promise.resolve()), - uploadFileToS3: jest.fn().mockReturnValue(Promise.resolve()), updateMemberPhoto: jest.fn().mockReturnValue(Promise.resolve('url-of-photo')), updateMemberProfile: jest.fn().mockReturnValue(Promise.resolve(profile)), addSkill: jest.fn().mockReturnValue(Promise.resolve({ skills: [skill] })), @@ -47,8 +45,6 @@ test('Module exports', () => expect(actions).toMatchSnapshot()); test('profile.uploadPhotoDone', async () => { const actionResult = await redux.resolveAction(actions.profile.uploadPhotoDone(handle, tokenV3)); expect(actionResult).toMatchSnapshot(); - expect(mockMembersService.getPresignedUrl).toBeCalled(); - expect(mockMembersService.uploadFileToS3).toBeCalled(); expect(mockMembersService.updateMemberPhoto).toBeCalled(); }); diff --git a/docs/services.members.md b/docs/services.members.md index 856b90ce..8fe477f2 100644 --- a/docs/services.members.md +++ b/docs/services.members.md @@ -25,9 +25,7 @@ members via API V3. * [.addSkill(handle, skillTagId)](#module_services.members..MembersService+addSkill) ⇒ <code>Promise</code> * [.hideSkill(handle, skillTagId)](#module_services.members..MembersService+hideSkill) ⇒ <code>Promise</code> * [.updateMemberProfile(profile)](#module_services.members..MembersService+updateMemberProfile) ⇒ <code>Promise</code> - * [.getPresignedUrl(userHandle, file)](#module_services.members..MembersService+getPresignedUrl) ⇒ <code>Promise</code> - * [.updateMemberPhoto(S3Response)](#module_services.members..MembersService+updateMemberPhoto) ⇒ <code>Promise</code> - * [.uploadFileToS3(presignedUrlResponse)](#module_services.members..MembersService+uploadFileToS3) ⇒ <code>Promise</code> + * [.updateMemberPhoto(userHandle, file)](#module_services.members..MembersService+updateMemberPhoto) ⇒ <code>Promise</code> * [.verifyMemberNewEmail(handle, emailVerifyToken)](#module_services.members..MembersService+verifyMemberNewEmail) ⇒ <code>Promise</code> <a name="module_services.members.getService"></a> @@ -65,9 +63,7 @@ Service class. * [.addSkill(handle, skillTagId)](#module_services.members..MembersService+addSkill) ⇒ <code>Promise</code> * [.hideSkill(handle, skillTagId)](#module_services.members..MembersService+hideSkill) ⇒ <code>Promise</code> * [.updateMemberProfile(profile)](#module_services.members..MembersService+updateMemberProfile) ⇒ <code>Promise</code> - * [.getPresignedUrl(userHandle, file)](#module_services.members..MembersService+getPresignedUrl) ⇒ <code>Promise</code> - * [.updateMemberPhoto(S3Response)](#module_services.members..MembersService+updateMemberPhoto) ⇒ <code>Promise</code> - * [.uploadFileToS3(presignedUrlResponse)](#module_services.members..MembersService+uploadFileToS3) ⇒ <code>Promise</code> + * [.updateMemberPhoto(userHandle, file)](#module_services.members..MembersService+updateMemberPhoto) ⇒ <code>Promise</code> * [.verifyMemberNewEmail(handle, emailVerifyToken)](#module_services.members..MembersService+verifyMemberNewEmail) ⇒ <code>Promise</code> <a name="new_module_services.members..MembersService_new"></a> @@ -256,42 +252,18 @@ Updates member profile. | --- | --- | --- | | profile | <code>Object</code> | The profile to update. | -<a name="module_services.members..MembersService+getPresignedUrl"></a> - -#### membersService.getPresignedUrl(userHandle, file) ⇒ <code>Promise</code> -Gets presigned url for member photo file. - -**Kind**: instance method of [<code>MembersService</code>](#module_services.members..MembersService) -**Returns**: <code>Promise</code> - Resolves to the api response content - -| Param | Type | Description | -| --- | --- | --- | -| userHandle | <code>String</code> | The user handle | -| file | <code>File</code> | The file to get its presigned url | - <a name="module_services.members..MembersService+updateMemberPhoto"></a> -#### membersService.updateMemberPhoto(S3Response) ⇒ <code>Promise</code> -Updates member photo. +#### membersService.updateMemberPhoto(userHandle, file) ⇒ <code>Promise</code> +Uploads and updates member photo. **Kind**: instance method of [<code>MembersService</code>](#module_services.members..MembersService) **Returns**: <code>Promise</code> - Resolves to the api response content | Param | Type | Description | | --- | --- | --- | -| S3Response | <code>Object</code> | The response from uploadFileToS3() function. | - -<a name="module_services.members..MembersService+uploadFileToS3"></a> - -#### membersService.uploadFileToS3(presignedUrlResponse) ⇒ <code>Promise</code> -Uploads file to S3. - -**Kind**: instance method of [<code>MembersService</code>](#module_services.members..MembersService) -**Returns**: <code>Promise</code> - Resolves to the api response content - -| Param | Type | Description | -| --- | --- | --- | -| presignedUrlResponse | <code>Object</code> | The presigned url response from getPresignedUrl() function. | +| userHandle | <code>String</code> | The user handle | +| file | <code>File</code> | The file to be uploaded | <a name="module_services.members..MembersService+verifyMemberNewEmail"></a> diff --git a/package.json b/package.json index 442f898e..2df0f751 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint:js": "./node_modules/.bin/eslint --ext .js,.jsx .", "test": "npm run lint && npm run jest" }, - "version": "1.2.4", + "version": "1000.28.8", "dependencies": { "auth0-js": "^6.8.4", "config": "^3.2.0", diff --git a/src/actions/profile.js b/src/actions/profile.js index bcd668cf..96a9ef5e 100644 --- a/src/actions/profile.js +++ b/src/actions/profile.js @@ -216,9 +216,7 @@ function uploadPhotoInit() {} */ function uploadPhotoDone(handle, tokenV3, file) { const service = getMembersService(tokenV3); - return service.getPresignedUrl(handle, file) - .then(res => service.uploadFileToS3(res)) - .then(res => service.updateMemberPhoto(res)) + return service.updateMemberPhoto(handle, file) .then(photoURL => ({ handle, photoURL })); } diff --git a/src/services/members.js b/src/services/members.js index 7b6648b0..61f3e9ab 100644 --- a/src/services/members.js +++ b/src/services/members.js @@ -4,11 +4,10 @@ * members via API V3. */ -/* global XMLHttpRequest */ +/* global FormData */ import _ from 'lodash'; import qs from 'qs'; import { decodeToken } from '@topcoder-platform/tc-auth-lib'; -import logger from '../utils/logger'; import { getApiResponsePayload, handleApiResponse } from '../utils/tc'; import { getApi } from './api'; @@ -238,73 +237,24 @@ class MembersService { return getApiResponsePayload(res); } - /** - * Gets presigned url for member photo file. - * @param {String} userHandle The user handle - * @param {File} file The file to get its presigned url - * @return {Promise} Resolves to the api response content - */ - async getPresignedUrl(userHandle, file) { - const res = await this.private.api.postJson(`/members/${userHandle}/photoUploadUrl`, { param: { contentType: file.type } }); - const payload = await getApiResponsePayload(res); - - return { - preSignedURL: payload.preSignedURL, - token: payload.token, - file, - userHandle, - }; - } - /** * Updates member photo. - * @param {Object} S3Response The response from uploadFileToS3() function. - * @return {Promise} Resolves to the api response content - */ - async updateMemberPhoto(S3Response) { - const res = await this.private.api.putJson(`/members/${S3Response.userHandle}/photo`, { param: S3Response.body }); - return getApiResponsePayload(res); - } - - /** - * Uploads file to S3. - * @param {Object} presignedUrlResponse The presigned url response from - * getPresignedUrl() function. + * @param {String} userHandle The user handle + * @param {File} file The photo to upload * @return {Promise} Resolves to the api response content */ - uploadFileToS3(presignedUrlResponse) { - _.noop(this); - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - xhr.open('PUT', presignedUrlResponse.preSignedURL, true); - xhr.setRequestHeader('Content-Type', presignedUrlResponse.file.type); - - xhr.onreadystatechange = () => { - const { status } = xhr; - if (((status >= 200 && status < 300) || status === 304) && xhr.readyState === 4) { - resolve({ - userHandle: presignedUrlResponse.userHandle, - body: { - token: presignedUrlResponse.token, - contentType: presignedUrlResponse.file.type, - }, - }); - } else if (status >= 400) { - const err = new Error('Could not upload image to S3'); - err.status = status; - reject(err); - } - }; - - xhr.onerror = (err) => { - logger.error('Could not upload image to S3', err); - - reject(err); - }; - - xhr.send(presignedUrlResponse.file); + async updateMemberPhoto(userHandle, file) { + const formData = new FormData(); + formData.append('photo', file); + const res = await this.private.apiV5.fetch(`/members/${userHandle}/photo`, { + method: 'POST', + headers: { + 'Content-Type': null, + }, + body: formData, }); + return handleApiResponse(res) + .then(({ photoURL }) => photoURL); } /** diff --git a/src/services/user-traits.js b/src/services/user-traits.js index c1d71b71..34d25a26 100644 --- a/src/services/user-traits.js +++ b/src/services/user-traits.js @@ -4,7 +4,7 @@ * via API V3. */ import toCapitalCase from 'to-capital-case'; -import { getApiResponsePayload } from '../utils/tc'; +import { handleApiResponse } from '../utils/tc'; import { getApi } from './api'; /** @@ -16,7 +16,7 @@ class UserTraitsService { */ constructor(tokenV3) { this.private = { - api: getApi('V3', tokenV3), + api: getApi('V5', tokenV3), tokenV3, }; } @@ -29,7 +29,7 @@ class UserTraitsService { async getAllUserTraits(handle) { // FIXME: Remove the .toLowerCase() when the API is fixed to ignore the case in the route params const res = await this.private.api.get(`/members/${handle.toLowerCase()}/traits`); - return getApiResponsePayload(res); + return handleApiResponse(res); } /** @@ -40,18 +40,16 @@ class UserTraitsService { * @return {Promise} Resolves to the member traits. */ async addUserTrait(handle, traitId, data) { - const body = { - param: [{ - traitId, - categoryName: toCapitalCase(traitId), - traits: { - data, - }, - }], - }; + const body = [{ + traitId, + categoryName: toCapitalCase(traitId), + traits: { + data, + }, + }]; const res = await this.private.api.postJson(`/members/${handle}/traits`, body); - return getApiResponsePayload(res); + return handleApiResponse(res); } /** @@ -62,18 +60,16 @@ class UserTraitsService { * @return {Promise} Resolves to the member traits. */ async updateUserTrait(handle, traitId, data) { - const body = { - param: [{ - traitId, - categoryName: toCapitalCase(traitId), - traits: { - data, - }, - }], - }; + const body = [{ + traitId, + categoryName: toCapitalCase(traitId), + traits: { + data, + }, + }]; const res = await this.private.api.putJson(`/members/${handle}/traits`, body); - return getApiResponsePayload(res); + return handleApiResponse(res); } /** @@ -84,7 +80,7 @@ class UserTraitsService { */ async deleteUserTrait(handle, traitId) { const res = await this.private.api.delete(`/members/${handle}/traits?traitIds=${traitId}`); - return getApiResponsePayload(res); + return handleApiResponse(res); } } diff --git a/src/utils/tc.js b/src/utils/tc.js index 2941de37..ff6d46f1 100644 --- a/src/utils/tc.js +++ b/src/utils/tc.js @@ -87,7 +87,8 @@ export async function getApiResponsePayload(res, shouldThrowError = true) { */ export function handleApiResponse(response) { if (!response.ok) throw new Error(response.statusText); - return response.json(); + return response.json() + .catch(() => null); } /** From 2a01da82ff62829099b3a49be08e8e07db211b88 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues <contato@luizrrodrigues.com.br> Date: Mon, 13 Dec 2021 09:18:45 -0300 Subject: [PATCH 2/3] ci: added tag test-release --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0e161844..e8eac28e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,7 @@ jobs: - attach_workspace: at: . - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - - run: npm publish + - run: npm publish --tag test-release # dont change anything workflows: version: 2 From e04f229fc34847e1b18e8c8bde8468c0a170cab6 Mon Sep 17 00:00:00 2001 From: Ubuntu <ubuntu@ip-172-31-19-225.ap-south-1.compute.internal> Date: Tue, 21 Dec 2021 10:51:40 +0000 Subject: [PATCH 3/3] update profile and skills to v5 --- __tests__/__snapshots__/index.js.snap | 2 + .../actions/__snapshots__/profile.js.snap | 2 + __tests__/actions/auth.js | 4 +- src/actions/auth.js | 8 ++-- src/actions/profile.js | 21 +++++++++ src/reducers/auth.js | 15 ++++++ src/reducers/profile.js | 45 ++++++++++++++++++ src/services/members.js | 46 +++++++++++-------- 8 files changed, 117 insertions(+), 26 deletions(-) diff --git a/__tests__/__snapshots__/index.js.snap b/__tests__/__snapshots__/index.js.snap index 283e8d01..351b4e3c 100644 --- a/__tests__/__snapshots__/index.js.snap +++ b/__tests__/__snapshots__/index.js.snap @@ -174,7 +174,9 @@ Object { "updatePasswordDone": [Function], "updatePasswordInit": [Function], "updateProfileDone": [Function], + "updateProfileDoneV5": [Function], "updateProfileInit": [Function], + "updateProfileInitV5": [Function], "uploadPhotoDone": [Function], "uploadPhotoInit": [Function], "verifyMemberNewEmailDone": [Function], diff --git a/__tests__/actions/__snapshots__/profile.js.snap b/__tests__/actions/__snapshots__/profile.js.snap index b784f238..0649581b 100644 --- a/__tests__/actions/__snapshots__/profile.js.snap +++ b/__tests__/actions/__snapshots__/profile.js.snap @@ -44,7 +44,9 @@ Object { "updatePasswordDone": [Function], "updatePasswordInit": [Function], "updateProfileDone": [Function], + "updateProfileDoneV5": [Function], "updateProfileInit": [Function], + "updateProfileInitV5": [Function], "uploadPhotoDone": [Function], "uploadPhotoInit": [Function], "verifyMemberNewEmailDone": [Function], diff --git a/__tests__/actions/auth.js b/__tests__/actions/auth.js index a36ef7e3..19cc26d5 100644 --- a/__tests__/actions/auth.js +++ b/__tests__/actions/auth.js @@ -1,5 +1,5 @@ const MOCK_GROUPS_REQ_URL = 'https://api.topcoder-dev.com/v5/groups?memberId=12345&membershipType=user'; -const MOCK_PROFILE_REQ_URL = 'https://api.topcoder-dev.com/v3/members/username12345'; +const MOCK_PROFILE_REQ_URL = 'https://api.topcoder-dev.com/v5/members/username12345'; jest.mock('isomorphic-fetch', () => jest.fn(url => Promise.resolve({ ok: true, @@ -10,7 +10,7 @@ jest.mock('isomorphic-fetch', () => jest.fn(url => Promise.resolve({ content = ['Group1', 'Group2']; break; case MOCK_PROFILE_REQ_URL: - content = { result: { content: { userId: 12345 }, status: 200 } }; + content = Promise.resolve({ userId: 12345 }); break; default: throw new Error('Unexpected URL!'); } diff --git a/src/actions/auth.js b/src/actions/auth.js index 3cec762a..c8a760b0 100644 --- a/src/actions/auth.js +++ b/src/actions/auth.js @@ -5,9 +5,10 @@ import { createActions } from 'redux-actions'; import { decodeToken } from '@topcoder-platform/tc-auth-lib'; -import { getApiV3, getApiV5 } from '../services/api'; +import { getApiV5 } from '../services/api'; import { setErrorIcon, ERROR_ICON_TYPES } from '../utils/errors'; import { getService } from '../services/groups'; +import { handleApiResponse } from '../utils/tc'; /** * Helper method that checks for HTTP error response v5 and throws Error in this case. @@ -41,11 +42,10 @@ async function checkErrorV5(res) { function loadProfileDone(userTokenV3) { if (!userTokenV3) return Promise.resolve(null); const user = decodeToken(userTokenV3); - const apiV3 = getApiV3(userTokenV3); const apiV5 = getApiV5(userTokenV3); return Promise.all([ - apiV3.get(`/members/${user.handle}`) - .then(res => res.json()).then(res => (res.result.status === 200 ? res.result.content : {})), + apiV5.get(`/members/${user.handle}`) + .then(handleApiResponse), apiV5.get(`/groups?memberId=${user.userId}&membershipType=user`) .then(checkErrorV5).then(res => res.result || []), ]).then(([profile, groups]) => ({ ...profile, groups })); diff --git a/src/actions/profile.js b/src/actions/profile.js index 96a9ef5e..e9368d0a 100644 --- a/src/actions/profile.js +++ b/src/actions/profile.js @@ -246,6 +246,25 @@ function updateProfileDone(profile, tokenV3) { return service.updateMemberProfile(profile); } +/** + * @static + * @desc Creates an action that signals beginning of updating user's profile. + * @return {Action} + */ +function updateProfileInitV5() {} + +/** + * @static + * @desc Creates an action that updates user's profile. + * @param {String} profile Topcoder user profile. + * @param {String} tokenV5 Topcoder auth token v5. + * @return {Action} + */ +function updateProfileDoneV5(profile, handle, tokenV3) { + const service = getMembersService(tokenV3); + return service.updateMemberProfileV5(profile, handle); +} + /** * @static * @desc Creates an action that signals beginning of adding user's skill. @@ -483,6 +502,8 @@ export default createActions({ DELETE_PHOTO_DONE: updateProfileDone, UPDATE_PROFILE_INIT: updateProfileInit, UPDATE_PROFILE_DONE: updateProfileDone, + UPDATE_PROFILE_INIT_V5: updateProfileInitV5, + UPDATE_PROFILE_DONE_V5: updateProfileDoneV5, ADD_SKILL_INIT: addSkillInit, ADD_SKILL_DONE: addSkillDone, HIDE_SKILL_INIT: hideSkillInit, diff --git a/src/reducers/auth.js b/src/reducers/auth.js index b20e7851..a957d6d8 100644 --- a/src/reducers/auth.js +++ b/src/reducers/auth.js @@ -106,6 +106,21 @@ function create(initialState) { }, }; }, + [profileActions.profile.updateProfileDoneV5]: (state, { payload, error }) => { + if (error) { + return state; + } + if (!state.profile || state.profile.handle !== payload.handle) { + return state; + } + return { + ...state, + profile: { + ...state.profile, + ...payload, + }, + }; + }, }, _.defaults(initialState, { authenticating: true, profile: null, diff --git a/src/reducers/profile.js b/src/reducers/profile.js index 8ca03fdf..e060f7d4 100644 --- a/src/reducers/profile.js +++ b/src/reducers/profile.js @@ -266,6 +266,49 @@ function onUpdateProfileDone(state, { payload, error }) { }; } +/** + * Handles PROFILE/UPDATE_PROFILE_DONE_V5 action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onUpdateProfileDoneV5(state, { payload, error }) { + const newState = { ...state, updatingProfile: false }; + + if (payload.isEmailConflict) { + return { + ...newState, + isEmailConflict: true, + updateProfileSuccess: false, + }; + } + + if (error) { + logger.error('Failed to update user profile', payload); + fireErrorMessage('ERROR: Failed to update user profile!'); + return { + ...newState, + updateProfileSuccess: false, + }; + } + + if (!newState.info || newState.info.handle !== payload.handle) { + return { + ...newState, + updateProfileSuccess: true, + }; + } + + return { + ...newState, + info: { + ...newState.info, + ...payload, + }, + updateProfileSuccess: true, + }; +} + /** * Handles PROFILE/ADD_SKILL_DONE action. * @param {Object} state @@ -530,6 +573,8 @@ function create(initialState) { [a.deletePhotoDone]: onDeletePhotoDone, [a.updateProfileInit]: state => ({ ...state, updatingProfile: true }), [a.updateProfileDone]: onUpdateProfileDone, + [a.updateProfileInitV5]: state => ({ ...state, updatingProfile: true }), + [a.updateProfileDoneV5]: onUpdateProfileDoneV5, [a.addSkillInit]: state => ({ ...state, addingSkill: true }), [a.addSkillDone]: onAddSkillDone, [a.hideSkillInit]: state => ({ ...state, hidingSkill: true }), diff --git a/src/services/members.js b/src/services/members.js index 61f3e9ab..5022b362 100644 --- a/src/services/members.js +++ b/src/services/members.js @@ -45,8 +45,8 @@ class MembersService { * @return {Promise} Resolves to the data object. */ async getMemberInfo(handle) { - const res = await this.private.api.get(`/members/${handle}`); - return getApiResponsePayload(res); + const res = await this.private.apiV5.get(`/members/${handle}`); + return handleApiResponse(res); } /** @@ -75,8 +75,8 @@ class MembersService { * @return {Promise} Resolves to the stats object. */ async getSkills(handle) { - const res = await this.private.api.get(`/members/${handle}/skills`); - return getApiResponsePayload(res); + const res = await this.private.apiV5.get(`/members/${handle}/skills`); + return handleApiResponse(res); } /** @@ -188,16 +188,12 @@ class MembersService { */ async addSkill(handle, skillTagId) { const body = { - param: { - skills: { - [skillTagId]: { - hidden: false, - }, - }, + [skillTagId]: { + hidden: false, }, }; - const res = await this.private.api.patchJson(`/members/${handle}/skills`, body); - return getApiResponsePayload(res); + const res = await this.private.apiV5.patchJson(`/members/${handle}/skills`, body); + return handleApiResponse(res); } /** @@ -208,19 +204,15 @@ class MembersService { */ async hideSkill(handle, skillTagId) { const body = { - param: { - skills: { - [skillTagId]: { - hidden: true, - }, - }, + [skillTagId]: { + hidden: true, }, }; - const res = await this.private.api.fetch(`/members/${handle}/skills`, { + const res = await this.private.apiV5.fetch(`/members/${handle}/skills`, { body: JSON.stringify(body), method: 'PATCH', }); - return getApiResponsePayload(res); + return handleApiResponse(res); } /** @@ -237,6 +229,20 @@ class MembersService { return getApiResponsePayload(res); } + /** + * Updates member profile. + * @param {Object} profile The profile to update. + * @return {Promise} Resolves to the api response content + */ + async updateMemberProfileV5(profile, handle) { + const url = profile.verifyUrl ? `/members/${handle}?verifyUrl=${profile.verifyUrl}` : `/members/${handle}`; + const res = await this.private.apiV5.putJson(url, profile.verifyUrl ? _.omit(profile, ['verifyUrl']) : profile); + if (profile.verifyUrl && res.status === 409) { + return Promise.resolve(Object.assign({}, profile, { isEmailConflict: true })); + } + return handleApiResponse(res); + } + /** * Updates member photo. * @param {String} userHandle The user handle