diff --git a/magic-links/assets/email-template.html b/magic-links/assets/email-template.html index 9a4bfe4f..e695cd0a 100644 --- a/magic-links/assets/email-template.html +++ b/magic-links/assets/email-template.html @@ -17,7 +17,7 @@ More details about how to set up email verification in the documentation.

- Click here to verify email

diff --git a/magic-links/assets/index.html b/magic-links/assets/index.html index a327dd20..6a56ee3c 100644 --- a/magic-links/assets/index.html +++ b/magic-links/assets/index.html @@ -1,77 +1,127 @@ - Login with Twilio Verify - - - + + + + One-Click Login with Twilio Verify + + -
-

Twilio Verify

-

- This example shows how to deploy - Twilio Verify - and Twilio functions for serverless email verification with magic links. -

+
+
+ + +
+
+
-
- -
- -
-
- - +
+

+ This example shows how to deploy + Twilio Verify + and Twilio functions for serverless email verification with magic links. +

+
+ +
+ +
+
+ +
+
-
- - + + \ No newline at end of file diff --git a/magic-links/assets/verify.html b/magic-links/assets/verify.html index 7112b58f..75c0711d 100644 --- a/magic-links/assets/verify.html +++ b/magic-links/assets/verify.html @@ -1,62 +1,100 @@ - Login with Twilio Verify - + + + + One-Click Login with Twilio Verify + - -
-

Twilio Verify

