diff --git a/__tests__/actions/profile.js b/__tests__/actions/profile.js index 4ea366a3..b4d1848b 100644 --- a/__tests__/actions/profile.js +++ b/__tests__/actions/profile.js @@ -18,6 +18,8 @@ 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] })), @@ -45,6 +47,8 @@ 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 8fe477f2..856b90ce 100644 --- a/docs/services.members.md +++ b/docs/services.members.md @@ -25,7 +25,9 @@ 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> - * [.updateMemberPhoto(userHandle, file)](#module_services.members..MembersService+updateMemberPhoto) ⇒ <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> * [.verifyMemberNewEmail(handle, emailVerifyToken)](#module_services.members..MembersService+verifyMemberNewEmail) ⇒ <code>Promise</code> <a name="module_services.members.getService"></a> @@ -63,7 +65,9 @@ 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> - * [.updateMemberPhoto(userHandle, file)](#module_services.members..MembersService+updateMemberPhoto) ⇒ <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> * [.verifyMemberNewEmail(handle, emailVerifyToken)](#module_services.members..MembersService+verifyMemberNewEmail) ⇒ <code>Promise</code> <a name="new_module_services.members..MembersService_new"></a> @@ -252,10 +256,10 @@ Updates member profile. | --- | --- | --- | | profile | <code>Object</code> | The profile to update. | -<a name="module_services.members..MembersService+updateMemberPhoto"></a> +<a name="module_services.members..MembersService+getPresignedUrl"></a> -#### membersService.updateMemberPhoto(userHandle, file) ⇒ <code>Promise</code> -Uploads and updates member photo. +#### 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 @@ -263,7 +267,31 @@ Uploads and updates member photo. | Param | Type | Description | | --- | --- | --- | | userHandle | <code>String</code> | The user handle | -| file | <code>File</code> | The file to be uploaded | +| 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. + +**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. | <a name="module_services.members..MembersService+verifyMemberNewEmail"></a> diff --git a/package.json b/package.json index 7ab753e2..b1bf9678 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.3", + "version": "1.2.2", "dependencies": { "auth0-js": "^6.8.4", "config": "^3.2.0", diff --git a/src/actions/profile.js b/src/actions/profile.js index 96a9ef5e..bcd668cf 100644 --- a/src/actions/profile.js +++ b/src/actions/profile.js @@ -216,7 +216,9 @@ function uploadPhotoInit() {} */ function uploadPhotoDone(handle, tokenV3, file) { const service = getMembersService(tokenV3); - return service.updateMemberPhoto(handle, file) + return service.getPresignedUrl(handle, file) + .then(res => service.uploadFileToS3(res)) + .then(res => service.updateMemberPhoto(res)) .then(photoURL => ({ handle, photoURL })); } diff --git a/src/services/members.js b/src/services/members.js index 61f3e9ab..7b6648b0 100644 --- a/src/services/members.js +++ b/src/services/members.js @@ -4,10 +4,11 @@ * members via API V3. */ -/* global FormData */ +/* global XMLHttpRequest */ 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,23 +239,72 @@ class MembersService { } /** - * Updates member photo. + * Gets presigned url for member photo file. * @param {String} userHandle The user handle - * @param {File} file The photo to upload + * @param {File} file The file to get its presigned url * @return {Promise} Resolves to the api response content */ - 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, + 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. + * @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); }); - return handleApiResponse(res) - .then(({ photoURL }) => photoURL); } /** diff --git a/src/services/user-traits.js b/src/services/user-traits.js index 34d25a26..c1d71b71 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 { handleApiResponse } from '../utils/tc'; +import { getApiResponsePayload } from '../utils/tc'; import { getApi } from './api'; /** @@ -16,7 +16,7 @@ class UserTraitsService { */ constructor(tokenV3) { this.private = { - api: getApi('V5', tokenV3), + api: getApi('V3', 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 handleApiResponse(res); + return getApiResponsePayload(res); } /** @@ -40,16 +40,18 @@ class UserTraitsService { * @return {Promise} Resolves to the member traits. */ async addUserTrait(handle, traitId, data) { - const body = [{ - traitId, - categoryName: toCapitalCase(traitId), - traits: { - data, - }, - }]; + const body = { + param: [{ + traitId, + categoryName: toCapitalCase(traitId), + traits: { + data, + }, + }], + }; const res = await this.private.api.postJson(`/members/${handle}/traits`, body); - return handleApiResponse(res); + return getApiResponsePayload(res); } /** @@ -60,16 +62,18 @@ class UserTraitsService { * @return {Promise} Resolves to the member traits. */ async updateUserTrait(handle, traitId, data) { - const body = [{ - traitId, - categoryName: toCapitalCase(traitId), - traits: { - data, - }, - }]; + const body = { + param: [{ + traitId, + categoryName: toCapitalCase(traitId), + traits: { + data, + }, + }], + }; const res = await this.private.api.putJson(`/members/${handle}/traits`, body); - return handleApiResponse(res); + return getApiResponsePayload(res); } /** @@ -80,7 +84,7 @@ class UserTraitsService { */ async deleteUserTrait(handle, traitId) { const res = await this.private.api.delete(`/members/${handle}/traits?traitIds=${traitId}`); - return handleApiResponse(res); + return getApiResponsePayload(res); } } diff --git a/src/utils/tc.js b/src/utils/tc.js index ff6d46f1..2941de37 100644 --- a/src/utils/tc.js +++ b/src/utils/tc.js @@ -87,8 +87,7 @@ export async function getApiResponsePayload(res, shouldThrowError = true) { */ export function handleApiResponse(response) { if (!response.ok) throw new Error(response.statusText); - return response.json() - .catch(() => null); + return response.json(); } /**