@mindful-web/marko-web-postup 1.61.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/api-client.js +152 -0
- package/env.js +7 -0
- package/hooks/index.js +51 -0
- package/hooks/on-authentication-success.js +16 -0
- package/hooks/on-login-link-sent.js +22 -0
- package/hooks/on-user-profile-update.js +15 -0
- package/index.js +72 -0
- package/middleware/set-identity-cookie.js +27 -0
- package/package.json +36 -0
- package/service.js +339 -0
- package/utils.js +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Parameter1 LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/api-client.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const debug = require('debug')('postup:api');
|
|
2
|
+
const fetch = require('node-fetch');
|
|
3
|
+
|
|
4
|
+
class PostUpApiClient {
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} credentials
|
|
7
|
+
*/
|
|
8
|
+
constructor(credentials) {
|
|
9
|
+
if (!credentials) throw new Error('Unable to use the PostUp API without credentials.');
|
|
10
|
+
this.headers = {
|
|
11
|
+
'content-type': 'application/json',
|
|
12
|
+
authorization: `Basic ${credentials}`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} endpoint
|
|
18
|
+
* @param {object} opts fetch options
|
|
19
|
+
* @returns {Promise<object>}
|
|
20
|
+
*/
|
|
21
|
+
async request(endpoint, opts = {}) {
|
|
22
|
+
const method = opts.method || 'post';
|
|
23
|
+
const { headers } = this;
|
|
24
|
+
const url = `https://api.postup.com/api/${endpoint}`;
|
|
25
|
+
const r = await fetch(url, { method, headers, ...opts || {} });
|
|
26
|
+
const response = await r.json();
|
|
27
|
+
const dbg = {
|
|
28
|
+
req: { url, ...{ method, headers, ...opts || {} } },
|
|
29
|
+
res: { headers: r.headers.raw(), body: response },
|
|
30
|
+
};
|
|
31
|
+
if (!r.ok) {
|
|
32
|
+
debug(`${method.toUpperCase()} ${url} ${r.status} ERR`, dbg);
|
|
33
|
+
if (response.message) throw new Error(response.message);
|
|
34
|
+
throw new Error(`API request was unsuccessful: ${r.status} ${r.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
debug(`${method.toUpperCase()} ${url} ${r.status} OK`, dbg);
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a recipient and sets idx external id
|
|
42
|
+
* @see https://apidocs.postup.com/docs/subscription-management
|
|
43
|
+
*
|
|
44
|
+
* @param {string} email
|
|
45
|
+
* @param {string} externalId
|
|
46
|
+
* @param {?string} [ip]
|
|
47
|
+
* @returns {Promise}
|
|
48
|
+
*/
|
|
49
|
+
async createRecipient(email, externalId, ip) {
|
|
50
|
+
return this.request('recipient', {
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
address: email,
|
|
53
|
+
externalId,
|
|
54
|
+
channel: 'E',
|
|
55
|
+
sourceDescription: 'IdentityX',
|
|
56
|
+
signupIP: ip,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retrieves subscription info for the supplied recipient
|
|
63
|
+
* @see https://apidocs.postup.com/docs/return-a-single-recipients-list-subscription
|
|
64
|
+
*
|
|
65
|
+
* @param {string} recipientId
|
|
66
|
+
* @returns {Promise<ListSubscription[]>}
|
|
67
|
+
*/
|
|
68
|
+
async listSubscriptionsForRecipient(recipientId) {
|
|
69
|
+
const params = new URLSearchParams({ recipid: recipientId });
|
|
70
|
+
return this.request(`listsubscription?${params}`, { method: 'get' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Retrieves a recipient by email address
|
|
75
|
+
* @see https://apidocs.postup.com/docs/retrieve-recipient-data
|
|
76
|
+
*
|
|
77
|
+
* @param {string} email
|
|
78
|
+
* @returns {Promise<Recipient>}
|
|
79
|
+
*/
|
|
80
|
+
async returnRecipientDataByEmailAddress(email) {
|
|
81
|
+
const params = new URLSearchParams({ address: email });
|
|
82
|
+
return this.request(`recipient?${params}`, { method: 'get' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Updates recipient data (fields, demographics, etc)
|
|
87
|
+
* @see https://apidocs.postup.com/docs/update-existing-recipient-data
|
|
88
|
+
*
|
|
89
|
+
* @param {string} recipientId
|
|
90
|
+
* @param {string} email
|
|
91
|
+
* @param {string[]} demographics
|
|
92
|
+
* @returns {Promise}
|
|
93
|
+
*/
|
|
94
|
+
updateRecipient(recipientId, email, demographics) {
|
|
95
|
+
return this.request(`recipient/${recipientId}`, {
|
|
96
|
+
method: 'put',
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
address: email,
|
|
99
|
+
demographics,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Updates recipient externalId
|
|
106
|
+
* @see https://apidocs.postup.com/docs/update-existing-recipient-data
|
|
107
|
+
*
|
|
108
|
+
* @param {string} recipientId The PostUp recipient id
|
|
109
|
+
* @param {string} externalId The IdentityX user id
|
|
110
|
+
* @returns {Promise}
|
|
111
|
+
*/
|
|
112
|
+
updateRecipientExternalId(recipientId, externalId) {
|
|
113
|
+
return this.request(`recipient/${recipientId}`, {
|
|
114
|
+
method: 'put',
|
|
115
|
+
body: JSON.stringify({ externalId }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Updates a list subscription for a recipient
|
|
121
|
+
* @see https://apidocs.postup.com/docs/subscribe-recipient-to-a-list
|
|
122
|
+
* @see https://apidocs.postup.com/docs/unsubscribe-recipient-from-a-list
|
|
123
|
+
*
|
|
124
|
+
* @param {string} recipientId
|
|
125
|
+
* @param {string} listId
|
|
126
|
+
* @param {boolean} value
|
|
127
|
+
* @returns {Promise}
|
|
128
|
+
*/
|
|
129
|
+
async updateSubscription(recipientId, listId, value) {
|
|
130
|
+
return this.request('listsubscription', {
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
listId,
|
|
133
|
+
recipientId,
|
|
134
|
+
status: value ? 'NORMAL' : 'UNSUB',
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = PostUpApiClient;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef ListSubscription
|
|
144
|
+
* @prop {Number} recipientId
|
|
145
|
+
* @prop {Number} listId
|
|
146
|
+
* @prop {ListSubscriptionStatus} status
|
|
147
|
+
* @prop {ListSubscriptionStatus} listStatus
|
|
148
|
+
* @prop {ListSubscriptionStatus} globalStatus
|
|
149
|
+
* @prop {Boolean} confirmed
|
|
150
|
+
*
|
|
151
|
+
* @typedef {("NORMAL"|"UNSUB")} ListSubscriptionStatus
|
|
152
|
+
*/
|
package/env.js
ADDED
package/hooks/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const onAuthenticationSuccess = require('./on-authentication-success');
|
|
2
|
+
const onLoginLinkSent = require('./on-login-link-sent');
|
|
3
|
+
const onUserProfileUpdate = require('./on-user-profile-update');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param {import('../index').IdentityXConfiguration} idxConfig
|
|
8
|
+
* @param {PostUpService} postUp
|
|
9
|
+
*/
|
|
10
|
+
module.exports = (idxConfig, postUp) => {
|
|
11
|
+
if (idxConfig.postupInstalled) return;
|
|
12
|
+
idxConfig.addHook({
|
|
13
|
+
name: 'onUserProfileUpdate',
|
|
14
|
+
shouldAwait: false,
|
|
15
|
+
fn: (args) => onUserProfileUpdate({ postUp, ...args }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
idxConfig.addHook({
|
|
19
|
+
name: 'onLoginLinkSent',
|
|
20
|
+
shouldAwait: false,
|
|
21
|
+
fn: (args) => onLoginLinkSent({ postUp, ...args }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
idxConfig.addHook({
|
|
25
|
+
name: 'onAuthenticationSuccess',
|
|
26
|
+
shouldAwait: true,
|
|
27
|
+
fn: (args) => onAuthenticationSuccess({ postUp, ...args }),
|
|
28
|
+
});
|
|
29
|
+
// eslint-disable-next-line no-param-reassign
|
|
30
|
+
idxConfig.postupInstalled = true;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {import('@mindful-web/marko-web-identity-x/service')} IdentityXService
|
|
35
|
+
* @typedef {import('../service')} PostUpService
|
|
36
|
+
* @typedef {import('../index').PostUpConfig} PostUpConfig
|
|
37
|
+
* @typedef {import('../index').PostUpRequest} PostUpRequest
|
|
38
|
+
*
|
|
39
|
+
* @typedef IdentityXUser
|
|
40
|
+
* @prop {String} id
|
|
41
|
+
* @prop {String} email
|
|
42
|
+
* @prop {import('@mindful-web/marko-web-identity-x/service').ExternalId[]} externalIds
|
|
43
|
+
*
|
|
44
|
+
* @typedef IdentityXHookArgs
|
|
45
|
+
* @prop {Object} additionalEventData
|
|
46
|
+
* @prop {Object} authToken
|
|
47
|
+
* @prop {PostUpService} postUp
|
|
48
|
+
* @prop {IdentityXService} service
|
|
49
|
+
* @prop {IdentityXUser} user
|
|
50
|
+
* @prop {String} loginSource
|
|
51
|
+
* */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import("./index").IdentityXHookArgs} args
|
|
3
|
+
*/
|
|
4
|
+
module.exports = async ({
|
|
5
|
+
// loginSource,
|
|
6
|
+
postUp,
|
|
7
|
+
service,
|
|
8
|
+
user,
|
|
9
|
+
}) => {
|
|
10
|
+
await postUp.ensureUser(user, service);
|
|
11
|
+
|
|
12
|
+
// Auto-subscribe the user if this is the first successful authentication
|
|
13
|
+
// if (loginSource === 'newsletterSignup' && user.verifiedCount === 1) {
|
|
14
|
+
// await postUp.defaultListOptIns(service, user);
|
|
15
|
+
// }
|
|
16
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import("./index").IdentityXHookArgs} args
|
|
3
|
+
*/
|
|
4
|
+
module.exports = async ({
|
|
5
|
+
// additionalEventData,
|
|
6
|
+
postUp,
|
|
7
|
+
service,
|
|
8
|
+
user,
|
|
9
|
+
}) => {
|
|
10
|
+
// Attempt to import user data to IdentityX
|
|
11
|
+
if (user && !user.verified) {
|
|
12
|
+
await postUp.updateIdentityXData(user, service);
|
|
13
|
+
await postUp.defaultListOptIns(service, user);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// If the user has already verified and is coming from nl signup, sign them up to the default list
|
|
17
|
+
// if (user && user.verified && additionalEventData.actionSource === 'newsletterSignup') {
|
|
18
|
+
// await postUp.defaultListOptIns(service, user);
|
|
19
|
+
// }
|
|
20
|
+
|
|
21
|
+
return user;
|
|
22
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import("./index").IdentityXHookArgs} args
|
|
3
|
+
*/
|
|
4
|
+
module.exports = async ({
|
|
5
|
+
// additionalEventData,
|
|
6
|
+
postUp,
|
|
7
|
+
user,
|
|
8
|
+
service,
|
|
9
|
+
}) => {
|
|
10
|
+
await postUp.ensureUser(user, service);
|
|
11
|
+
|
|
12
|
+
await postUp.updatePostUpData(user, service);
|
|
13
|
+
|
|
14
|
+
return user;
|
|
15
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { get } = require('@mindful-web/object-path');
|
|
2
|
+
|
|
3
|
+
/* eslint-disable max-len */
|
|
4
|
+
const Joi = require('@parameter1/joi');
|
|
5
|
+
const { validate } = require('@parameter1/joi/utils');
|
|
6
|
+
const PostUp = require('./service');
|
|
7
|
+
const hooks = require('./hooks');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import("express").Application} app
|
|
11
|
+
* @param {IdentityXConfiguration} idxConfig
|
|
12
|
+
* @param {PostUpConfig} params
|
|
13
|
+
*
|
|
14
|
+
* @returns {PostUpConfig}
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
module.exports = (app, idxConfig, params = {}) => {
|
|
18
|
+
const args = validate(Joi.object({
|
|
19
|
+
defaultListQuestionIds: Joi.array().items(Joi.string().pattern(/[a-f0-9]{24}/i)).required(),
|
|
20
|
+
fieldMap: Joi.object().default({
|
|
21
|
+
// PostUp standard fields
|
|
22
|
+
FirstName: 'givenName',
|
|
23
|
+
LastName: 'familyName',
|
|
24
|
+
_Address1: 'street',
|
|
25
|
+
_Address2: 'addressExtra',
|
|
26
|
+
_City: 'city',
|
|
27
|
+
_Country: 'countryCode',
|
|
28
|
+
_CountryCode: 'countryCode',
|
|
29
|
+
_HomePhone: 'phoneNumber',
|
|
30
|
+
_PostalCode: 'postalCode',
|
|
31
|
+
_State: 'regionCode',
|
|
32
|
+
// PostUp custom fields
|
|
33
|
+
companyname: 'organization',
|
|
34
|
+
title: 'organizationTitle',
|
|
35
|
+
}).description('The PostUp fields that should be mapped to IdentityX fields.'),
|
|
36
|
+
}), params);
|
|
37
|
+
|
|
38
|
+
app.use((req, res, next) => {
|
|
39
|
+
// Install service
|
|
40
|
+
const service = new PostUp({ ...args, cookies: req.cookies || {} });
|
|
41
|
+
req.PostUp = service;
|
|
42
|
+
res.locals.PostUp = service;
|
|
43
|
+
|
|
44
|
+
// Install custom handler for content metering
|
|
45
|
+
// eslint-disable-next-line no-param-reassign
|
|
46
|
+
app.locals.contentBypassGatingHandler = ({ content }) => {
|
|
47
|
+
// Skip metering if we already have an IdX identity
|
|
48
|
+
const idt = req.identityX.getIdentity(res);
|
|
49
|
+
if (idt) return true;
|
|
50
|
+
|
|
51
|
+
// Skip metering if we have a constant contact email param
|
|
52
|
+
if (req.query.pu_ext_id) return true;
|
|
53
|
+
|
|
54
|
+
// Fall back to default behavior
|
|
55
|
+
return get(content, 'userRegistration.bypassGating', false);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Install hooks
|
|
59
|
+
hooks(idxConfig, service);
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {import('@mindful-web/marko-web-identity-x/config')} IdentityXConfiguration
|
|
66
|
+
* @typedef PostUpConfig
|
|
67
|
+
* @prop {Array} defaultListQuestionIds Array of object ids representing the default subscription questions
|
|
68
|
+
* @prop {Object} fieldMap An object representing how fields should map from PostUp to IdentityX.
|
|
69
|
+
*
|
|
70
|
+
* @typedef PostUpRequest
|
|
71
|
+
* @prop {PostUp} postUp
|
|
72
|
+
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { asyncRoute } = require('@mindful-web/utils');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef OIDXRequest
|
|
5
|
+
* @prop {import('@mindful-web/marko-web-identity-x/service')} identityX
|
|
6
|
+
*
|
|
7
|
+
* @typedef MiddlewareConstructor
|
|
8
|
+
*/
|
|
9
|
+
module.exports = asyncRoute(async (req, res, next) => {
|
|
10
|
+
/** @type {OIDXRequest} */
|
|
11
|
+
const { identityX: idx } = req;
|
|
12
|
+
const cookie = idx.getIdentity(res);
|
|
13
|
+
|
|
14
|
+
// Don't overwrite an existing cookie
|
|
15
|
+
if (cookie) return next();
|
|
16
|
+
|
|
17
|
+
// get oly enc id. if we don't have one, bail
|
|
18
|
+
const postUpId = req.query.pu_ext_id;
|
|
19
|
+
if (!postUpId) return next();
|
|
20
|
+
|
|
21
|
+
// Look up idx user by pu_ext_id or our idx mongoID
|
|
22
|
+
const identity = await idx.findUserById(postUpId);
|
|
23
|
+
if (identity) {
|
|
24
|
+
idx.setIdentityCookie(identity.id);
|
|
25
|
+
}
|
|
26
|
+
return next();
|
|
27
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindful-web/marko-web-postup",
|
|
3
|
+
"version": "1.61.3",
|
|
4
|
+
"author": "Josh Worden <josh@parameter1.com>",
|
|
5
|
+
"repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-postup",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"lint": "eslint --ext .js --ext .vue --max-warnings 5 --config ../../.eslintrc.js --ignore-path ../../.eslintignore ./",
|
|
9
|
+
"lint:fix": "yarn lint --fix",
|
|
10
|
+
"test": "yarn lint"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@mindful-web/env": "^1.0.0",
|
|
14
|
+
"@mindful-web/marko-web-identity-x": "^1.60.0",
|
|
15
|
+
"@mindful-web/object-path": "^1.57.0",
|
|
16
|
+
"@mindful-web/utils": "^1.57.0",
|
|
17
|
+
"@parameter1/joi": "^1.2.10",
|
|
18
|
+
"debug": "^4.1.1",
|
|
19
|
+
"express": "^4.17.1",
|
|
20
|
+
"graphql-tag": "^2.12.6",
|
|
21
|
+
"newrelic": "^9.10.2",
|
|
22
|
+
"node-fetch": "^2.6.1"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=14.15"
|
|
26
|
+
},
|
|
27
|
+
"os": [
|
|
28
|
+
"darwin",
|
|
29
|
+
"linux",
|
|
30
|
+
"win32"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "713b78883a51d0bcce2e82d84cf361353fa4823a"
|
|
36
|
+
}
|
package/service.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
const debug = require('debug')('postup:service');
|
|
2
|
+
const gql = require('graphql-tag');
|
|
3
|
+
const { get, getAsArray } = require('@mindful-web/object-path');
|
|
4
|
+
const { POSTUP_API_CREDENTIALS } = require('./env');
|
|
5
|
+
const { filterByExternalId, filterFieldsByExternalId } = require('./utils');
|
|
6
|
+
const PostUpApiClient = require('./api-client');
|
|
7
|
+
|
|
8
|
+
const SET_DATA = gql`
|
|
9
|
+
mutation SetPostUpData($input: SetAppUserUnverifiedDataMutationInput!) {
|
|
10
|
+
setAppUserUnverifiedData(input: $input) { id }
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const SET_SELECT_FIELD_ANSWERS = gql`
|
|
15
|
+
mutation SetPostUpSelectFieldAnswers($input: UpdateAppUserCustomSelectAnswersMutationInput!) {
|
|
16
|
+
updateAppUserCustomSelectAnswers(input: $input) { id }
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const SET_BOOLEAN_FIELD_ANSWERS = gql`
|
|
21
|
+
mutation SetPostUpBooleanFieldAnswers($input: UpdateAppUserCustomBooleanAnswersMutationInput!) {
|
|
22
|
+
updateAppUserCustomBooleanAnswers(input: $input) { id }
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
class PostUp {
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} defaultListQuestionIds The array of default lists for auto-signups
|
|
29
|
+
* @param {object} fieldMap A mapping of postup>identityx field keys
|
|
30
|
+
*/
|
|
31
|
+
constructor({
|
|
32
|
+
defaultListQuestionIds,
|
|
33
|
+
fieldMap = {},
|
|
34
|
+
tenant = 'transpire',
|
|
35
|
+
} = {}) {
|
|
36
|
+
this.defaultListQuestionIds = defaultListQuestionIds;
|
|
37
|
+
this.fieldMap = Object.entries(fieldMap).reduce((map, [key, value]) => {
|
|
38
|
+
map.set(key, value);
|
|
39
|
+
return map;
|
|
40
|
+
}, new Map());
|
|
41
|
+
this.tenant = tenant;
|
|
42
|
+
this.client = new PostUpApiClient(POSTUP_API_CREDENTIALS);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Writes PostUp-supplied user data to IdentityX
|
|
47
|
+
*
|
|
48
|
+
* @param {import('./hooks').IdentityXUser} user
|
|
49
|
+
* @param {IdentityXService} idx The IdentityX user service
|
|
50
|
+
* @returns {Promise<any>}
|
|
51
|
+
*/
|
|
52
|
+
async updateIdentityXData(user, idx) {
|
|
53
|
+
// Don't overwrite verified user data
|
|
54
|
+
if (user.verified) return;
|
|
55
|
+
|
|
56
|
+
const [userData = {}] = await this.getUserByEmail(user.email);
|
|
57
|
+
if (!userData || !userData.recipientId) {
|
|
58
|
+
await this.ensureUser(user, idx);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { demographics = [], recipientId } = userData;
|
|
63
|
+
|
|
64
|
+
debug('updateIdentityXData.externalId', recipientId);
|
|
65
|
+
await this.setExternalIds(idx, user.id, recipientId);
|
|
66
|
+
|
|
67
|
+
const demoData = demographics.reduce((o, kv) => {
|
|
68
|
+
const [k, v] = kv.split('=');
|
|
69
|
+
return { ...o, [k]: v };
|
|
70
|
+
}, {});
|
|
71
|
+
|
|
72
|
+
// Update root fields
|
|
73
|
+
const payload = Object.entries(demoData).reduce((o, [k, v]) => ({
|
|
74
|
+
...o,
|
|
75
|
+
...(this.fieldMap.has(k) && v && {
|
|
76
|
+
[this.fieldMap.get(k)]: v,
|
|
77
|
+
}),
|
|
78
|
+
}), { email: user.email });
|
|
79
|
+
|
|
80
|
+
debug('updateIdentityXData.data', payload);
|
|
81
|
+
await idx.client.mutate({
|
|
82
|
+
context: { apiToken: idx.getOrgUserApiToken() },
|
|
83
|
+
mutation: SET_DATA,
|
|
84
|
+
variables: { input: payload },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Update custom selects
|
|
88
|
+
const questions = filterFieldsByExternalId(getAsArray(user, 'customSelectFieldAnswers'), 'demographic');
|
|
89
|
+
const customSelectFieldAnswers = questions.reduce((arr, ans) => {
|
|
90
|
+
// Don't overwrite previously set answers
|
|
91
|
+
if (!ans.hasAnswered) {
|
|
92
|
+
const key = `${get(ans, 'field.externalId.identifier.value')}`.toLocaleLowerCase();
|
|
93
|
+
const { id: fieldId } = ans.field;
|
|
94
|
+
if (demoData[key]) {
|
|
95
|
+
// find the answer, set the value
|
|
96
|
+
const found = ans.field.options.find((opt) => opt.externalIdentifier === demoData[key]);
|
|
97
|
+
if (found) arr.push({ fieldId, optionIds: [found.id] });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return arr;
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
debug('updateIdentityXData.selects', customSelectFieldAnswers);
|
|
104
|
+
if (customSelectFieldAnswers.length) {
|
|
105
|
+
await idx.client.mutate({
|
|
106
|
+
context: { apiToken: idx.getOrgUserApiToken() },
|
|
107
|
+
mutation: SET_SELECT_FIELD_ANSWERS,
|
|
108
|
+
variables: {
|
|
109
|
+
input: {
|
|
110
|
+
id: user.id,
|
|
111
|
+
answers: customSelectFieldAnswers,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const subs = await this.client.listSubscriptionsForRecipient(recipientId);
|
|
118
|
+
|
|
119
|
+
const subscriptions = filterFieldsByExternalId(getAsArray(user, 'customBooleanFieldAnswers'), 'list');
|
|
120
|
+
const customBooleanFieldAnswers = subscriptions.reduce((arr, ans) => {
|
|
121
|
+
const listId = get(ans, 'field.externalId.identifier.value');
|
|
122
|
+
const match = subs.find((sub) => sub.listId === parseInt(listId, 10));
|
|
123
|
+
if (match) arr.push({ fieldId: ans.field.id, value: match.status === 'NORMAL' });
|
|
124
|
+
return arr;
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
debug('updateIdentityXData.bools', customBooleanFieldAnswers);
|
|
128
|
+
if (customBooleanFieldAnswers.length) {
|
|
129
|
+
await idx.client.mutate({
|
|
130
|
+
context: { apiToken: idx.getOrgUserApiToken() },
|
|
131
|
+
mutation: SET_BOOLEAN_FIELD_ANSWERS,
|
|
132
|
+
variables: {
|
|
133
|
+
input: {
|
|
134
|
+
id: user.id,
|
|
135
|
+
answers: customBooleanFieldAnswers,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Writes user-supplied IdentityX data to PostUp
|
|
144
|
+
*
|
|
145
|
+
* @param {import('./hooks').IdentityXUser} user
|
|
146
|
+
* @returns {Promise<any>}
|
|
147
|
+
*/
|
|
148
|
+
async updatePostUpData(user) {
|
|
149
|
+
const demographics = [...this.fieldMap.entries()].reduce((o, [p, i]) => ({
|
|
150
|
+
...o,
|
|
151
|
+
[p]: get(user, i),
|
|
152
|
+
}), {});
|
|
153
|
+
|
|
154
|
+
// External ID tagged questions
|
|
155
|
+
const questions = filterFieldsByExternalId(getAsArray(user, 'customSelectFieldAnswers'), 'demographic');
|
|
156
|
+
questions.forEach((ans) => {
|
|
157
|
+
if (ans.hasAnswered) {
|
|
158
|
+
const key = get(ans, 'field.externalId.identifier.value');
|
|
159
|
+
const answers = getAsArray(ans, 'answers').map((a) => a.writeInValue || a.externalIdentifier);
|
|
160
|
+
demographics[key] = ans.field.multiple ? answers.join(',') : answers.pop();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Update Postup with the user data and demographics
|
|
165
|
+
await this.updateRecipient(user, demographics);
|
|
166
|
+
|
|
167
|
+
const answers = filterFieldsByExternalId(getAsArray(user, 'customBooleanFieldAnswers'), 'list');
|
|
168
|
+
const subscriptions = answers.reduce((map, answer) => {
|
|
169
|
+
const id = get(answer, 'field.externalId.identifier.value');
|
|
170
|
+
map.set(`${id}`, answer.value);
|
|
171
|
+
return map;
|
|
172
|
+
}, new Map());
|
|
173
|
+
|
|
174
|
+
// Update Postup with user subscriptions
|
|
175
|
+
await this.updateSubscriptions(user, subscriptions);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Ensure the user has been sent to postup.
|
|
180
|
+
*
|
|
181
|
+
* @param {import('./hooks').IdentityXUser} user
|
|
182
|
+
* @param {IdentityXService} idx The IdentityX user service
|
|
183
|
+
* @returns {Promise<any>}
|
|
184
|
+
*/
|
|
185
|
+
async ensureUser(user, idx) {
|
|
186
|
+
// 1. Check IdentityX user externalIds for postup recipient id, bail if found
|
|
187
|
+
const found = filterByExternalId(user.externalIds, 'recipient');
|
|
188
|
+
if (found && found.length) return user;
|
|
189
|
+
|
|
190
|
+
// 2. Attempt to look up by email address. If found, push externalId to IdX/PostUp users
|
|
191
|
+
const data = await this.getUserByEmail(user.email);
|
|
192
|
+
const recipientId = get(data, '0.recipientId');
|
|
193
|
+
if (recipientId) return this.setExternalIds(idx, user.id, recipientId);
|
|
194
|
+
|
|
195
|
+
// 3. Create user. Push externalId to IdX user
|
|
196
|
+
return this.createRecipient(user.email, user.id, idx);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Sets the IdentityX externalId
|
|
201
|
+
*
|
|
202
|
+
* @param {IdentityXService} idx The IdentityX user service
|
|
203
|
+
* @param {string} userId The IdentityX user id
|
|
204
|
+
* @param {string} externalId The PostUp recipient id
|
|
205
|
+
* @return {Promise}
|
|
206
|
+
*/
|
|
207
|
+
async setExternalIds(idx, userId, externalId) {
|
|
208
|
+
return Promise.all([
|
|
209
|
+
idx.addExternalUserId({
|
|
210
|
+
userId,
|
|
211
|
+
identifier: { value: `${externalId}` },
|
|
212
|
+
namespace: { provider: 'postup', tenant: this.tenant, type: 'recipient' },
|
|
213
|
+
}),
|
|
214
|
+
this.client.updateRecipientExternalId(externalId, userId),
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Subscribes a user to the default list.
|
|
220
|
+
*
|
|
221
|
+
* @param {IdentityXService} idx The IdentityX user service
|
|
222
|
+
* @param {import('./hooks').IdentityXUser} user The IdentityX user object
|
|
223
|
+
*/
|
|
224
|
+
async defaultListOptIns(idx, user) {
|
|
225
|
+
const subscriptions = filterFieldsByExternalId(getAsArray(user, 'customBooleanFieldAnswers'), 'list');
|
|
226
|
+
|
|
227
|
+
const recipientId = await this.getRecipientId(user);
|
|
228
|
+
|
|
229
|
+
const answers = new Map();
|
|
230
|
+
const postUpSupscritions = subscriptions.reduce((map, a) => {
|
|
231
|
+
const id = get(a, 'field.id');
|
|
232
|
+
const externalId = get(a, 'field.externalId.identifier.value');
|
|
233
|
+
if (this.defaultListQuestionIds.includes(id)) {
|
|
234
|
+
map.set(`${externalId}`, true);
|
|
235
|
+
answers.set(`${id}`, { fieldId: id, value: true });
|
|
236
|
+
}
|
|
237
|
+
return map;
|
|
238
|
+
}, new Map());
|
|
239
|
+
|
|
240
|
+
debug('defaultListOptIn', {
|
|
241
|
+
user: user.id,
|
|
242
|
+
recipientId,
|
|
243
|
+
postUpSupscritions,
|
|
244
|
+
answers: [...answers.values()],
|
|
245
|
+
});
|
|
246
|
+
await Promise.all([
|
|
247
|
+
// Update Postup with user subscriptions by default
|
|
248
|
+
await this.updateSubscriptions(user, postUpSupscritions),
|
|
249
|
+
// Set question value in IdentityX
|
|
250
|
+
idx.client.mutate({
|
|
251
|
+
context: { apiToken: idx.getOrgUserApiToken() },
|
|
252
|
+
mutation: SET_BOOLEAN_FIELD_ANSWERS,
|
|
253
|
+
variables: {
|
|
254
|
+
input: {
|
|
255
|
+
id: user.id,
|
|
256
|
+
answers: [...answers.values()],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Returns the PostUp id for the supplied user
|
|
265
|
+
*
|
|
266
|
+
* @param {import('./hooks').IdentityXUser} user
|
|
267
|
+
* @returns {Promise<string>}
|
|
268
|
+
*/
|
|
269
|
+
async getRecipientId(user) {
|
|
270
|
+
const { email } = user;
|
|
271
|
+
const extIds = filterByExternalId(user.externalIds, 'recipient');
|
|
272
|
+
const extId = get(extIds, '0.identifier.value');
|
|
273
|
+
if (extId) return extId;
|
|
274
|
+
|
|
275
|
+
const data = await this.getUserByEmail(email);
|
|
276
|
+
const found = get(data, '0.recipientId');
|
|
277
|
+
if (found) return found;
|
|
278
|
+
|
|
279
|
+
throw new Error(`Unable to find a recipient id for IdentityX user "${user.id}".`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Updates recipient data (fields, demographics, etc)
|
|
284
|
+
*
|
|
285
|
+
* @param {import('./hooks').IdentityXUser} user
|
|
286
|
+
* @param {object} demographics
|
|
287
|
+
* @returns {Promise<any>}
|
|
288
|
+
*/
|
|
289
|
+
async updateRecipient(user, demographics) {
|
|
290
|
+
const { email } = user;
|
|
291
|
+
const formatted = Object.keys(demographics).reduce((arr, k) => {
|
|
292
|
+
arr.push(`${k}=${demographics[k]}`);
|
|
293
|
+
return arr;
|
|
294
|
+
}, []);
|
|
295
|
+
const recipientId = await this.getRecipientId(user);
|
|
296
|
+
return this.client.updateRecipient(recipientId, email, formatted);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {import('./hooks').IdentityXUser} user
|
|
301
|
+
* @param {Map<string, boolean>} subscriptions
|
|
302
|
+
*/
|
|
303
|
+
async updateSubscriptions(user, subscriptions) {
|
|
304
|
+
const recipientId = await this.getRecipientId(user);
|
|
305
|
+
return Promise.all(
|
|
306
|
+
[...subscriptions].map(async ([id, v]) => this.client.updateSubscription(recipientId, id, v)),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Retrieves a recipient by email address
|
|
312
|
+
*
|
|
313
|
+
* @param {string} email
|
|
314
|
+
* @returns {Promise<Recipient>}
|
|
315
|
+
*/
|
|
316
|
+
async getUserByEmail(email) {
|
|
317
|
+
return this.client.returnRecipientDataByEmailAddress(email);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Creates a recipient and sets idx external id
|
|
322
|
+
*
|
|
323
|
+
* @param {string} email
|
|
324
|
+
* @param {string} externalId
|
|
325
|
+
* @param {IdentityXService} idx
|
|
326
|
+
* @returns {Promise}
|
|
327
|
+
*/
|
|
328
|
+
async createRecipient(email, externalId, idx) {
|
|
329
|
+
const ip = idx.req.header('cf-connecting-ip') || idx.req.ip;
|
|
330
|
+
const created = await this.client.createRecipient(email, externalId, ip);
|
|
331
|
+
return this.setExternalIds(idx, externalId, `${created.recipientId}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = PostUp;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @typedef {import("@mindful-web/marko-web-identity-x/service")} IdentityXService
|
|
339
|
+
*/
|
package/utils.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const { getAsObject } = require('@mindful-web/object-path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
filterByExternalId: (arr, type) => arr.filter((v) => {
|
|
5
|
+
const ns = getAsObject(v, 'namespace');
|
|
6
|
+
return ns.provider === 'postup' && ns.type === type && ns.tenant === 'transpire';
|
|
7
|
+
}),
|
|
8
|
+
filterFieldsByExternalId: (arr, type) => arr.filter((v) => {
|
|
9
|
+
const ns = getAsObject(v, 'field.externalId.namespace');
|
|
10
|
+
return ns.provider === 'postup' && ns.type === type && ns.tenant === 'transpire';
|
|
11
|
+
}),
|
|
12
|
+
};
|