+
+
+ + +
+
+
-
+ + - \ No newline at end of file + diff --git a/magic-links/functions/check-verify.js b/magic-links/functions/check-verify.js index 210e1357..685000a0 100644 --- a/magic-links/functions/check-verify.js +++ b/magic-links/functions/check-verify.js @@ -1,13 +1,6 @@ /** - * Check Verification - * - * This Function shows you how to check a verification token for Twilio Verify. - * - * Pre-requisites - * - Create a Verify Service (https://www.twilio.com/console/verify/services) - * - Add VERIFY_SERVICE_SID from above to your Environment Variables (https://www.twilio.com/console/functions/configure) - * - Enable ACCOUNT_SID and AUTH_TOKEN in your functions configuration (https://www.twilio.com/console/functions/configure) - * + * - Check if the verification code is correct + * - docs: https://www.twilio.com/docs/verify/api/verification-check * * Returns JSON: * { @@ -15,65 +8,48 @@ * "message": string * } */ +class ApiError extends Error { + constructor(message, status) { + super(message); + this.name = 'ApiError'; + this.status = status; + } +} -// eslint-disable-next-line consistent-return -exports.handler = function (context, event, callback) { +exports.handler = async function (context, event, callback) { const response = new Twilio.Response(); response.appendHeader('Content-Type', 'application/json'); - /* - * uncomment to support CORS - * response.appendHeader('Access-Control-Allow-Origin', '*'); - * response.appendHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - * response.appendHeader('Access-Control-Allow-Headers', 'Content-Type'); - */ + try { + const { to, code } = event; + [to, code].forEach((param) => { + if (typeof param === 'undefined' || param === null) { + throw new ApiError(`Missing parameter.`, 400); + } + }); + + const client = context.getTwilioClient(); + const verificationCheck = await client.verify.v2 + .services(context.VERIFY_SERVICE_SID) + .verificationChecks.create({ to, code }); + + if (verificationCheck.status !== 'approved') { + throw new ApiError('Incorrect token.', 401); + } - if ( - typeof event.to === 'undefined' || - typeof event.verification_code === 'undefined' - ) { + response.setStatusCode(200); + response.setBody({ + success: true, + message: 'Verification success.', + }); + return callback(null, response); + } catch (error) { + console.log(error); + response.setStatusCode(error.status || 500); response.setBody({ success: false, - message: 'Missing parameter.', + message: error.message, }); - response.setStatusCode(400); return callback(null, response); } - - const client = context.getTwilioClient(); - const service = context.VERIFY_SERVICE_SID; - const { to, verification_code: code } = event; - - client.verify - .services(service) - .verificationChecks.create({ - to, - code, - }) - .then((check) => { - if (check.status === 'approved') { - response.setStatusCode(200); - response.setBody({ - success: true, - message: 'Verification success.', - }); - return callback(null, response); - } - - response.setStatusCode(401); - response.setBody({ - success: false, - message: 'Incorrect token.', - }); - return callback(null, response); - }) - .catch((error) => { - console.log(error); - response.setStatusCode(error.status); - response.setBody({ - success: false, - message: error.message, - }); - return callback(null, response); - }); }; diff --git a/magic-links/functions/start-verify.js b/magic-links/functions/start-verify.js index c40ddf6d..11e48676 100644 --- a/magic-links/functions/start-verify.js +++ b/magic-links/functions/start-verify.js @@ -1,92 +1,68 @@ +/* eslint-disable camelcase */ /** - * Start Verification - * - * This Function shows you how to send a verification token for Twilio Verify. + * - Sends an email verification code + * - docs: https://www.twilio.com/docs/verify/api/verification * * Pre-requisites * - Create a Verify Service (https://www.twilio.com/console/verify/services) - * - Add VERIFY_SERVICE_SID from above to your Environment Variables (https://www.twilio.com/console/functions/configure) - * - Enable ACCOUNT_SID and AUTH_TOKEN in your functions configuration (https://www.twilio.com/console/functions/configure) - * + * - Set up Sendgrid template & email integration (https://www.twilio.com/docs/verify/email) * * Returns JSON * { * "success": boolean, - * "error": { // not present if success is true - * "message": string, - * "moreInfo": url string - * } + * "message": string, * } */ -// eslint-disable-next-line consistent-return -exports.handler = function (context, event, callback) { +function callbackUrl(context) { + const protocol = context.DOMAIN_NAME.startsWith('localhost:') + ? 'http' + : 'https'; + + return `${protocol}://${context.DOMAIN_NAME}/${context.CALLBACK_PATH}`; +} + +exports.callbackUrl = callbackUrl; +exports.handler = async function (context, event, callback) { const response = new Twilio.Response(); response.appendHeader('Content-Type', 'application/json'); - /* - * uncomment to support CORS - * response.appendHeader('Access-Control-Allow-Origin', '*'); - * response.appendHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - * response.appendHeader('Access-Control-Allow-Headers', 'Content-Type'); - */ + try { + if (typeof event.to === 'undefined') { + throw new Error('Missing parameter; please provide an email.'); + } + + const client = context.getTwilioClient(); + const { to } = event; + const channelConfiguration = { + template_id: context.VERIFY_TEMPLATE_ID, + substitutions: { + email: to, + callback_url: exports.callbackUrl(context), + }, + }; + + const verification = await client.verify.v2 + .services(context.VERIFY_SERVICE_SID) + .verifications.create({ + channelConfiguration, + to, + channel: 'email', + }); - if (typeof event.to === 'undefined') { + response.setStatusCode(200); + response.setBody({ + success: true, + message: `Sent verification to ${to}.`, + }); + return callback(null, response); + } catch (error) { + console.error(error); + response.setStatusCode(error.status || 400); response.setBody({ success: false, - error: { - message: 'Missing parameter; please provide an email.', - moreInfo: 'https://www.twilio.com/docs/verify/api/verification', - }, + message: error.message, }); - response.setStatusCode(400); return callback(null, response); } - - const client = context.getTwilioClient(); - const service = context.VERIFY_SERVICE_SID; - const { to } = event; - const protocol = context.DOMAIN_NAME.startsWith('localhost:') - ? 'http' - : 'https'; - const callbackUrl = `${protocol}://${ - context.DOMAIN_NAME - }${context.PATH.substr(0, context.PATH.lastIndexOf('/'))}/${ - context.CALLBACK_PATH - }`; - - client.verify - .services(service) - .verifications.create({ - to, - channel: 'email', - channelConfiguration: { - substitutions: { - // used in email template - email: to, - // eslint-disable-next-line camelcase - callback_url: callbackUrl, - }, - }, - }) - .then((verification) => { - console.log(`Sent verification: '${verification.sid}'`); - response.setStatusCode(200); - response.setBody({ - success: true, - }); - callback(null, response); - }) - .catch((error) => { - console.log(error); - response.setStatusCode(error.status); - response.setBody({ - success: false, - error: { - message: error.message, - moreInfo: error.moreInfo, - }, - }); - callback(null, response); - }); }; diff --git a/magic-links/tests/check-verify.test.js b/magic-links/tests/check-verify.test.js index 305e3025..b9ab9cf4 100644 --- a/magic-links/tests/check-verify.test.js +++ b/magic-links/tests/check-verify.test.js @@ -1,19 +1,19 @@ const checkVerifyFunction = require('../functions/check-verify').handler; const helpers = require('../../test/test-helper'); -const mockService = { - verificationChecks: { - create: jest.fn(() => - Promise.resolve({ - status: 'approved', - }) - ), - }, -}; - const mockClient = { verify: { - services: jest.fn(() => mockService), + v2: { + services: jest.fn(() => ({ + verificationChecks: { + create: jest.fn(() => + Promise.resolve({ + status: 'approved', + }) + ), + }, + })), + }, }, }; @@ -30,44 +30,37 @@ describe('verify/check-verification', () => { helpers.teardown(); }); - test('returns an error response when required to parameter is missing', (done) => { + test('returns an error response when required "to" parameter is missing', (done) => { const callback = (_err, result) => { - expect(result).toBeDefined(); expect(result._body.success).toEqual(false); - expect(mockClient.verify.services).not.toHaveBeenCalledWith( + expect(result._body.message).toEqual(`Missing parameter.`); + expect(mockClient.verify.v2.services).not.toHaveBeenCalledWith( testContext.VERIFY_SERVICE_SID ); done(); }; const event = { - // eslint-disable-next-line camelcase - verification_code: '123456', + code: '123456', }; checkVerifyFunction(testContext, event, callback); }); - test('returns an error response when required verification_code parameter is missing', (done) => { + test('returns an error response when required "code" parameter is missing', (done) => { const callback = (_err, result) => { - expect(result).toBeDefined(); expect(result._body.success).toEqual(false); - expect(mockClient.verify.services).not.toHaveBeenCalledWith( - testContext.VERIFY_SERVICE_SID - ); + expect(result._body.message).toEqual(`Missing parameter.`); done(); }; const event = { - to: 'hello@example.com', + to: 'foo@bar.com', }; checkVerifyFunction(testContext, event, callback); }); - test('returns an error response when required parameters are missing', (done) => { + test('returns an error response when both required parameters are missing', (done) => { const callback = (_err, result) => { - expect(result).toBeDefined(); expect(result._body.success).toEqual(false); - expect(mockClient.verify.services).not.toHaveBeenCalledWith( - testContext.VERIFY_SERVICE_SID - ); + expect(result._body.message).toEqual('Missing parameter.'); done(); }; const event = {}; @@ -76,17 +69,56 @@ describe('verify/check-verification', () => { test('returns success with valid request', (done) => { const callback = (_err, result) => { - expect(result).toBeDefined(); expect(result._body.success).toEqual(true); - expect(mockClient.verify.services).toHaveBeenCalledWith( + expect(result._body.message).toEqual('Verification success.'); + expect(mockClient.verify.v2.services).toHaveBeenCalledWith( testContext.VERIFY_SERVICE_SID ); done(); }; const event = { to: 'hello@example.com', - // eslint-disable-next-line camelcase - verification_code: '123456', + code: '123456', + }; + checkVerifyFunction(testContext, event, callback); + }); + + test('returns error when verification fails', (done) => { + mockClient.verify.v2.services = jest.fn(() => ({ + verificationChecks: { + create: jest.fn(() => + Promise.resolve({ + status: 'pending', + }) + ), + }, + })); + + const callback = (_err, result) => { + expect(result._body.success).toEqual(false); + expect(result._body.message).toEqual('Incorrect token.'); + done(); + }; + const event = { + to: 'hello@example.com', + code: '00000', + }; + checkVerifyFunction(testContext, event, callback); + }); + + test('returns a 500 error when an unexpected error occurs', (done) => { + mockClient.verify.v2.services = jest.fn(() => { + throw new Error('Test error'); + }); + + const callback = (_err, result) => { + expect(result._body.success).toEqual(false); + expect(result._statusCode).toEqual(500); + done(); + }; + const event = { + to: 'hello@example.com', + code: '123456', }; checkVerifyFunction(testContext, event, callback); }); diff --git a/magic-links/tests/start-verify.test.js b/magic-links/tests/start-verify.test.js index 1d406f15..060d85cf 100644 --- a/magic-links/tests/start-verify.test.js +++ b/magic-links/tests/start-verify.test.js @@ -1,4 +1,5 @@ const startVerifyFunction = require('../functions/start-verify').handler; +const { callbackUrl } = require('../functions/start-verify'); const helpers = require('../../test/test-helper'); const mockService = { @@ -13,14 +14,16 @@ const mockService = { const mockClient = { verify: { - services: jest.fn(() => mockService), + v2: { + services: jest.fn(() => mockService), + }, }, }; const testContext = { VERIFY_SERVICE_SID: 'default', DOMAIN_NAME: 'example.com', - PATH: 'verify.html', + CALLBACK_PATH: 'verify.html', getTwilioClient: () => mockClient, }; @@ -32,14 +35,28 @@ describe('verify/start-verification', () => { helpers.teardown(); }); + test('constructs the correct callback URL for a live URL', () => { + const result = callbackUrl(testContext); + expect(result).toEqual('https://example.com/verify.html'); + }); + + test('constructs the correct callback URL for localhost', () => { + const context = { + DOMAIN_NAME: 'localhost:3000', + CALLBACK_PATH: 'verify.html', + }; + const result = callbackUrl(context); + expect(result).toEqual('http://localhost:3000/verify.html'); + }); + test('returns an error response when required parameters are missing', (done) => { const callback = (_err, result) => { expect(result).toBeDefined(); expect(result._body.success).toEqual(false); - expect(result._body.error.message).toEqual( + expect(result._body.message).toEqual( 'Missing parameter; please provide an email.' ); - expect(mockClient.verify.services).not.toHaveBeenCalledWith( + expect(mockClient.verify.v2.services).not.toHaveBeenCalledWith( testContext.VERIFY_SERVICE_SID ); done(); @@ -52,7 +69,10 @@ describe('verify/start-verification', () => { const callback = (_err, result) => { expect(result).toBeDefined(); expect(result._body.success).toEqual(true); - expect(mockClient.verify.services).toHaveBeenCalledWith( + expect(result._body.message).toEqual( + 'Sent verification to hello@example.com.' + ); + expect(mockClient.verify.v2.services).toHaveBeenCalledWith( testContext.VERIFY_SERVICE_SID ); done();