@mindful-web/marko-web-identity-x 1.0.0

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.
Files changed (138) hide show
  1. package/.eslintignore +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +200 -0
  4. package/api/fragment-types.json +1 -0
  5. package/api/fragments/active-user.js +108 -0
  6. package/api/mutations/add-external-user-id.js +24 -0
  7. package/api/mutations/create-app-user.js +11 -0
  8. package/api/mutations/delete-app-user-for-current-application.js +7 -0
  9. package/api/mutations/impersonate-app-user.js +12 -0
  10. package/api/mutations/logout-app-user.js +11 -0
  11. package/api/mutations/send-change-email-link.js +7 -0
  12. package/api/mutations/send-login-link.js +7 -0
  13. package/api/mutations/set-custom-attributes.js +15 -0
  14. package/api/queries/check-content-access.js +19 -0
  15. package/api/queries/get-active-context.js +45 -0
  16. package/api/queries/load-user-by-external-id.js +14 -0
  17. package/api/queries/load-user-by-id.js +11 -0
  18. package/api/queries/load-user.js +11 -0
  19. package/browser/.eslintrc.js +11 -0
  20. package/browser/access.vue +377 -0
  21. package/browser/authenticate.vue +278 -0
  22. package/browser/change-email-confirm.vue +120 -0
  23. package/browser/change-email-init.vue +179 -0
  24. package/browser/comments/create.vue +107 -0
  25. package/browser/comments/post.vue +151 -0
  26. package/browser/comments/stream.vue +319 -0
  27. package/browser/custom-column.vue +314 -0
  28. package/browser/dayjs.js +6 -0
  29. package/browser/download-related.vue +120 -0
  30. package/browser/download.vue +380 -0
  31. package/browser/errors/authentication.js +7 -0
  32. package/browser/errors/feature.js +7 -0
  33. package/browser/errors/form.js +7 -0
  34. package/browser/errors/logout.js +7 -0
  35. package/browser/form/address-block.vue +131 -0
  36. package/browser/form/common/checkbox-group.vue +140 -0
  37. package/browser/form/common/form-group.vue +24 -0
  38. package/browser/form/common/form-label.vue +17 -0
  39. package/browser/form/consent.vue +121 -0
  40. package/browser/form/fields/address-extra.vue +68 -0
  41. package/browser/form/fields/city.vue +68 -0
  42. package/browser/form/fields/comment-body.vue +63 -0
  43. package/browser/form/fields/country.vue +94 -0
  44. package/browser/form/fields/custom-boolean.vue +58 -0
  45. package/browser/form/fields/custom-select-group.vue +25 -0
  46. package/browser/form/fields/custom-select-multiple.vue +97 -0
  47. package/browser/form/fields/custom-select-write-in-inline.vue +39 -0
  48. package/browser/form/fields/custom-select-write-in.vue +66 -0
  49. package/browser/form/fields/custom-select.vue +191 -0
  50. package/browser/form/fields/custom-text.vue +62 -0
  51. package/browser/form/fields/display-name.vue +64 -0
  52. package/browser/form/fields/email.vue +62 -0
  53. package/browser/form/fields/family-name.vue +68 -0
  54. package/browser/form/fields/given-name.vue +68 -0
  55. package/browser/form/fields/organization-title.vue +68 -0
  56. package/browser/form/fields/organization.vue +68 -0
  57. package/browser/form/fields/phone-number.vue +68 -0
  58. package/browser/form/fields/postal-code.vue +68 -0
  59. package/browser/form/fields/receive-email.vue +58 -0
  60. package/browser/form/fields/region.vue +106 -0
  61. package/browser/form/fields/regional-policy.vue +59 -0
  62. package/browser/form/fields/street.vue +68 -0
  63. package/browser/graphql/create-client.js +65 -0
  64. package/browser/graphql/parse.js +1 -0
  65. package/browser/graphql/queries/download-related-content.js +38 -0
  66. package/browser/index.js +110 -0
  67. package/browser/login-fields.vue +214 -0
  68. package/browser/login.vue +331 -0
  69. package/browser/logout.vue +68 -0
  70. package/browser/mixins/global-auto-signup-event-emitter.js +18 -0
  71. package/browser/mixins/global-event-emitter.js +28 -0
  72. package/browser/profile.vue +688 -0
  73. package/browser/service.js +32 -0
  74. package/browser/utils/clean-path.js +1 -0
  75. package/browser/utils/cookies-enabled.js +10 -0
  76. package/browser/utils/get-as-array.js +6 -0
  77. package/browser/utils/get-referring-page.js +7 -0
  78. package/browser/utils/get.js +8 -0
  79. package/browser/utils/post.js +10 -0
  80. package/browser/utils/redirect.js +7 -0
  81. package/browser/utils/region-country-codes.js +1 -0
  82. package/components/access.marko +24 -0
  83. package/components/access.marko.js +61 -0
  84. package/components/comment-stream.marko +31 -0
  85. package/components/comment-stream.marko.js +71 -0
  86. package/components/context.marko +15 -0
  87. package/components/context.marko.js +52 -0
  88. package/components/form-access.marko +60 -0
  89. package/components/form-access.marko.js +111 -0
  90. package/components/form-authenticate.marko +32 -0
  91. package/components/form-authenticate.marko.js +73 -0
  92. package/components/form-change-email.marko +27 -0
  93. package/components/form-change-email.marko.js +68 -0
  94. package/components/form-login.marko +32 -0
  95. package/components/form-login.marko.js +72 -0
  96. package/components/form-logout.marko +7 -0
  97. package/components/form-logout.marko.js +36 -0
  98. package/components/form-profile.marko +38 -0
  99. package/components/form-profile.marko.js +78 -0
  100. package/components/form-register.marko +2 -0
  101. package/components/form-register.marko.js +32 -0
  102. package/components/identify.marko +17 -0
  103. package/components/identify.marko.js +59 -0
  104. package/components/marko.json +90 -0
  105. package/components/non-auth-identify.marko +15 -0
  106. package/components/non-auth-identify.marko.js +53 -0
  107. package/config.js +161 -0
  108. package/hooks.js +20 -0
  109. package/index.js +30 -0
  110. package/marko.json +6 -0
  111. package/middleware/content-access-state.js +31 -0
  112. package/middleware/content-download-state.js +27 -0
  113. package/middleware.js +32 -0
  114. package/package.json +40 -0
  115. package/routes/access.js +29 -0
  116. package/routes/authenticate.js +58 -0
  117. package/routes/change-email-confirm.js +60 -0
  118. package/routes/change-email-init.js +16 -0
  119. package/routes/comment-count.js +19 -0
  120. package/routes/comments.js +43 -0
  121. package/routes/countries.js +19 -0
  122. package/routes/create-comment.js +25 -0
  123. package/routes/download.js +29 -0
  124. package/routes/flag-comment.js +21 -0
  125. package/routes/index.js +40 -0
  126. package/routes/login-fields.js +35 -0
  127. package/routes/login.js +95 -0
  128. package/routes/logout.js +19 -0
  129. package/routes/profile.js +183 -0
  130. package/routes/regions.js +22 -0
  131. package/service.js +483 -0
  132. package/utils/call-hooks-for.js +28 -0
  133. package/utils/context-cookie.js +30 -0
  134. package/utils/create-client.js +48 -0
  135. package/utils/create-identity-builder.js +33 -0
  136. package/utils/get-as-array.js +6 -0
  137. package/utils/json-error-handler.js +41 -0
  138. package/utils/token-cookie.js +30 -0
