Photo by Michael Marais on Unsplash
In one week, we partnered with local authorities to build a volunteer call center app to help vulnerable populations during the current crisis.
On Wednesday, March 18, the Israeli Ministry of Finance and Welfare asked Wix to build a system to organize their outreach efforts to senior citizens and those in other at risk groups. Their volunteers would be calling to check on one million Israeli individuals: do they need food delivered; home assistance; any other form of help?
The very next day we assembled a team, and one week later the “Golden Guard” system was ready for testing. On Monday, March 30, it was available to users.
You can see the user facing site here (Hebrew): apply.israelvolunteer.org.il
And here is the call center app for volunteers (Hebrew): www.israelvolunteer.org.il
This is the story of how we built this web application using Velo by Wix.
Velo is the Wix development platform, offering a Node.js backend, database, enabling integrations and FE coding with an integrated visual builder. The result is a productive development platform, ideal for delivering web applications on a tight schedule.
Want to hear more firsthand from Yoav? Listen to his interview on our Wix Engineering Podcast:
The System
We considered two key questions before building the application: what problem does the system need to solve, and what is the minimum required for it to be functional? Given a tight schedule, focusing on key functionality is a major enabler to deliver such a system on time.
We worked intensively with the ministry office to design the web application main user flows, understanding the roles and responsibilities and determining how the system can support them.
There are three roles in the system:
Volunteer
Coordinator - A volunteer who also manages a group of volunteers
Super Coordinator - An official from a local municipal authority who is responsible for an area
And there are two main areas of the system:
Volunteer Application system - For volunteers to apply to help, and for the government to identify, check, and approve volunteers
Call Center system - For volunteers to conduct the calls
The Approval System Flows
First, a potential volunteer gets a link to the application system. To apply, they have to sign in using the government portal, and then fill out an application form specifying who they are, their availability, etc. Once they submit the form, it goes through a validation check to ensure there are no legal blockers to them becoming a volunteer.
At this point, Super Coordinators check the lists of potential volunteers and approve people based on official criteria. Once approved, a volunteer gets both an SMS and an email to register to the call center system via a one-time use URL and token.
Why both email and SMS? Some email ends up as spam if it’s not sent from a known email address (i.e., an address in the Contacts of the email receiver). Also, some devices, like iPhones, don’t highlight SMS alerts as high priority. In any case, it’s better to have redundancy so no volunteer misses the notification.
The Call Center System Flows
When a volunteer is approved, they get an SMS and email with a link to register for the call center system. After registration, they receive a list of 20 citizens who are potentially in need. The volunteer selects an individual and clicks the Call button. At this time, the call center creates a call between the volunteer’s phone and the citizen’s phone, without exposing the phone numbers to either party. The volunteer explains the purpose of the call, confirms they have reached the correct individual, and asks about their condition, including if they need food, medical assistance, or any other type of assistance.
The information and details of the call are entered in a call summary form, which is then logged in the database for later review or subsequent calls. Based on the information collected from the call center, the relevant data is exported to a food distribution operation, as well as medical assistance providers, if required.
The Coordinator and Super Coordinator roles can also use the system for reports and to manage volunteers.
Technical Challenges
When building this system, we encountered a few challenges:
We had to identify volunteers using the Israeli government portal, in order to validate that the volunteer registering online is actually the same person listed on the government issued physical ID card.
Because we are storing a large government-regulated dataset, we had to use the right database solution.
Volunteers are calling lots of citizens, so we had to protect their privacy by not exposing phone numbers.
Let’s dig into the challenges in order.
Challenge #1: User Identification Through the Government Portal
The challenge is making sure the real-world person who is volunteering matches the persona that is logging in. We want to make sure we are preventing stolen identity and impersonation of potential volunteers and coordinators.
The technical challenge is integrating with the government portal using the SAML 2.0 protocol for identifying the volunteers and coordinators.
Using SAML in Velo is as simple as installing the saml2-js NPM package, and getting all the needed credentials and secrets from the government portal.
The diagram below shows the process.
On the volunteer page, there are four buttons with links for applying to volunteer (apply for a call center volunteer, apply for coordinator, and application for volunteer and coordinators of food delivery). Clicking any of the buttons navigates the volunteer to a Velo router.
A Velo router is server code that reacts to an HTTP request and either selects which page to show or redirects to another page. In this case, the router creates a SAML login request using the saml2-js package, which creates a redirect URL with an encoded and signed param (the SAML request). Then the router returns instructions to the browser to redirect to the government portal using the returned URL. The code for the router is below:
backend/routers.js
1 import { redirect, ok, notFound, WixRouterSitemapEntry } from "wix-router";
2 import wixUsersBackend from 'wix-users-backend';
3 import { getSecret } from 'wix-secrets-backend';
4 import wixData from 'wix-data';
5 import saml2 from 'saml2-js';
6
7 import { loginServiceUrl, metadataUrl, logoutUrl, assertEndpoint, spCert,
8 govEncryptionCertificate, govSiginingCertificate, idp_options }
9 from "..."
10
11 let privateKey;
12 getSecret("saml_gov").then(secret => privateKey = secret);
13
14 export async function login_Router(request) {
15 try {
16 const isLoggedIn = wixUsersBackend.currentUser.loggedIn
17 if (isLoggedIn) {
18 return redirect("/logedinusersArea")
19 }
20 if (!validateRequest(request))
21 return notFound("Missing query params");
22 const params = request.url.substr(request.url.indexOf('?'));
23
24 const sp_options = {
25 entity_id: "...",
26 private_key: privateKey,
27 certificate: spCert,
28 assert_endpoint: assertEndpoint + params,
29 force_authn: true,
30 nameid_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
31 sign_get_request: true,
32 allow_unencrypted_assertion: false
33 }
34
35 let sp = new saml2.ServiceProvider(sp_options);
36 let idp = new saml2.IdentityProvider(idp_options);
37 const url = await new Promise((resolve, reject) => {
38 sp.create_login_request_url(idp, {}, (err, login_url, request_id) => {
39 if (err !== null) { return reject(err) }
40 resolve(login_url)
41 })
42 });
43 return redirect(url, "302"));
44 }
45 catch (e) {...}
46 }
Once a user logs in, the government portal calls a pre-configured API in the volunteer registration system, which is implemented as an HTTP function (POST method). An HTTP function, similar to AWS Lambda or GCP Cloud Functions, is a JavaScript function that is exported as an API for the project.
The body of the POST operation includes a SAML Response, decoded using the saml2-js package, which extracts from the SAML Response the user ID and stores the ID in the database. The function then returns the instruction to the browser to redirect to a form page (the targetPage parameter).
The specific HTTP function is below.
backend/http-functions.js
1 import { response, ok, badRequest } from 'wix-http-functions';
2 import wixData from 'wix-data';
3 import { getSecret } from 'wix-secrets-backend';
4 import saml2 from 'saml2-js';
5 import { loginServiceUrl, metadataUrl, logoutUrl, assertEndpoint, spCert,
6 govEncryptionCertificate, govSiginingCertificate, idp_options }
7 from "..."
8
9
10 let privateKey;
11 getSecret("saml_gov").then(secret => privateKey = secret);
12
13 const dbOptions = {suppressAuth: true};
14 export async function post_assertPost(request) {
15 try {
16 if (!validateRequest(request)) {
17 return badRequest({body: {"error": "Bad request"},
18 headers: {"Content-Type": "application/json"}});
19 }
20 let params = request.url.substr(request.url.indexOf('?'));
21 let reqBody = await request.body.text();
22 reqBody = reqBody.slice(13)
23
24 const sp_options = {
25 entity_id: "...",
26 private_key: privateKey,
27 certificate: spCert,
28 assert_endpoint: assertEndpoint,
29 force_authn: true,
30 nameid_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
31 sign_get_request: true,
32 allow_unencrypted_assertion: false
33 }
34
35 let sp = new saml2.ServiceProvider(sp_options);
36 let idp = new saml2.IdentityProvider(idp_options);
37
38 const options = {
39 allow_unencrypted_assertion: false,
40 request_body: { SAMLResponse: decodeURIComponent(reqBody) }
41 };
42 const collectionItemId = await new Promise((resolve, reject) => {
43 sp.post_assert(idp, options, async function (err, saml_response) {
44 if (err !== null) {
45 console.error(err)
46 return reject(err);
47 }
48 if (saml_response && saml_response.user &&
49 saml_response.user.attributes &&
50 saml_response.user.attributes.IDNum) {
51 const userData = {
52 "gov_auth_collection": saml_response.user.attributes.IDNum };
53 const dataItem = await wixData.insert("gov_auth_collection",
54 userData, dbOptions);
55 resolve(dataItem._id)
56 }
57 return reject("...")
58 });
59 });
60 const targetPage = request.query.targetPage;
61 return response({
62 status: 302,
63 headers: { 'Location': "https://.../" + targetPage + params +
64 "&id=" + collectionItemId } });
65 });
66 }
67 Catch (e) {...}
68 }
After this code, the user ID is in the database and the code in the targetPage can load the user ID based on the synthetic collectionItemId. The onReady function is called for each Velo page before it is shown to the user. In the onReady function, we use a web module to query the database.
pages/application-form.js
1 import wixLocation from 'wix-location';
2 import {getID} from 'backend/tz.jsw';
3
4 $w.onReady(async function () {
5 const itemIdFromMzCollection = wixLocation.query.id;
6 try {
7 if (!itemIdFromMzCollection) {
8 wixLocation.to('/home')
9 } else {
10 let volunteerId = await getID(itemIdFromMzCollection);
11 init(volunteerId);
12 }
13 } catch (err) {...
14 }
15 });
Challenge #2: Scalable and Secure Data Storage
The Velo database is scalable and secure, but due to the government regulations and tight schedule, we didn’t have time to get it approved. So we needed another trusted and approved database solution.
We used GCP Cloud SQL. Fortunately, we have a pre-existing integration with GCP Cloud SQL in the form of an external DB driver, an open source project on GitHub.
It only took a few hours to set up the project and have a compliant database.
Challenge #3: Making Anonymous Calls
To protect the privacy of citizens, it was required that a volunteer could not see the phone number of the person they are calling. So we wanted the volunteer to simply click a button in the web application that would initiate a call between his phone and the citizen’s. This requires an API, such as the Twilio API.
For that, we created a web module—a Node.js module that, for each exported function, automatically creates a proxy module for use in the browser, allowing client code in the browser to call backend code as regular async functions. The functions in the web module are then called when the volunteer clicks the call button to initiate a phone call.
Here’s the code for the function:
backend/make-calls.jsw
1 import { getSecret } from 'wix-secrets-backend';
2 import wixData from 'wix-data';
3 import wixUsersBackend from 'wix-users-backend';
4 const accountSid = '...';
5 const twilioNumber = '...';
6
7 export async function createCall(agentId, seniorCitizenId, callTo) {
8 const agentData = await wixUsersBackend.getUser(agentId);
9 const agentNumber = agentData.phones[0];
10 const seniorCitizenData = await wixData.get('senior_citizens', seniorCitizenId);
11 let seniorCitizenNumber = callTo?
12 seniorCitizenData.items[0].phone:
13 seniorCitizenData.items[0].mobilePhone;
14 try {
15 secret = await getSecret("twillioAuthToken");
16 client = require('twilio')(accountSid, secret);
17
18 return client.calls
19 .create({
20 url: 'https://twimlets.com/forward?PhoneNumber=' + seniorCitizenNumber,
21 to: agentNumber,
22 from: twilioNumber
23 })
24 .then(call => {
25 return call.status
26 })
27 } catch (err) {
28 console.error('createCall err', err);
29 return 'failed to create a call - ' + err.message
30 }
31 }
And on the client page, here’s the code to connect a button with the server function of making the call:
pages/make-calls.js
1 import { createCall } from 'backend/twilioProxy.jsw';
2 ...
3 $w('#callToMobile').onClick(() => {
4 createCall(currentUserId, seniorCtznID, 'mobile');
5 $w('#callFeedback').show();
6 setTimeout(function () { $w('#callFeedback').hide(); }, 10000);
7 })
Summary
Here at Wix we are proud to assist with the Covid-19 situation. We are using our platforms (Wix and Velo), developers, designers, and product people to build multiple systems helping with different challenges.
In the same way we developed the volunteer call center system for the Israeli government, we would be happy to translate it to other languages, adjust it to other government regulations, and assist in implementing it, if requested. Please contact us if we can be of service in your region.
It is fortunate that the tools we are building can make an impact in these challenging times and we are very proud to help.
This post was written by Yoav Abrahami and originally posted on Velo's Blog
You can follow him on Twitter
For more engineering updates and insights:
Visit us on GitHub
Subscribe to our YouTube channel
Comments