How to create a magic email button and handler

Sometimes you want to get people to accomplish a task but don’t want to walk them through a series of steps on the website. Instead, you want to send them an email or send them to an internal page that contains a button that carries out the action automatically. This capability will usually be put in the hands of organisation or system admins.

This page walks through the example of the magic org membership button and how to add your own to the system.

example uses:

  • automatically join people to organisations

  • automatically click interested on an opportunity

  • create a new op from activity

  • associate a new email with your account.

Main Steps

  1. A UI component collects the necessary information and requests that a link be generated

  2. An API call creates the callback token then renders and sends the email to the admin.

  3. The admin forwards the email to their target group, or builds an internal web page containing the button

  4. The recipients then click on the button which loads the callback URL in a browser

  5. The Callback URL validates the request and carries out the action using the payload information.

  6. The person is forwarded to the final destination page with the new status.

Example - Invite Org Members

The Invite Org Members flow allows an organisation administrator (orgAdmin) to generate emails that invite other people to become followers or members of their org. It can be used by School to link their staff to the school org if they don’t have single sign-on, by a corporate to add staff members any by anyone to generate a follow us link.

UI Component <InviteMembers>

This component is shown on the Manage Members tab of the organisation page and is only available to orgAdmins of the organisation.

The component needs to collect any information that will be used by the action handler when it is received. In this case, we collect the member status that we want to set for anyone clicking on the link. We also collect a message that will be added to the email.

The component submit handler then calls the notify/org/:orgid API endpoint.

It's not strictly necessary to create a UI for an action. You can just call the generate Token API directly. (but you will need to be authenticated).

Generate Token API

The api/notify/org. folder provides two modules:

  1. Email/Link Generator - [notifyOrgId].js, this generates the email containing the callback

  2. Action Handler - action.js - this handles the callback.

The module [notifyOrgId].js sits in /pages/api/notify/org. it is a specific api endpoint that can only make org invitation emails. Its not a generic action generator.

When called it receives in the request the orgid we are generating the links for and the user session so we know who is signed in.

The first check is that the person is signed in, the orgid matches an org in the system and that the person is an orgAdmin for that organisation.

 

