From 165a2f1ad8860bd700609b1a10f67865c773b5e6 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 17 Nov 2020 20:26:24 -0700 Subject: [PATCH 01/15] Create verify_passcode asset. Move token.js --- src/server/assets/verify_passcode.js | 35 ++++++++++++++ src/server/functions/token.js | 70 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/server/assets/verify_passcode.js create mode 100644 src/server/functions/token.js diff --git a/src/server/assets/verify_passcode.js b/src/server/assets/verify_passcode.js new file mode 100644 index 0000000..8f58ceb --- /dev/null +++ b/src/server/assets/verify_passcode.js @@ -0,0 +1,35 @@ +/* global Twilio */ +'use strict'; + +module.exports = async (context, event, callback) => { + const { API_PASSCODE, API_PASSCODE_EXPIRY, DOMAIN_NAME } = context; + + const { passcode } = event; + const [, appID, serverlessID] = DOMAIN_NAME.match(/-?(\d*)-(\d+)(?:-\w+)?.twil.io$/); + + let response = new Twilio.Response(); + response.appendHeader('Content-Type', 'application/json'); + + if (Date.now() > API_PASSCODE_EXPIRY) { + response.setStatusCode(401); + response.setBody({ + error: { + message: 'passcode expired', + explanation: + 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.', + }, + }); + return callback(null, response); + } + + if (API_PASSCODE + appID + serverlessID !== passcode) { + response.setStatusCode(401); + response.setBody({ + error: { + message: 'passcode incorrect', + explanation: 'The passcode used to validate application users is incorrect.', + }, + }); + return callback(null, response); + } +}; diff --git a/src/server/functions/token.js b/src/server/functions/token.js new file mode 100644 index 0000000..1b0dc9c --- /dev/null +++ b/src/server/functions/token.js @@ -0,0 +1,70 @@ +/* global Twilio */ +'use strict'; + +const AccessToken = Twilio.jwt.AccessToken; +const VideoGrant = AccessToken.VideoGrant; +const MAX_ALLOWED_SESSION_DURATION = 14400; + +module.exports.handler = async (context, event, callback) => { + const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE } = context; + + const authHandler = require(Runtime.getAssets()['/auth-handler.js'].path); + authHandler(context, event, callback); + + const { user_identity, room_name, create_room = true } = event; + + let response = new Twilio.Response(); + response.appendHeader('Content-Type', 'application/json'); + + if (typeof create_room !== 'boolean') { + response.setStatusCode(400); + response.setBody({ + error: { + message: 'invalid parameter', + explanation: 'A boolean value must be provided for the create_room parameter', + }, + }); + return callback(null, response); + } + + if (!user_identity) { + response.setStatusCode(400); + response.setBody({ + error: { + message: 'missing user_identity', + explanation: 'The user_identity parameter is missing.', + }, + }); + return callback(null, response); + } + + if (create_room) { + const client = context.getTwilioClient(); + + try { + await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE }); + } catch (e) { + // Ignore 53113 error (room already exists). See: https://www.twilio.com/docs/api/errors/53113 + if (e.code !== 53113) { + response.setStatusCode(500); + response.setBody({ + error: { + message: 'error creating room', + explanation: 'Something went wrong when creating a room.', + }, + }); + return callback(null, response); + } + } + } + + const token = new AccessToken(ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, { + ttl: MAX_ALLOWED_SESSION_DURATION, + }); + token.identity = user_identity; + const videoGrant = new VideoGrant({ room: room_name }); + token.addGrant(videoGrant); + response.setStatusCode(200); + response.setBody({ token: token.toJwt(), room_type: ROOM_TYPE }); + return callback(null, response); +}; From 51332b4b11c856b854116d177dbe32534e3e7f49 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 17 Nov 2020 20:26:44 -0700 Subject: [PATCH 02/15] Delete old token server --- src/video-token-server.js | 99 --------------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 src/video-token-server.js diff --git a/src/video-token-server.js b/src/video-token-server.js deleted file mode 100644 index adace0d..0000000 --- a/src/video-token-server.js +++ /dev/null @@ -1,99 +0,0 @@ -/* global Twilio */ -'use strict'; - -const AccessToken = Twilio.jwt.AccessToken; -const VideoGrant = AccessToken.VideoGrant; -const MAX_ALLOWED_SESSION_DURATION = 14400; - -module.exports.handler = async (context, event, callback) => { - const { - ACCOUNT_SID, - TWILIO_API_KEY_SID, - TWILIO_API_KEY_SECRET, - API_PASSCODE, - API_PASSCODE_EXPIRY, - DOMAIN_NAME, - ROOM_TYPE, - } = context; - - const { user_identity, room_name, passcode, create_room = true } = event; - const [, appID, serverlessID] = DOMAIN_NAME.match(/-?(\d*)-(\d+)(?:-\w+)?.twil.io$/); - - let response = new Twilio.Response(); - response.appendHeader('Content-Type', 'application/json'); - - if (typeof create_room !== 'boolean') { - response.setStatusCode(400); - response.setBody({ - error: { - message: 'invalid parameter', - explanation: 'A boolean value must be provided for the create_room parameter', - }, - }); - return callback(null, response); - } - - if (Date.now() > API_PASSCODE_EXPIRY) { - response.setStatusCode(401); - response.setBody({ - error: { - message: 'passcode expired', - explanation: - 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.', - }, - }); - return callback(null, response); - } - - if (API_PASSCODE + appID + serverlessID !== passcode) { - response.setStatusCode(401); - response.setBody({ - error: { - message: 'passcode incorrect', - explanation: 'The passcode used to validate application users is incorrect.', - }, - }); - return callback(null, response); - } - - if (!user_identity) { - response.setStatusCode(400); - response.setBody({ - error: { - message: 'missing user_identity', - explanation: 'The user_identity parameter is missing.', - }, - }); - return callback(null, response); - } - - if (create_room) { - const client = context.getTwilioClient(); - - try { - await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE }); - } catch (e) { - // Ignore 53113 error (room already exists). See: https://www.twilio.com/docs/api/errors/53113 - if (e.code !== 53113) { - response.setStatusCode(500); - response.setBody({ - error: { - message: 'error creating room', - explanation: 'Something went wrong when creating a room.', - }, - }); - return callback(null, response); - } - } - } - - const token = new AccessToken(ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, { - ttl: MAX_ALLOWED_SESSION_DURATION, - }); - token.identity = user_identity; - const videoGrant = new VideoGrant({ room: room_name }); - token.addGrant(videoGrant); - response.setStatusCode(200); - response.setBody({ token: token.toJwt(), room_type: ROOM_TYPE }); - return callback(null, response); -}; From 7856bb855697a61734f73b4bf1da68eccbe1b579 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 17 Nov 2020 20:44:39 -0700 Subject: [PATCH 03/15] Add recordingRules route --- src/server/functions/recordingrules.js | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/server/functions/recordingrules.js diff --git a/src/server/functions/recordingrules.js b/src/server/functions/recordingrules.js new file mode 100644 index 0000000..b97c581 --- /dev/null +++ b/src/server/functions/recordingrules.js @@ -0,0 +1,29 @@ +/* global Twilio */ +'use strict'; + +// We need to use a newer twilio client than the one provided by context.getTwilioClient(), +// so we require it here. The version is specified in helpers.js in the 'deployOptions' object. +const twilio = require('twilio'); + +module.exports.handler = async (context, event, callback) => { + const authHandler = require(Runtime.getAssets()['/auth-handler.js'].path); + authHandler(context, event, callback); + + const client = twilio(context.ACCOUNT_SID, context.AUTH_TOKEN); + + let response = new Twilio.Response(); + response.appendHeader('Content-Type', 'application/json'); + + try { + const recordingRulesResponse = await client.video + .rooms(event.room_sid) + .recordingRules.update({ rules: event.rules }); + response.setStatusCode(200); + response.setBody(recordingRulesResponse); + } catch (err) { + response.setStatusCode(500); + response.setBody({ error: { message: err.message, code: err.code } }); + } + + callback(null, response); +}; From 10a23cb266d391306776ce2f40ab6b40597041ef Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 17 Nov 2020 21:04:03 -0700 Subject: [PATCH 04/15] Move functions and assets from server/ to serverless/ --- src/{server => serverless}/assets/verify_passcode.js | 0 src/{server => serverless}/functions/recordingrules.js | 0 src/{server => serverless}/functions/token.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/{server => serverless}/assets/verify_passcode.js (100%) rename src/{server => serverless}/functions/recordingrules.js (100%) rename src/{server => serverless}/functions/token.js (100%) diff --git a/src/server/assets/verify_passcode.js b/src/serverless/assets/verify_passcode.js similarity index 100% rename from src/server/assets/verify_passcode.js rename to src/serverless/assets/verify_passcode.js diff --git a/src/server/functions/recordingrules.js b/src/serverless/functions/recordingrules.js similarity index 100% rename from src/server/functions/recordingrules.js rename to src/serverless/functions/recordingrules.js diff --git a/src/server/functions/token.js b/src/serverless/functions/token.js similarity index 100% rename from src/server/functions/token.js rename to src/serverless/functions/token.js From 6f3d199720b6e22f215c4bb023d3fe0c6a24970f Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 17 Nov 2020 21:25:45 -0700 Subject: [PATCH 05/15] Update helpers file to deploy new files --- src/helpers.js | 56 ++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index 1d8258e..1905fa6 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -46,19 +46,28 @@ async function getAssets(folder) { }); const indexHTML = assets.find(asset => asset.name.includes('index.html')); + const authHandlerFn = fs.readFileSync(path.join(__dirname, './serverless/assets/verify_passcode.js')); - assets.push({ - ...indexHTML, - path: '/', - name: '/', - }); - assets.push({ - ...indexHTML, - path: '/login', - name: '/login', - }); + const allAssets = assets.concat([ + { + ...indexHTML, + path: '/', + name: '/', + }, + { + ...indexHTML, + path: '/login', + name: '/login', + }, + { + name: 'auth-handler', + path: '/auth-handler.js', + content: authHandlerFn, + access: 'private', + }, + ]); - return assets; + return allAssets; } async function findApp() { @@ -80,7 +89,7 @@ async function getAppInfo() { const assets = await appInstance.assets.list(); const functions = await appInstance.functions.list(); - const tokenServerFunction = functions.find(fn => fn.friendlyName === 'token'); + const tokenServerFunction = functions.find(fn => fn.friendlyName.includes('token')); const passcodeVar = variables.find(v => v.key === 'API_PASSCODE'); const expiryVar = variables.find(v => v.key === 'API_PASSCODE_EXPIRY'); @@ -130,6 +139,10 @@ async function displayAppInfo() { async function deploy() { const assets = this.flags['app-directory'] ? await getAssets(this.flags['app-directory']) : []; + const { functions } = await getListOfFunctionsAndAssets(__dirname, { + functionsFolderNames: ['serverless/functions'], + assetsFolderNames: [], + }); if (this.twilioClient.username === this.twilioClient.accountSid) { // When twilioClient.username equals twilioClient.accountSid, it means that the user @@ -158,8 +171,6 @@ TWILIO_API_SECRET = the secret for the API Key`); const pin = getRandomInt(6); const expiryTime = Date.now() + EXPIRY_PERIOD; - const fn = fs.readFileSync(path.join(__dirname, './video-token-server.js')); - cli.action.start('deploying app'); const deployOptions = { @@ -170,17 +181,14 @@ TWILIO_API_SECRET = the secret for the API Key`); API_PASSCODE_EXPIRY: expiryTime, ROOM_TYPE: this.flags['room-type'], }, - pkgJson: {}, - functionsEnv: 'dev', - functions: [ - { - name: 'token', - path: '/token', - content: fn, - access: 'public', + pkgJson: { + dependencies: { + twilio: '^3.51.0', }, - ], - assets: assets, + }, + functionsEnv: 'dev', + functions, + assets, }; if (this.appInfo && this.appInfo.sid) { From c5de2dd0c3c9397e0c07df843496fd9b57f54f84 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Wed, 18 Nov 2020 19:30:45 -0700 Subject: [PATCH 06/15] Add tests for verify_passcode and fix tests for token --- .../serverless/assets/verify_passcode.test.js | 59 +++++++++++++++++++ .../functions/token.test.js} | 59 +++---------------- test/setupTests.js | 13 ++++ 3 files changed, 79 insertions(+), 52 deletions(-) create mode 100644 test/serverless/assets/verify_passcode.test.js rename test/{video-token-server.test.js => serverless/functions/token.test.js} (75%) diff --git a/test/serverless/assets/verify_passcode.test.js b/test/serverless/assets/verify_passcode.test.js new file mode 100644 index 0000000..ebef928 --- /dev/null +++ b/test/serverless/assets/verify_passcode.test.js @@ -0,0 +1,59 @@ +const verifyPasscode = jest.requireActual('../../../src/serverless/assets/verify_passcode'); + +describe('the verify_passcode asset', () => { + it('should return an "unauthorized" error when the passcode is incorrect', () => { + Date.now = () => 5; + const mockCallback = jest.fn(); + verifyPasscode( + { API_PASSCODE: '123456', API_PASSCODE_EXPIRY: '10', DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io' }, + { passcode: '9876543210' }, + mockCallback + ); + + expect(mockCallback).toHaveBeenCalledWith(null, { + body: { + error: { + message: 'passcode incorrect', + explanation: 'The passcode used to validate application users is incorrect.', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 401, + }); + }); + + it('should return an "expired" error when the current time is past the API_PASSCODE_EXPIRY time', () => { + Date.now = () => 15; + const mockCallback = jest.fn(); + + verifyPasscode( + { API_PASSCODE: '123456', API_PASSCODE_EXPIRY: '10', DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io' }, + { passcode: '12345612345678', user_identity: 'test identity' }, + mockCallback + ); + + expect(mockCallback).toHaveBeenCalledWith(null, { + body: { + error: { + message: 'passcode expired', + explanation: + 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 401, + }); + }); + + it('should not call the callback function when the passcode is correct and not expired', () => { + Date.now = () => 5; + const mockCallback = jest.fn(); + verifyPasscode( + { API_PASSCODE: '123456', API_PASSCODE_EXPIRY: '10', DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io' }, + { passcode: '12345612345678' }, + mockCallback + ); + + expect(mockCallback).not.toHaveBeenCalled(); + }); +}); diff --git a/test/video-token-server.test.js b/test/serverless/functions/token.test.js similarity index 75% rename from test/video-token-server.test.js rename to test/serverless/functions/token.test.js index 90b2520..0524de1 100644 --- a/test/video-token-server.test.js +++ b/test/serverless/functions/token.test.js @@ -1,4 +1,4 @@ -const { handler } = require('../src/video-token-server'); +const { handler } = require('../../../src/serverless/functions/token'); const jwt = require('jsonwebtoken'); const { set } = require('lodash'); @@ -8,64 +8,23 @@ const mockCreateFunction = jest.fn(); const mockTwilioClient = set({}, 'video.rooms.create', mockCreateFunction); +Date.now = () => 5; + const mockContext = { ACCOUNT_SID: 'AC1234', TWILIO_API_KEY_SID: 'SK1234', TWILIO_API_KEY_SECRET: 'api_secret', - API_PASSCODE: '123456', - API_PASSCODE_EXPIRY: '10', - DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io', ROOM_TYPE: 'group', getTwilioClient: () => mockTwilioClient, }; describe('the video-token-server', () => { beforeEach(() => { - Date.now = () => 5; mockCreateFunction.mockImplementation(() => Promise.resolve()); }); - it('should return an "unauthorized" error when the passcode is incorrect', () => { - handler(mockContext, { passcode: '9876543210', user_identity: 'test identity' }, callback); - - expect(callback).toHaveBeenCalledWith(null, { - body: { - error: { - message: 'passcode incorrect', - explanation: 'The passcode used to validate application users is incorrect.', - }, - }, - headers: { 'Content-Type': 'application/json' }, - statusCode: 401, - }); - }); - - it('should return an "expired" error when the current time is past the API_PASSCODE_EXPIRY time', () => { - Date.now = () => 15; - - handler(mockContext, { passcode: '12345612345678', user_identity: 'test identity' }, callback); - - expect(callback).toHaveBeenCalledWith(null, { - body: { - error: { - message: 'passcode expired', - explanation: - 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.', - }, - }, - headers: { 'Content-Type': 'application/json' }, - statusCode: 401, - }); - }); - it('should return an "invalid parameter" error when the create_room parameter is not a boolean', async () => { - Date.now = () => 5; - - await handler( - mockContext, - { passcode: '12345612345678', user_identity: 'test identity', create_room: 'no thanks' }, - callback - ); + await handler(mockContext, { user_identity: 'test identity', create_room: 'no thanks' }, callback); expect(callback).toHaveBeenCalledWith(null, { body: { @@ -80,7 +39,7 @@ describe('the video-token-server', () => { }); it('should return a "missing user_identity" error when the "user_identity" parameter is not supplied', () => { - handler(mockContext, { passcode: '12345612345678' }, callback); + handler(mockContext, {}, callback); expect(callback).toHaveBeenCalledWith(null, { body: { @@ -95,7 +54,7 @@ describe('the video-token-server', () => { }); it('should return a token when no room_name is supplied', async () => { - await handler(mockContext, { passcode: '12345612345678', user_identity: 'test identity' }, callback); + await handler(mockContext, { user_identity: 'test identity' }, callback); expect(callback).toHaveBeenCalledWith(null, { body: { token: expect.any(String), room_type: 'group' }, @@ -118,11 +77,7 @@ describe('the video-token-server', () => { describe('when passcode, room_name, and user_identity parameters are supplied', () => { it('should return a valid token', async () => { - await handler( - mockContext, - { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user' }, - callback - ); + await handler(mockContext, { room_name: 'test-room', user_identity: 'test-user' }, callback); expect(callback).toHaveBeenCalledWith(null, { body: { token: expect.any(String), room_type: 'group' }, diff --git a/test/setupTests.js b/test/setupTests.js index 8ae2918..af5d86c 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -20,3 +20,16 @@ class Response { global.Twilio = require('twilio'); global.Twilio.Response = Response; + +const verifyPasscodePath = `${__dirname}/../src/serverless/assets/verify_passcode.js`; + +global.Runtime = { + getAssets: () => ({ + '/auth-handler.js': { + path: verifyPasscodePath, + }, + }), +}; + +// Mocking this as a no-op since this function is tested in 'tests/serverless/assets/verify_passcode.ts'. +jest.mock(`${__dirname}/../src/serverless/assets/verify_passcode.js`, () => () => {}); From 93efc815813c5582ba9419f5913f51d451675ed3 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Wed, 18 Nov 2020 20:30:49 -0700 Subject: [PATCH 07/15] Add parameter validation and tests to recordingrules route --- src/serverless/functions/recordingrules.js | 32 +++++-- .../functions/recordingrules.test.js | 88 +++++++++++++++++++ test/setupTests.js | 2 +- 3 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 test/serverless/functions/recordingrules.test.js diff --git a/src/serverless/functions/recordingrules.js b/src/serverless/functions/recordingrules.js index b97c581..9f8af73 100644 --- a/src/serverless/functions/recordingrules.js +++ b/src/serverless/functions/recordingrules.js @@ -9,15 +9,37 @@ module.exports.handler = async (context, event, callback) => { const authHandler = require(Runtime.getAssets()['/auth-handler.js'].path); authHandler(context, event, callback); - const client = twilio(context.ACCOUNT_SID, context.AUTH_TOKEN); - let response = new Twilio.Response(); response.appendHeader('Content-Type', 'application/json'); + const { room_sid, rules } = event; + + if (typeof room_sid === 'undefined') { + response.setStatusCode(400); + response.setBody({ + error: { + message: 'missing room_sid', + explanation: 'The room_sid parameter is missing.', + }, + }); + return callback(null, response); + } + + if (typeof rules === 'undefined') { + response.setStatusCode(400); + response.setBody({ + error: { + message: 'missing rules', + explanation: 'The rules parameter is missing.', + }, + }); + return callback(null, response); + } + + const client = twilio(context.ACCOUNT_SID, context.AUTH_TOKEN); + try { - const recordingRulesResponse = await client.video - .rooms(event.room_sid) - .recordingRules.update({ rules: event.rules }); + const recordingRulesResponse = await client.video.rooms(room_sid).recordingRules.update({ rules }); response.setStatusCode(200); response.setBody(recordingRulesResponse); } catch (err) { diff --git a/test/serverless/functions/recordingrules.test.js b/test/serverless/functions/recordingrules.test.js new file mode 100644 index 0000000..1d80c4b --- /dev/null +++ b/test/serverless/functions/recordingrules.test.js @@ -0,0 +1,88 @@ +const { handler } = require('../../../src/serverless/functions/recordingrules'); +const twilio = require('twilio'); + +const mockUpdateFn = jest.fn(() => Promise.resolve('mockSuccessResponse')); + +const mockClient = jest.fn(() => ({ + video: { + rooms: jest.fn(() => ({ + recordingRules: { + update: mockUpdateFn, + }, + })), + }, +})); + +jest.mock('twilio'); +twilio.mockImplementation(mockClient); + +describe('the recordingrules function', () => { + it('should correctly respond when a room update is successful', async () => { + const mockCallback = jest.fn(); + + await handler( + { ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' }, + { room_sid: 'mockRoomSid', rules: 'mockRules' }, + mockCallback + ); + + expect(mockClient).toHaveBeenCalledWith('1234', '2345'); + expect(mockCallback).toHaveBeenCalledWith(null, { + body: 'mockSuccessResponse', + headers: { 'Content-Type': 'application/json' }, + statusCode: 200, + }); + }); + + it('should correctly respond when a room update is not successful', async () => { + const mockCallback = jest.fn(); + const mockError = { message: 'mockErrorMesage', code: 123 }; + mockUpdateFn.mockImplementationOnce(() => Promise.reject(mockError)); + + await handler( + { ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' }, + { room_sid: 'mockRoomSid', rules: 'mockRules' }, + mockCallback + ); + + expect(mockCallback).toHaveBeenCalledWith(null, { + body: { error: mockError }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 500, + }); + }); + + it('should return a "missing room_sid" error when the room_sid is absent', async () => { + const mockCallback = jest.fn(); + + await handler({ ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' }, { rules: 'mockRules' }, mockCallback); + + expect(mockCallback).toHaveBeenCalledWith(null, { + body: { + error: { + message: 'missing room_sid', + explanation: 'The room_sid parameter is missing.', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 400, + }); + }); + + it('should return a "missing rules" error when the rules array is absent', async () => { + const mockCallback = jest.fn(); + + await handler({ ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' }, { room_sid: 'mockSid' }, mockCallback); + + expect(mockCallback).toHaveBeenCalledWith(null, { + body: { + error: { + message: 'missing rules', + explanation: 'The rules parameter is missing.', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 400, + }); + }); +}); diff --git a/test/setupTests.js b/test/setupTests.js index af5d86c..87550eb 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -32,4 +32,4 @@ global.Runtime = { }; // Mocking this as a no-op since this function is tested in 'tests/serverless/assets/verify_passcode.ts'. -jest.mock(`${__dirname}/../src/serverless/assets/verify_passcode.js`, () => () => {}); +jest.doMock(verifyPasscodePath, () => () => {}); From 72fffc8bdce2cf440c319e9c1d67c37a13dc83dd Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Mon, 23 Nov 2020 12:00:54 -0700 Subject: [PATCH 08/15] Fix helpers tests --- test/helpers/helpers.test.js | 49 +++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/test/helpers/helpers.test.js b/test/helpers/helpers.test.js index ae8723c..b8c4ce8 100644 --- a/test/helpers/helpers.test.js +++ b/test/helpers/helpers.test.js @@ -114,23 +114,38 @@ describe('the verifyAppDirectory function', () => { describe('the getAssets function', () => { it('should add index.html at "/" and "/login" paths', async () => { - expect(await getAssets('mockFolder')).toEqual([ - { - name: 'index.html', - path: 'index.html', - content: 'mockHTMLcontent', - }, - { - name: '/', - path: '/', - content: 'mockHTMLcontent', - }, - { - name: '/login', - path: '/login', - content: 'mockHTMLcontent', - }, - ]); + expect(await getAssets('mockFolder')).toEqual( + expect.arrayContaining([ + { + name: 'index.html', + path: 'index.html', + content: 'mockHTMLcontent', + }, + { + name: '/', + path: '/', + content: 'mockHTMLcontent', + }, + { + name: '/login', + path: '/login', + content: 'mockHTMLcontent', + }, + ]) + ); + }); + + it('should add the auth-handler.js as a private asset', async () => { + expect(await getAssets('mockFolder')).toEqual( + expect.arrayContaining([ + { + name: 'auth-handler', + path: '/auth-handler.js', + content: expect.any(Buffer), + access: 'private', + }, + ]) + ); }); it('should use the CWD when provided with a relative path', async () => { From 3638ef5375c305148d1de8b07607d5b163a4c6e4 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Fri, 4 Dec 2020 14:28:54 -0700 Subject: [PATCH 09/15] Change assets/verify_passcode.js to middleware/auth.js --- src/helpers.js | 2 +- .../{assets/verify_passcode.js => middleware/auth.js} | 0 .../verify_passcode.test.js => middleware/auth.test.js} | 4 ++-- test/setupTests.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/serverless/{assets/verify_passcode.js => middleware/auth.js} (100%) rename test/serverless/{assets/verify_passcode.test.js => middleware/auth.test.js} (96%) diff --git a/src/helpers.js b/src/helpers.js index 1905fa6..d9ce8eb 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -46,7 +46,7 @@ async function getAssets(folder) { }); const indexHTML = assets.find(asset => asset.name.includes('index.html')); - const authHandlerFn = fs.readFileSync(path.join(__dirname, './serverless/assets/verify_passcode.js')); + const authHandlerFn = fs.readFileSync(path.join(__dirname, './serverless/middleware/auth.js')); const allAssets = assets.concat([ { diff --git a/src/serverless/assets/verify_passcode.js b/src/serverless/middleware/auth.js similarity index 100% rename from src/serverless/assets/verify_passcode.js rename to src/serverless/middleware/auth.js diff --git a/test/serverless/assets/verify_passcode.test.js b/test/serverless/middleware/auth.test.js similarity index 96% rename from test/serverless/assets/verify_passcode.test.js rename to test/serverless/middleware/auth.test.js index ebef928..fa73511 100644 --- a/test/serverless/assets/verify_passcode.test.js +++ b/test/serverless/middleware/auth.test.js @@ -1,6 +1,6 @@ -const verifyPasscode = jest.requireActual('../../../src/serverless/assets/verify_passcode'); +const verifyPasscode = jest.requireActual('../../../src/serverless/middleware/auth'); -describe('the verify_passcode asset', () => { +describe('the auth middleware', () => { it('should return an "unauthorized" error when the passcode is incorrect', () => { Date.now = () => 5; const mockCallback = jest.fn(); diff --git a/test/setupTests.js b/test/setupTests.js index 87550eb..14c10fe 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -21,7 +21,7 @@ class Response { global.Twilio = require('twilio'); global.Twilio.Response = Response; -const verifyPasscodePath = `${__dirname}/../src/serverless/assets/verify_passcode.js`; +const verifyPasscodePath = `${__dirname}/../src/serverless/middleware/auth.js`; global.Runtime = { getAssets: () => ({ @@ -31,5 +31,5 @@ global.Runtime = { }), }; -// Mocking this as a no-op since this function is tested in 'tests/serverless/assets/verify_passcode.ts'. +// Mocking this as a no-op since this function is tested in 'tests/serverless/middleware/auth.ts'. jest.doMock(verifyPasscodePath, () => () => {}); From 81ec0e322b4367f0fd0352b26b22959f45d6ff70 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Thu, 17 Dec 2020 09:10:14 -0700 Subject: [PATCH 10/15] Fix issue where middleware was not uploaded when an app-directory was not provided Also, changed logic in getAppInfo so web app URL is only display when web assets exist --- src/helpers.js | 21 +++++++++++++++------ test/helpers/helpers.test.js | 17 ++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index d9ce8eb..69e3df8 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -46,7 +46,6 @@ async function getAssets(folder) { }); const indexHTML = assets.find(asset => asset.name.includes('index.html')); - const authHandlerFn = fs.readFileSync(path.join(__dirname, './serverless/middleware/auth.js')); const allAssets = assets.concat([ { @@ -59,15 +58,22 @@ async function getAssets(folder) { path: '/login', name: '/login', }, + ]); + + return allAssets; +} + +function getMiddleware() { + const authHandlerFn = fs.readFileSync(path.join(__dirname, './serverless/middleware/auth.js')); + + return [ { name: 'auth-handler', path: '/auth-handler.js', content: authHandlerFn, access: 'private', }, - ]); - - return allAssets; + ]; } async function findApp() { @@ -106,7 +112,7 @@ async function getAppInfo() { expiry: moment(Number(expiry)).toString(), sid: app.sid, passcode: fullPasscode, - hasAssets: Boolean(assets.length), + hasWebAssets: Boolean(assets.find(asset => asset.friendlyName.includes('index.html'))), roomType, environmentSid: environment.sid, functionSid: tokenServerFunction.sid, @@ -121,7 +127,7 @@ async function displayAppInfo() { return; } - if (appInfo.hasAssets) { + if (appInfo.hasWebAssets) { console.log(`Web App URL: ${appInfo.url}`); } @@ -144,6 +150,8 @@ async function deploy() { assetsFolderNames: [], }); + assets.push(...getMiddleware()); + if (this.twilioClient.username === this.twilioClient.accountSid) { // When twilioClient.username equals twilioClient.accountSid, it means that the user // authenticated with the Twilio CLI by providing their Account SID and Auth Token @@ -213,6 +221,7 @@ module.exports = { displayAppInfo, findApp, getAssets, + getMiddleware, getAppInfo, getPasscode, getRandomInt, diff --git a/test/helpers/helpers.test.js b/test/helpers/helpers.test.js index b8c4ce8..7f239d4 100644 --- a/test/helpers/helpers.test.js +++ b/test/helpers/helpers.test.js @@ -8,6 +8,7 @@ const { getPasscode, getRandomInt, verifyAppDirectory, + getMiddleware, } = require('../../src/helpers'); const { getListOfFunctionsAndAssets } = require('@twilio-labs/serverless-api/dist/utils/fs'); const path = require('path'); @@ -43,7 +44,7 @@ function getMockTwilioInstance(options) { }; const mockAppInstance = { - assets: { list: () => Promise.resolve(options.hasAssets ? [{}] : []) }, + assets: { list: () => Promise.resolve(options.hasWebAssets ? [{ friendlyName: 'index.html' }] : []) }, functions: {}, update: jest.fn(() => Promise.resolve()), }; @@ -134,9 +135,11 @@ describe('the getAssets function', () => { ]) ); }); +}); +describe('the getMiddleware function', () => { it('should add the auth-handler.js as a private asset', async () => { - expect(await getAssets('mockFolder')).toEqual( + expect(await getMiddleware('mockFolder')).toEqual( expect.arrayContaining([ { name: 'auth-handler', @@ -200,7 +203,7 @@ describe('the getAppInfo function', () => { expiry: 'Wed May 20 2020 18:40:00 GMT+0000', environmentSid: 'environmentSid', functionSid: 'tokenFunctionSid', - hasAssets: false, + hasWebAssets: false, passcode: '12345612345678', sid: 'appSid', url: `https://${APP_NAME}-1234-5678-dev.twil.io?passcode=12345612345678`, @@ -208,15 +211,15 @@ describe('the getAppInfo function', () => { }); }); - it('should return the correct information when there are assets', async () => { + it('should return the correct information when there are web assets', async () => { const result = await getAppInfo.call({ - twilioClient: getMockTwilioInstance({ exists: true, hasAssets: true }), + twilioClient: getMockTwilioInstance({ exists: true, hasWebAssets: true }), }); expect(result).toEqual({ expiry: 'Wed May 20 2020 18:40:00 GMT+0000', environmentSid: 'environmentSid', functionSid: 'tokenFunctionSid', - hasAssets: true, + hasWebAssets: true, passcode: '12345612345678', sid: 'appSid', url: `https://${APP_NAME}-1234-5678-dev.twil.io?passcode=12345612345678`, @@ -251,7 +254,7 @@ describe('the displayAppInfo function', () => { it('should display the correct information when there are assets', async () => { await displayAppInfo.call({ - twilioClient: getMockTwilioInstance({ exists: true, hasAssets: true }), + twilioClient: getMockTwilioInstance({ exists: true, hasWebAssets: true }), }); expect(stdout.output).toMatchInlineSnapshot(` "Web App URL: https://${APP_NAME}-1234-5678-dev.twil.io?passcode=12345612345678 From c2bdc81818e9bc5cc2e594d366bcc11aee7680bf Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Fri, 18 Dec 2020 11:18:13 -0700 Subject: [PATCH 11/15] Fix lint error --- src/serverless/functions/recordingrules.js | 2 +- src/serverless/functions/token.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serverless/functions/recordingrules.js b/src/serverless/functions/recordingrules.js index 9f8af73..ecdfc11 100644 --- a/src/serverless/functions/recordingrules.js +++ b/src/serverless/functions/recordingrules.js @@ -1,4 +1,4 @@ -/* global Twilio */ +/* global Twilio Runtime */ 'use strict'; // We need to use a newer twilio client than the one provided by context.getTwilioClient(), diff --git a/src/serverless/functions/token.js b/src/serverless/functions/token.js index 1b0dc9c..4952811 100644 --- a/src/serverless/functions/token.js +++ b/src/serverless/functions/token.js @@ -1,4 +1,4 @@ -/* global Twilio */ +/* global Twilio Runtime */ 'use strict'; const AccessToken = Twilio.jwt.AccessToken; From ed99adc0d2e8f42ea6bf1697ce45c49406e841b9 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Fri, 18 Dec 2020 11:22:51 -0700 Subject: [PATCH 12/15] Add todo comment --- src/serverless/functions/recordingrules.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/serverless/functions/recordingrules.js b/src/serverless/functions/recordingrules.js index ecdfc11..8bf80f2 100644 --- a/src/serverless/functions/recordingrules.js +++ b/src/serverless/functions/recordingrules.js @@ -3,6 +3,7 @@ // We need to use a newer twilio client than the one provided by context.getTwilioClient(), // so we require it here. The version is specified in helpers.js in the 'deployOptions' object. +// TODO: replace with context.getTwilioClient() when https://issues.corp.twilio.com/browse/RUN-3731 is complete const twilio = require('twilio'); module.exports.handler = async (context, event, callback) => { From 28d04237a450f27e7eb1be77d5cbc5a945f48adf Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 5 Jan 2021 13:48:13 -0700 Subject: [PATCH 13/15] Update readme to include recordingrules endpoint --- README.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ced5a92..fa2d149 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ The following section documents the application [token server](/src/video-token- ##### Authentication -The application token server requires an [authentication mechanism](#twilio-rtcappsvideodeploy---authentication-auth) to be specified when deploying. The following section documents each support authentication mechanism. +The application token server requires an [authentication mechanism](#twilio-rtcappsvideodeploy---authentication-auth) to be specified when deploying. The following section documents each supported authentication mechanism. ###### Passcode @@ -189,6 +189,108 @@ POST /token +##### Recording Rules + +Changes the Recording Rules for a given room SID. + +```shell +POST /recordingrules +``` + +###### Parameters + +| Name | Type | Description | +| ---------- | -------- | ------------------------------------------------------------------- | +| `passcode` | `string` | **Required**. The application passcode. | +| `room_sid` | `string` | **Required**. The SID of the room to change the recording rules of. | +| `rules` | `array` | **Required**. An array of recording rules to apply to the room. | + +###### Success Responses + + + + + + + + + + +
Status Response
200 + +```json +{ + "roomSid": "RM00000000000000000000000000000000", + "rules": [ + { + "all": true, + "type": "exclude" + } + ], + "dateCreated": "2020-11-18T02:58:20.000Z", + "dateUpdated": "2020-11-18T03:21:18.000Z" +} +``` + +
+ +###### Error Responses + + + + + + + + + + + + + + + + + + + + + +
Status Response
400 + +```json +{ + "error": { + "message": "missing room_sid", + "explanation": "The room_sid parameter is missing." + } +} +``` + +
400 + +```json +{ + "error": { + "message": "missing rules", + "explanation": "The rules parameter is missing." + } +} +``` + +
401 + +```json +{ + "error": { + "message": "passcode incorrect", + "explanation": "The passcode used to validate application users is incorrect." + } +} +``` + +
+ ## Commands From 2f2a5215c4b1abe34a9d7dbff17a1f50a5ad2ab0 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 5 Jan 2021 16:56:24 -0700 Subject: [PATCH 14/15] Update changelog and bump version --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41fb14a..688e289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.7.0 + +### Enhancements + +- This version adds a new endpoint which allows users to update the recording rules for a given room. See the [README](README.md#recording-rules) for more details. +- Upgraded @twilio/cli-core from 5.9.1 to 5.9.3 +- Upgraded moment from 2.28.0 to 2.29.0 +- Upgraded nanoid from 3.1.13 to 3.1.16 +- Upgraded @twilio-labs/serverless-api from 4.0.2 to 4.0.3 + ## 0.6.0 ### Enhancements diff --git a/package-lock.json b/package-lock.json index 55fe888..d808961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@twilio-labs/plugin-rtc", - "version": "0.6.1", + "version": "0.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9b19e39..3caff93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@twilio-labs/plugin-rtc", - "version": "0.6.1", + "version": "0.7.0", "description": "A Twilio-CLI plugin for real-time communication apps", "main": "index.js", "publishConfig": { From dfffe25d3dbc1dc3fd85a5dd277df65d652a16e7 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Tue, 5 Jan 2021 17:00:32 -0700 Subject: [PATCH 15/15] Improve readme headings --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fa2d149..50710df 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ A mobile and web collaboration application built with Twilio Programmable Video. - [iOS App](https://github.com/twilio/twilio-video-app-ios) - [Android App](https://github.com/twilio/twilio-video-app-android) -#### Token Server API Documentation +## Token Server API Documentation The following section documents the application [token server](/src/video-token-server.js) used to provide [Programable Video access tokens](https://www.twilio.com/docs/video/tutorials/user-identity-access-tokens) to supported Twilio Video applications. The token server is deployed as a [Twilio Function](https://www.twilio.com/docs/runtime/functions). @@ -64,11 +64,11 @@ The following section documents the application [token server](/src/video-token- | ------ | ------------------ | | POST | [`/token`](#token) | -##### Authentication +### Authentication The application token server requires an [authentication mechanism](#twilio-rtcappsvideodeploy---authentication-auth) to be specified when deploying. The following section documents each supported authentication mechanism. -###### Passcode +#### Passcode Each request is verified using a passcode generated at deploy time. Passcodes remain valid for one week. After the passcode expires, users can redeploy an application and a new passcode will be generated. The snippet below provides an example request body used by a supported application. @@ -80,7 +80,7 @@ Each request is verified using a passcode generated at deploy time. Passcodes re } ``` -##### Token +### Token Returns a Programmable Video Access token. @@ -88,7 +88,7 @@ Returns a Programmable Video Access token. POST /token ``` -###### Parameters +#### Parameters | Name | Type | Description | | --------------- | --------- | -------------------------------------------------------------------------------------- | @@ -97,7 +97,7 @@ POST /token | `room_name` | `string` | A room name that will be used to create a token scoped to connecting to only one room. | | `create_room` | `boolean` | (default: `true`) When false, a room will not be created when a token is requested. | -###### Success Responses +#### Success Responses @@ -119,7 +119,7 @@ POST /token
-###### Error Responses +#### Error Responses @@ -189,7 +189,7 @@ POST /token
-##### Recording Rules +### Recording Rules Changes the Recording Rules for a given room SID. @@ -197,7 +197,7 @@ Changes the Recording Rules for a given room SID. POST /recordingrules ``` -###### Parameters +#### Parameters | Name | Type | Description | | ---------- | -------- | ------------------------------------------------------------------- | @@ -205,7 +205,7 @@ POST /recordingrules | `room_sid` | `string` | **Required**. The SID of the room to change the recording rules of. | | `rules` | `array` | **Required**. An array of recording rules to apply to the room. | -###### Success Responses +#### Success Responses @@ -234,7 +234,7 @@ POST /recordingrules
-###### Error Responses +#### Error Responses