package/service.js ADDED
@@ -0,0 +1,483 @@
1
+ const { get, getAsObject } = require('@mindful-web/object-path');
2
+ const { getResponseCookies } = require('@mindful-web/utils');
3
+ const createClient = require('./utils/create-client');
4
+ const getActiveContext = require('./api/queries/get-active-context');
5
+ const checkContentAccess = require('./api/queries/check-content-access');
6
+ const loadUser = require('./api/queries/load-user');
7
+ const appUserByExternalIdQuery = require('./api/queries/load-user-by-external-id');
8
+ const appUserByIdQuery = require('./api/queries/load-user-by-id');
9
+ const addExternalUserId = require('./api/mutations/add-external-user-id');
10
+ const setCustomAttributes = require('./api/mutations/set-custom-attributes');
11
+ const impersonateAppUser = require('./api/mutations/impersonate-app-user');
12
+ const sendChangeEmailLinkMutation = require('./api/mutations/send-change-email-link');
13
+ const sendLoginLinkMutation = require('./api/mutations/send-login-link');
14
+ const createAppUser = require('./api/mutations/create-app-user');
15
+ const logoutAppUser = require('./api/mutations/logout-app-user');
16
+ const deleteAppUserForCurrentApplicationMutation = require('./api/mutations/delete-app-user-for-current-application');
17
+ const tokenCookie = require('./utils/token-cookie');
18
+ const callHooksFor = require('./utils/call-hooks-for');
19
+
20
+ const isEmpty = (v) => v == null || v === '';
21
+ const isFn = (v) => typeof v === 'function';
22
+ const IDENTITY_COOKIE_NAME = '__idx_idt';
23
+
24
+ /**
25
+ * @typedef ActiveContext
26
+ * @prop {Application} application
27
+ * @prop {Boolean} hasUser
28
+ * @prop {User} user
29
+ * @prop {Boolean} hasTeams
30
+ * @prop {Team[]} mergedTeams
31
+ * @prop {AccessLevel[]} mergedAccessLevels
32
+ *
33
+ * @typedef Application
34
+ * @prop {String} id
35
+ * @prop {String} name
36
+ * @prop {Object} organization
37
+ *
38
+ * @typedef User
39
+ * @prop {String} id
40
+ * @prop {String} email
41
+ * @prop {Boolean} verified
42
+ * @prop {Number} verifiedCount
43
+ * @prop {String} givenName
44
+ * @prop {String} familyName
45
+ * @prop {String} displayName
46
+ * @prop {String} organization
47
+ * @prop {String} organizationTitle
48
+ * @prop {String} countryCode
49
+ * @prop {String} regionCode
50
+ * @prop {String} postalCode
51
+ * @prop {String} city
52
+ * @prop {String} street
53
+ * @prop {String} addressExtra
54
+ * @prop {String} mobileNumber
55
+ * @prop {String} phoneNumber
56
+ * @prop {Boolean} receiveEmail
57
+ * @prop {Boolean} mustReVerifyProfile
58
+ * @prop {ExternalId[]} externalIds
59
+ * @prop {Object} customAttributes
60
+ * @prop {RegionalConsentAnswer[]} regionalConsentAnswers
61
+ * @prop {CustomBooleanFieldAnswer[]} customBooleanFieldAnswers
62
+ * @prop {CustomSelectFieldAnswer[]} customSelectFieldAnswers
63
+ * @prop {CustomTextFieldAnswer[]} customTextFieldAnswers
64
+ *
65
+ * @typedef ExternalId
66
+ * @prop {String} id
67
+ * @prop {ExternalIdIdentifier} identifier
68
+ * @prop {ExternalIdNamespace} namespace
69
+ *
70
+ * @typedef ExternalIdIdentifier
71
+ * @prop {String} value
72
+ * @prop {String} type
73
+ *
74
+ * @typedef ExternalIdNamespace
75
+ * @prop {String} provider
76
+ * @prop {String} tenant
77
+ * @prop {String} typedef
78
+ *
79
+ * @typedef RegionalConsentAnswer
80
+ * @prop {String} id
81
+ * @prop {Boolean} given
82
+ * @prop {Date} date
83
+ *
84
+ * @typedef CustomBooleanFieldAnswer
85
+ * @prop {String} id
86
+ * @prop {Boolean} hasAnswered
87
+ * @prop {BooleanFieldAnswer} answer
88
+ * @prop {BooleanField} field
89
+ *
90
+ * @typedef CustomSelectFieldAnswer
91
+ * @prop {String} id
92
+ * @prop {Boolean} hasAnswered
93
+ * @prop {SelectFieldAnswer[]} answers
94
+ * @prop {SelectField} field
95
+ *
96
+ * @typedef SelectField
97
+ * @typedef SelectFieldAnswer
98
+ * @typedef BooleanField
99
+ * @typedef BooleanFieldAnswer
100
+ *
101
+ * @typedef Team
102
+ * @prop {String} id
103
+ * @prop {String} name
104
+ * @prop {String} photoURL
105
+ *
106
+ * @typedef AccessLevel
107
+ * @prop {String} id
108
+ * @prop {String} name
109
+ */
110
+
111
+ class IdentityX {
112
+ /**
113
+ * @param {Object} o
114
+ * @param {import('express').Request} o.req
115
+ * @param {import('express').Response} o.res
116
+ * @param {import('./config')} o.config
117
+ */
118
+ constructor({
119
+ req,
120
+ res,
121
+ config,
122
+ } = {}) {
123
+ this.req = req;
124
+ this.res = res;
125
+ this.config = config;
126
+ this.setToken(tokenCookie.getFrom(req));
127
+ }
128
+
129
+ /**
130
+ * Validates the email address with a user-supplied function, if present.
131
+ * @param {String} email
132
+ * @returns {[Boolean, String]} The validation status and error message (if present)
133
+ */
134
+ async validateEmail(email) {
135
+ const targetFn = get(this, 'config.options.emailValidator');
136
+ const fn = isFn(targetFn) ? targetFn : () => [Boolean(email)];
137
+ return fn({ email, req: this.req });
138
+ }
139
+
140
+ /**
141
+ * Loads the current application, user, and team context.
142
+ *
143
+ * @returns {Promise<ActiveContext>}
144
+ */
145
+ async loadActiveContext({ forceQuery = false } = {}) {
146
+ // Only run the active context query once
147
+ if (!this.activeContextQuery || forceQuery) {
148
+ this.activeContextQuery = this.client.query({ query: getActiveContext });
149
+ }
150
+ const { data = {} } = await this.activeContextQuery;
151
+ const activeContext = data.activeAppContext || {};
152
+ await callHooksFor(this, 'onLoadActiveContext', {
153
+ req: this.req,
154
+ res: this.res,
155
+ activeContext,
156
+ });
157
+ return activeContext;
158
+ }
159
+
160
+ /**
161
+ * Returns an IdentityX user record by email address
162
+ */
163
+ async loadAppUserByEmail(email) {
164
+ const { data } = await this.client.query({
165
+ query: loadUser,
166
+ variables: { email },
167
+ fetchPolicy: 'no-cache',
168
+ });
169
+ return data.appUser;
170
+ }
171
+
172
+ /**
173
+ * Checks whether the current user can access the given content.
174
+ *
175
+ * @param {object} input
176
+ * @returns {Promise<object>}
177
+ */
178
+ async checkContentAccess(input) {
179
+ const variables = { input };
180
+ const { data = {} } = await this.client.query({ query: checkContentAccess, variables });
181
+ const access = data.checkContentAccess || {};
182
+ access.requiresUserInput = false;
183
+
184
+ const requiredFields = this.config.getRequiredServerFields();
185
+ if (access.isLoggedIn && requiredFields.length) {
186
+ // Check if the user requires additonal input.
187
+ const { user, application } = await this.loadActiveContext();
188
+
189
+ access.requiresUserInput = user ? requiredFields.some((key) => isEmpty(user[key])) : false;
190
+ if (user && !access.requiresUserInput) {
191
+ access.requiresUserInput = Boolean(user.mustReVerifyProfile);
192
+ }
193
+ if (user && !access.requiresUserInput) {
194
+ // Check if user needs to answer any globally required custom fields.
195
+ const ids = this.config.getActiveCustomFieldIds();
196
+ access.requiresUserInput = user.customSelectFieldAnswers
197
+ .filter(!ids.length ? ({ field }) => ids.includes(field.id) : () => true)
198
+ .some(({ hasAnswered, field }) => field.required && !hasAnswered);
199
+ }
200
+
201
+ if (user && !access.requiresUserInput) {
202
+ const { regionalConsentPolicies } = application.organization;
203
+ const matchingPolicies = regionalConsentPolicies.filter((policy) => {
204
+ const countryCodes = policy.countries.map((country) => country.id);
205
+ return countryCodes.includes(user.countryCode);
206
+ });
207
+ const policiesAnswered = user.regionalConsentAnswers
208
+ .reduce((o, answer) => ({ [answer.id]: true }), {});
209
+ const hasRequiredAnswers = matchingPolicies.length
210
+ ? matchingPolicies.every((policy) => policiesAnswered[policy.id])
211
+ : true;
212
+ access.requiresUserInput = !hasRequiredAnswers;
213
+ }
214
+ }
215
+ return access;
216
+ }
217
+
218
+ /**
219
+ * Returns the identity id from the request or supplied response.
220
+ *
221
+ * @param {import('express').Response} res
222
+ * @returns {String}
223
+ */
224
+ getIdentity(res) {
225
+ const id = get(this.req, `cookies.${IDENTITY_COOKIE_NAME}`);
226
+ if (id) return id;
227
+ const { [IDENTITY_COOKIE_NAME]: resid } = getResponseCookies(res || this.res);
228
+ return resid;
229
+ }
230
+
231
+ /**
232
+ * Returns GTM formatted data for the current identity/user
233
+ * @returns {Object}
234
+ */
235
+ async getIdentityData() {
236
+ const context = await this.loadActiveContext();
237
+ if (context.user) return this.getGTMUserData(context.user, 'authenticated');
238
+ const identity = this.getIdentity(this.res);
239
+ if (identity) {
240
+ const user = await this.findUserById(identity);
241
+ if (user) return this.getGTMUserData(user, 'identified');
242
+ }
243
+ return this.getGTMUserData({}, 'anonymous');
244
+ }
245
+
246
+ /**
247
+ * Formats user data for use with GTM
248
+ *
249
+ * @param {User} user
250
+ * @returns {Object}
251
+ */
252
+ getGTMUserData(user, state = 'anonymous') {
253
+ return {
254
+ ...this.config.getGTMUserData(user),
255
+ state,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Sets the IdentityX Identity cookie to the response
261
+ */
262
+ setIdentityCookie(id) {
263
+ const options = {
264
+ maxAge: 60 * 60 * 24 * 365,
265
+ httpOnly: false,
266
+ };
267
+ this.res.cookie(IDENTITY_COOKIE_NAME, id, options);
268
+ }
269
+
270
+ /**
271
+ * @param {Object} o
272
+ * @param {String} o.identifier
273
+ * @param {ExternalIdNamespace} o.namespace
274
+ *
275
+ * @returns {Promise<User|null>}
276
+ */
277
+ async findUserByExternalId({ identifier, namespace }) {
278
+ const apiToken = this.config.getApiToken();
279
+ if (!apiToken) throw new Error('Unable to look up user: No API token has been configured.');
280
+ const { data } = await this.client.query({
281
+ query: appUserByExternalIdQuery,
282
+ variables: {
283
+ identifier: { value: identifier },
284
+ namespace,
285
+ },
286
+ context: { apiToken },
287
+ });
288
+ return data.appUserByExternalId;
289
+ }
290
+
291
+ /**
292
+ * @param {String} id The user id
293
+ *
294
+ * @returns {Promise<User|null>}
295
+ */
296
+ async findUserById(id) {
297
+ const apiToken = this.config.getApiToken();
298
+ if (!apiToken) throw new Error('Unable to retrieve identity: No API token has been configured.');
299
+ const { data } = await this.client.query({
300
+ query: appUserByIdQuery,
301
+ variables: { id },
302
+ context: { apiToken },
303
+ });
304
+ return data.appUserById;
305
+ }
306
+
307
+ /**
308
+ *
309
+ * @param {object} params
310
+ * @returns {Promise<object>}
311
+ */
312
+ async addExternalUserId({
313
+ userId,
314
+ identifier = {},
315
+ namespace = {},
316
+ } = {}) {
317
+ const input = { userId, identifier, namespace };
318
+ const apiToken = this.config.getApiToken();
319
+ if (!apiToken) throw new Error('Unable to add external ID: No API token has been configured.');
320
+ const variables = { input };
321
+ const { data } = await this.client.mutate({
322
+ mutation: addExternalUserId,
323
+ variables,
324
+ context: { apiToken },
325
+ });
326
+ return data.addAppUserExternalId;
327
+ }
328
+
329
+ /**
330
+ *
331
+ * @param {*} param0
332
+ */
333
+ setToken(token) {
334
+ this.token = token;
335
+
336
+ // Re-init the client if the token changed
337
+ this.client = createClient({
338
+ req: this.req,
339
+ token,
340
+ appId: this.config.getAppId(),
341
+ config: this.config,
342
+ });
343
+ }
344
+
345
+ async impersonateAppUser({ userId }) {
346
+ const apiToken = this.config.getApiToken();
347
+ if (!apiToken) throw new Error('Unable to add external ID: No API token has been configured.');
348
+ const variables = { input: { id: userId, verify: true } };
349
+ const { data } = await this.client.mutate({
350
+ mutation: impersonateAppUser,
351
+ variables,
352
+ context: { apiToken },
353
+ });
354
+ const { token } = data.impersonateAppUser;
355
+ tokenCookie.setTo(this.res, token.value);
356
+ this.setToken(token);
357
+ }
358
+
359
+ async logoutAppUser() {
360
+ const { token } = this;
361
+ const input = { token };
362
+ const variables = { input };
363
+ const user = getAsObject({
364
+ ...(token && await this.client.mutate({ mutation: logoutAppUser, variables })),
365
+ }, 'data.logoutAppUserWithData');
366
+ tokenCookie.removeFrom(this.res);
367
+ await callHooksFor(this, 'onLogout', {
368
+ req: this.req,
369
+ res: this.res,
370
+ user,
371
+ });
372
+ this.token = null;
373
+ }
374
+
375
+ /**
376
+ * Creates an AppUser from an email address
377
+ */
378
+ async createAppUser({ email }) {
379
+ const [valid, msg] = await this.validateEmail(email);
380
+ if (!valid) throw new Error(msg || 'The supplied email address cannot be used.');
381
+ const { data } = await this.client.mutate({
382
+ mutation: createAppUser,
383
+ variables: { email },
384
+ });
385
+ return data.createAppUser;
386
+ }
387
+
388
+ /**
389
+ * Sets custom key-value data on the user.
390
+ */
391
+ async setAppUserCustomAttributes({ userId, attributes = {}, dispatch = true } = {}) {
392
+ const { data: { updateOwnAppUserCustomAttributes: user } } = await this.client.mutate({
393
+ mutation: setCustomAttributes,
394
+ variables: { input: { id: userId, attributes } },
395
+ });
396
+ if (dispatch) await callHooksFor(this, 'onUserProfileUpdate', { req: this.req, user });
397
+ return user;
398
+ }
399
+
400
+ /**
401
+ * Sends a change email link to an existing user
402
+ */
403
+ async sendChangeEmailLink({ email }) {
404
+ const authUrl = `${this.req.protocol}://${this.req.get('host')}${this.config.getEndpointFor('changeEmail')}`;
405
+ await this.client.mutate({
406
+ mutation: sendChangeEmailLinkMutation,
407
+ variables: { input: { email, authUrl, appContextId: this.config.get('appContextId') } },
408
+ });
409
+ await callHooksFor(this, 'onChangeEmailLinkSent', { email });
410
+ }
411
+
412
+ /**
413
+ * @typedef GenerateEntityIdArgs
414
+ * @prop {?string} appId The application id to generate for
415
+ * @prop {?string} userId The user id to genereate for
416
+ *
417
+ * @param {GenerateEntityIdArgs} args
418
+ * @returns {?string} The entityId of the active user/identity, if present.
419
+ */
420
+ async generateEntityId({ appId, userId } = {}) {
421
+ const activeContext = await this.loadActiveContext();
422
+ const applicationId = appId || activeContext.application.id;
423
+ const activeUserId = (activeContext && activeContext.user)
424
+ ? activeContext.user.id : await this.getIdentity();
425
+ const uid = userId || activeUserId;
426
+ return `identity-x.${applicationId}.app-user*${uid}`;
427
+ }
428
+
429
+ /**
430
+ * Sends a login link to an existing user
431
+ */
432
+ async sendLoginLink({
433
+ appUser,
434
+ authUrl,
435
+ source,
436
+ redirectTo,
437
+ additionalEventData,
438
+ }) {
439
+ await this.client.mutate({
440
+ mutation: sendLoginLinkMutation,
441
+ variables: {
442
+ input: {
443
+ email: appUser.email,
444
+ authUrl: authUrl || `${this.req.protocol}://${this.req.get('host')}${this.config.getEndpointFor('authenticate')}`,
445
+ appContextId: this.config.get('appContextId'),
446
+ source,
447
+ redirectTo,
448
+ },
449
+ },
450
+ });
451
+ await callHooksFor(this, 'onLoginLinkSent', {
452
+ ...(additionalEventData || {}),
453
+ additionalEventData,
454
+ req: this.req,
455
+ source,
456
+ user: appUser,
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Delete the user record for the current application using the provided email or id
462
+ */
463
+ async deleteAppUserForCurrentApplication({
464
+ email,
465
+ userId,
466
+ }) {
467
+ const apiToken = this.config.getApiToken();
468
+ if (!apiToken) throw new Error('Unable to delete user: No API token has been configured.');
469
+ const { data } = await this.client.mutate({
470
+ mutation: deleteAppUserForCurrentApplicationMutation,
471
+ variables: {
472
+ input: {
473
+ ...(email && { email }),
474
+ ...(userId && { userId }),
475
+ },
476
+ },
477
+ context: { apiToken },
478
+ });
479
+ return data.deleteAppUserForCurrentApplication;
480
+ }
481
+ }
482
+
483
+ module.exports = IdentityX;
@@ -0,0 +1,28 @@
1
+ const { getAsArray } = require('@mindful-web/object-path');
2
+ const { isFunction: isFn } = require('@mindful-web/utils');
3
+
4
+ /**
5
+ * Calls all registered hooks for the provided hook queue name.
6
+ *
7
+ * @param {IdentityX} service The IdentityX service instance.
8
+ * @param {string} name The hook queue name
9
+ * @param {object} params Parameters to send to the hook function
10
+ */
11
+ module.exports = async (service, name, params = {}) => {
12
+ const { error } = console;
13
+ const onError = service.config.get('onHookError');
14
+ const onHookError = isFn(onError) ? onError : (e) => error(e);
15
+ const promises = { wait: [], skip: [] };
16
+ getAsArray(service, `config.hooks.${name}`).forEach(({ fn, shouldAwait }) => {
17
+ const key = shouldAwait ? 'wait' : 'skip';
18
+ const additionalEventData = params.additionalEventData || {};
19
+ const maybePromise = fn({
20
+ ...params,
21
+ additionalEventData,
22
+ service,
23
+ });
24
+ if (maybePromise.catch) maybePromise.catch(onHookError);
25
+ promises[key].push(maybePromise);
26
+ });
27
+ if (promises.wait.length) await Promise.all(promises.wait).catch(onHookError);
28
+ };
@@ -0,0 +1,30 @@
1
+ const cookie = require('cookie');
2
+
3
+ const COOKIE_NAME = '__idx_ctx';
4
+ const COOKIE_MAX_AGE = 1000 * 60 * 60 * 24 * 365 * 2;
5
+
6
+ const setTo = (res, context) => {
7
+ res.cookie(COOKIE_NAME, JSON.stringify(context), { maxAge: COOKIE_MAX_AGE, httpOnly: false });
8
+ };
9
+
10
+ const getFrom = (req) => {
11
+ try {
12
+ const cookies = cookie.parse(req.get('cookie') || '');
13
+ const { [COOKIE_NAME]: value } = cookies;
14
+ return value;
15
+ } catch (e) {
16
+ // @todo log this error.
17
+ return undefined;
18
+ }
19
+ };
20
+
21
+ const removeFrom = (res) => {
22
+ res.clearCookie(COOKIE_NAME);
23
+ };
24
+
25
+ module.exports = {
26
+ exists: (req) => Boolean(getFrom(req)),
27
+ setTo,
28
+ getFrom,
29
+ removeFrom,
30
+ };
@@ -0,0 +1,48 @@
1
+ const fetch = require('node-fetch');
2
+ const { ApolloClient } = require('apollo-client');
3
+ const { InMemoryCache, IntrospectionFragmentMatcher } = require('apollo-cache-inmemory');
4
+ const { createHttpLink } = require('apollo-link-http');
5
+ const { setContext } = require('apollo-link-context');
6
+ const introspectionQueryResultData = require('../api/fragment-types.json');
7
+
8
+ const rootConfig = {
9
+ connectToDevTools: false,
10
+ ssrMode: true,
11
+ };
12
+
13
+ module.exports = ({
14
+ req,
15
+ token,
16
+ appId,
17
+ config,
18
+ linkConfig = {},
19
+ } = {}) => {
20
+ if (!appId) throw new Error('An IdentityX application ID is required.');
21
+
22
+ const headers = {
23
+ ...linkConfig.headers,
24
+ 'x-app-id': appId,
25
+ 'x-forwarded-for': req.ip,
26
+ 'user-agent': req.get('user-agent'),
27
+ };
28
+ if (token) headers.authorization = `AppUser ${token}`;
29
+
30
+ const httpLink = createHttpLink({
31
+ ...linkConfig,
32
+ uri: process.env.IDENTITYX_GRAPHQL_URI || 'https://identity-x.parameter1.com/graphql',
33
+ fetch,
34
+ headers,
35
+ });
36
+ const contextLink = setContext((_, { apiToken }) => ({
37
+ headers: { ...(apiToken && { authorization: `OrgUserApiToken ${apiToken}` }) },
38
+ }));
39
+
40
+ return new ApolloClient({
41
+ ...config,
42
+ ...rootConfig,
43
+ link: contextLink.concat(httpLink),
44
+ cache: new InMemoryCache({
45
+ fragmentMatcher: new IntrospectionFragmentMatcher({ introspectionQueryResultData }),
46
+ }),
47
+ });
48
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @typedef IdentityXContext
3
+ * @prop {IdentityXApp} application
4
+ * @prop {IdentityXAppUser|null} user
5
+ * @prop {Object[]} mergedTeams
6
+ * @prop {Object[]} mergedAccessLevels
7
+ * @prop {Boolean} hasTeams
8
+ * @prop {Boolean} hasUser
9
+ * @prop {Boolean} isEnabled
10
+ *
11
+ * @typedef IdentityXApp
12
+ * @prop {String} id
13
+ * @prop {String} name
14
+ * @prop {String} consentPolicy
15
+ * @prop {String} emailConsentRequest
16
+ * @prop {Object[]} regionalConsentPolicies
17
+ *
18
+ * @typedef IdentityXAppUser
19
+ * @prop {String} id
20
+ * @prop {String} email
21
+ *
22
+ * @param {IdentityXContext} context
23
+ * @param {String|null} identity
24
+ */
25
+ module.exports = (context = {}, identity = null) => {
26
+ if (context.hasUser) {
27
+ return `return 'identity-x.${context.application.id}.app-user*' + '${context.user.id}';`;
28
+ }
29
+ if (identity) {
30
+ return `return 'identity-x.${context.application.id}.app-user*' + '${identity}';`;
31
+ }
32
+ return '';
33
+ };
@@ -0,0 +1,6 @@
1
+ import { get } from 'object-path';
2
+
3
+ export default (obj, path) => {
4
+ const v = get(obj, path, []);
5
+ return Array.isArray(v) ? v : [];
6
+ };
@@ -0,0 +1,41 @@
1
+ const { get, getAsArray } = require('@mindful-web/object-path');
2
+
3
+ const dev = process.env.NODE_ENV === 'development';
4
+ const { isArray } = Array;
5
+
6
+ const render = (res, { status, err }) => {
7
+ const { message, stack } = err;
8
+ const obj = { error: true, status, message };
9
+ if (dev && stack) obj.stack = stack.split('\n');
10
+ res.status(status).json(obj);
11
+ };
12
+
13
+ const renderNetworkErrorResult = (res, { status, result }) => {
14
+ const message = result.errors && isArray(result.errors) ? result.errors[0].message : 'Unknown fatal.';
15
+ render(res, { status, err: new Error(message) });
16
+ };
17
+
18
+ const renderGraphqlError = (res, graphQLError) => {
19
+ const { message } = graphQLError;
20
+ const err = new Error(message);
21
+ err.stack = getAsArray(graphQLError, 'extensions.exception.stacktrace').join('\n');
22
+ const status = get(graphQLError, 'extensions.exception.statusCode', 500);
23
+ render(res, { status, err });
24
+ };
25
+
26
+ // eslint-disable-next-line no-unused-vars
27
+ module.exports = () => (err, req, res, next) => {
28
+ const status = err.statusCode || err.status || 500;
29
+ const { networkError, graphQLErrors } = err;
30
+ if (networkError) {
31
+ if (networkError.result) {
32
+ renderNetworkErrorResult(res, { status, result: networkError.result });
33
+ } else {
34
+ render(res, { status, err: networkError });
35
+ }
36
+ } else if (graphQLErrors && graphQLErrors.length) {
37
+ renderGraphqlError(res, graphQLErrors[0]);
38
+ } else {
39
+ render(res, { status, err });
40
+ }
41
+ };
@@ -0,0 +1,30 @@
1
+ const cookie = require('cookie');
2
+
3
+ const COOKIE_NAME = '__idx';
4
+ const COOKIE_MAX_AGE = 1000 * 60 * 60 * 24 * 365 * 2;
5
+
6
+ const setTo = (res, token) => {
7
+ res.cookie(COOKIE_NAME, token, { maxAge: COOKIE_MAX_AGE, httpOnly: false });
8
+ };
9
+
10
+ const getFrom = (req) => {
11
+ try {
12
+ const cookies = cookie.parse(req.get('cookie') || '');
13
+ const { [COOKIE_NAME]: token } = cookies;
14
+ return token;
15
+ } catch (e) {
16
+ // @todo log this error.
17
+ return undefined;
18
+ }
19
+ };
20
+
21
+ const removeFrom = (res) => {
22
+ res.clearCookie(COOKIE_NAME);
23
+ };
24
+
25
+ module.exports = {
26
+ exists: (req) => Boolean(getFrom(req)),
27
+ setTo,
28
+ getFrom,
29
+ removeFrom,
30
+ };