if (!req.session || !req.session.isAuthenticated) { return res.status(403).end() } try { // verify the org const orgid = req.query.notifyOrgId const org = await Organisation.findById(orgid) // verify I am orgAdmin of org const me = req.session.me const membershipQuery = { person: me._id, organisation: orgid } const membership = await Member.findOne(membershipQuery).exec() if (!(membership && membership.status === MemberStatus.ORGADMIN)) { console.error('you are not an orgadmin of this organisation') return res.status(403).json({ error: 'signed-in person is not an orgadmin of the requested organisation' }) } const orgAdmin = me

 

The next step is to construct the payload that will be encoded into the callback link token. This must contain the following elements

  • landingURL - URL for the callback enpoint API. This is path only. The domain will be set by the server and the query parameters will always be ?token={the token}

  • redirectURL - where to go once the action has been carried out. This should be a page not an api location.

  • data - any parameters required to carry out the action. e.g ids for the org and new member status.

  • action - a verb that will map to the table of action handlers on callback. in this example there is only ‘join’.

  • expiresIn: an interval e.g. 2d(ays). 1w(eek) etc. If no interval is provided the default is 2d. All tokens should have an expiry time set appropriate for how long people will have to click on the link.

const payload = { landingUrl: '/api/notify/org/action', redirectUrl: `/orgs/${orgid}`, data: { orgid, orgAdmin, memberStatus: req.query.memberStatus, memberValidation: req.query.memberValidation }, action: 'join', expiresIn: '2d' }

Next we generate a JWT from the payload

payload.token = makeURLToken(payload)

A JSON Web Token (JWT) is a signed and encrypted BASE64 code representing the payload along with other key properties. It looks like this:

token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJsYW5kaW5nVXJsIjoiL2FwaS9ub3RpZnkvb3JnL2FjdGlvbiIsInJlZGlyZWN0VXJsIjoiL29yZ3MvNWQ5ZmU2ODA0ZWIxNzkyMThjOGQxZDMyIiwiZGF0YSI6eyJvcmdpZCI6IjVkOWZlNjgwNGViMTc5MjE4YzhkMWQzMiIsIm9yZ0FkbWluIjp7InRlYWNoZXIiOnsicmVnaXN0cmF0aW9uIjp7InRybiI6IjI0ODU2NCIsImZpcnN0bmFtZSI6IkFubmEgTG91aXNlIiwibGFzdG5hbWUiOiJTbWl0aCIsImNhdGVnb3J5IjoiU3ViamVjdCB0byBDb25maXJtYXRpb24iLCJleHBpcnkiOiIwOSBBcHIgMjAyMiJ9fSwibmlja25hbWUiOiJhdm93a2luZCIsImFib3V0IjoiPGgyPkl0cyBBbmRyZXcncyBHaXRodWIgYWNjb3VudDwvaDI-PHA-RWRpdGVkIDY8L3A-IiwibG9jYXRpb24iOiJBdWNrbGFuZCIsImltZ1VybCI6Imh0dHBzOi8vYXZhdGFyczIuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTU5NjQzNz92PTQiLCJwcm9ub3VuIjp7InN1YmplY3QiOiJoZSIsIm9iamVjdCI6ImhpbSIsInBvc3Nlc3NpdmUiOiJoaXMifSwibGFuZ3VhZ2UiOiJlbiIsInJvbGUiOlsib3Bwb3J0dW5pdHlQcm92aWRlciIsInZvbHVudGVlciIsImFjdGl2aXR5UHJvdmlkZXIiLCJ0ZXN0ZXIiLCJhZG1pbiJdLCJzdGF0dXMiOiJhY3RpdmUiLCJ0YWdzIjpbXSwiX2lkIjoiNWQ5ZmU1NTY0ZWIxNzkyMThjOGQxZDJjIiwiYXZhdGFyIjoiIiwiZGF0ZUFkZGVkIjoiMjAxOS0xMC0xMVQwMjoxMzo0Mi4wODRaIiwibmFtZSI6IkFuZHJldyBXYXRraW5zIiwiZW1haWwiOiJhbmRyZXdAZ3JvYXQubnoiLCJfX3YiOjEzLCJnZW5kZXIiOiIiLCJwaG9uZSI6IjAyNzcwMzEwMDciLCJ0d2l0dGVyIjoiYXZvd2tpbmQifSwibWVtYmVyU3RhdHVzIjoiZm9sbG93ZXIiLCJtZW1iZXJWYWxpZGF0aW9uIjoiT3Bwb3J0dW5pdHkgcHJvdmlkZXIgMSBJbnZpdGF0aW9uIn0sImFjdGlvbiI6ImpvaW4iLCJleHBpcmVzSW4iOiIyZCIsImlhdCI6MTU3NDg4NDExMCwiZXhwIjoxNTc1MDU2OTEwfQ.oUVbN6Cgl-xYLTHpkI72DgGIOhwRb3toMGvtwRgbGJTjqzRzC4WH_5FgDSAqw5GIfFidlVIKhjkDFsB0ypwDLlfGovt235lPpJhkIFh7VXhYUb5JEm2i_jbIhBY14CzXiC7xUJrdRmRqmWMT51-pvX5QwuMAj51TSmd8uUMlQkQgsAwYjPuBe5CcjyeJBk_BzUV3-102ifVhiZ62lZneYuMuuByLB4ZahPd9zXsxonxBTOBBC4f_9fxS-bx-gVAUkVfSuoFh9O0vejKxhUYYGwxFFQTYYoqH2pxp6XG8I6Egwei-m0ypQlgKGUPKpgFnQjrH5F3yusUecLg8hq8-Kw

JWTs use public-key encryption. That means they are encrypted using the voluntarily private key and can be read and verified using our public key. This makes it almost impossible for someone to fake an action request.

At this point, the API could return the token to the UI for incorporating in a cut/paste HTML block that could be posted on social media. In our example, we now generate an email using the standard built-in email templates and then send this email to the org admin for circulation

Finally the API call should return something to the caller so they know it all worked.

 

Here is the email generated

 

Action Handler

The confirm membership button has an href of http://localhost:3122/api/notify/org/action?token=eyJhbGciOetc

When clicked this will hit the action.js module in api/notify/org. Again we don’t put all our actions in one place instead creating new endpoints and handlers for each group of actions that rely on similar data.

The action handler is quite simple:

All the work of checking and decoding the token is done by `handleToken` This takes a parameter that is a function table, that is an object where each key is one of the possible action verbs found in the token and the value of each key is a function that will process the data in the token and carry out the action. handle token also looks after the final redirect.

In this example, we have a single action ‘join’ which calls addMember with the data provided by the properties passed in from the decoded data, along with current values such as the signed in person.

AddMember is a function we already have that creates a new membership record using a person and organisation. Its the function lying behind the membership API. Action handers should always seek to call functions provided by other parts of the system rather than coding directly and those calls should already have their own test functions.

 

HandleToken

Handle token is a generic handler for API calls that have the query parameter token=

It will check a token is present, if the current user is not authenticated it will ask them to sign in - by routing their browser to the sign-thru path with the original api call as a return address. If sign in succeeds we should end up back in the same function with a valid me.

Next handleURLToken is called with the token and actionTable. if that is successful the page is redirected to the target given in the payload.

handleURLToken (/lib/sec)

This verifies the JWT was created by voluntarily and has not expired. It then looks for the action given in the payload data in the actionTable and if present calls it with the data also found in the payload data.

The net result is a sort of encrypted remote procedure call

Discussion

This fairly elaborate process gives us some useful features:

  • We can’t accidentally call random functions on the server each action verb has to match a specific handler in a specific API endpoint.

  • Tokens are encrypted, hard to forge and will expire after a set time. You can’t take a working token, modify some values and use it again.

  • no usernames or passwords are involved.

  • The design puts token generation, generating a payload, verification and handling actions into separate modules so to implement a new magic token you only need to write a construction function and provide an action table.

 

Another Example

Using action tokens to originate a new school organisation

https://voluntarily.atlassian.net/browse/VP-826

For each field trial school we need to send a link to the named school admin which will:

  1. Take them to the sign up page

  2. Allow them to sign in and view dashboard

  3. Activate that school on the platform

  4. Pre populate the school’s profile with information from the database

  5. make the person orgAdmin for the school

This requires a page available only to system Administrators.

On the page we will require a UI Form to collect:

  • name of the school (matched to the database )

  • person to send email to - name and email address

  • invitation message text

 

The email generator function would also be in /notify/org folder and we can add another action to action.js to create the organisation (AddOrg instead of AddMember).

I’d suggest placing all the initialisation data in the payload rather than accessing the database in the action handler. This would allow us to use other means to mint similar preconfigured organisations.

In this case the email might be sent directly to the target email address (ccd to the administrator.) although the default pattern of sending to the admin might be safer as it allows for checking.

We will need another email template to hold the instructions to the recipient and what to expect when they click on the link. e.g. Activate your school.

The UI could use the database to list the known schools and allow the admin to pick one to send the invitation to (or many).