@mindful-web/marko-web-identity-x 1.79.6 → 1.79.7
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/CLAUDE.md +29 -0
- package/components/access.marko.js +2 -2
- package/components/comment-stream.marko.js +2 -2
- package/components/context.marko.js +2 -2
- package/components/form-access.marko.js +2 -2
- package/components/form-authenticate.marko.js +2 -2
- package/components/form-change-email.marko.js +2 -2
- package/components/form-login.marko.js +2 -2
- package/components/form-logout.marko.js +2 -2
- package/components/form-profile.marko.js +2 -2
- package/components/form-register.marko.js +2 -2
- package/components/google-init.marko +82 -14
- package/components/google-init.marko.js +85 -16
- package/components/google-sign-in-button.marko.js +2 -2
- package/components/identify.marko.js +2 -2
- package/components/non-auth-identify.marko.js +2 -2
- package/components/subscribe.marko.js +2 -2
- package/config.js +21 -4
- package/package.json +2 -2
- package/routes/google.js +45 -4
- package/service.js +7 -2
- package/utils/add-custom-attributes.js +50 -0
package/CLAUDE.md
CHANGED
|
@@ -119,6 +119,35 @@ login/authenticate pages get the button with **no per-site edit**. For the site-
|
|
|
119
119
|
document layout, gated on `!req.identityX.token`. The button tag can also be placed in a content
|
|
120
120
|
meter / gate.
|
|
121
121
|
|
|
122
|
+
**Telemetry.** Two seams. Neither needs site configuration:
|
|
123
|
+
- `config.addRequestAttributes` — called with a flat object as the route progresses
|
|
124
|
+
(`idxGoogleStep`, `idxUserId`, `idxCreatedNewUser`, `idxRequiresUserInput`, `idxGoogleSurface`,
|
|
125
|
+
`idxGoogleMissingFieldsBehavior`). **Defaults to `utils/add-custom-attributes.js`**, which tags
|
|
126
|
+
the current New Relic transaction, so the attributes ride on the `Transaction` *and* any
|
|
127
|
+
`TransactionError` it produces — a 500 says which step failed instead of being a bare GraphQL
|
|
128
|
+
message. That default resolves the agent **only if the app already loaded it** (checks
|
|
129
|
+
`require.cache`, never requires it cold — requiring `newrelic` would *start* an agent on sites
|
|
130
|
+
that don't use it), so it arrives fleet-wide on a dep bump and is inert everywhere else. Pass a
|
|
131
|
+
function to override, or a no-op to disable. Deliberately **not** `noticeError`: that records a
|
|
132
|
+
second error next to the one the error handler already captures and doubles the counts.
|
|
133
|
+
- `config.onHookError` — non-fatal errors, now called as `(error, customAttributes)` to match
|
|
134
|
+
`newrelic.noticeError`. The Google `sub` linkage failures report through it with their step and
|
|
135
|
+
user id, which keeps them distinguishable from fatal errors on the same route (they arrive on a
|
|
136
|
+
200).
|
|
137
|
+
|
|
138
|
+
Client-side, `fail()` fires a p1events `Identity / Authenticate Failed` event mirroring the success
|
|
139
|
+
event. There is no New Relic Browser agent on the monorail sites, so without it a client-side
|
|
140
|
+
failure reaches no backend at all. The originating surface (`button` | `prompt`, from GIS
|
|
141
|
+
`select_by`) is posted to the route as `body.surface` — both surfaces otherwise share a route, a
|
|
142
|
+
`loginSource` and an impersonation method, so nothing server-side could tell them apart.
|
|
143
|
+
|
|
144
|
+
**Failure handling.** All client-side failures funnel through `fail()` in `google-init.marko`: it
|
|
145
|
+
always `console.error`s, writes to `#google-sign-in-error` when that element is on the page (it is
|
|
146
|
+
rendered by `google-sign-in-button.marko`, so it exists only where the button does), and otherwise
|
|
147
|
+
— the site-wide floating prompt case — redirects to the login endpoint with `?redirectTo=`. Never
|
|
148
|
+
let a failure be a silent no-op: GIS closes the prompt on its own, so the user reads "nothing
|
|
149
|
+
happened" as success and only discovers they're anonymous on the next page load.
|
|
150
|
+
|
|
122
151
|
**CSS.** `.google-sign-in` styles ship in `marko-web-theme-monorail/scss/components/_identity-x.scss`
|
|
123
152
|
(with the rest of the auth CSS) — no separate import.
|
|
124
153
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/access.marko",
|
|
6
6
|
marko_component = require("./access.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -53,7 +53,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
53
53
|
}, marko_component);
|
|
54
54
|
|
|
55
55
|
marko_template.meta = {
|
|
56
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
56
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/access.marko",
|
|
57
57
|
component: "./access.marko",
|
|
58
58
|
tags: [
|
|
59
59
|
"@mindful-web/marko-core/components/resolve.marko"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/comment-stream.marko",
|
|
6
6
|
marko_component = require("./comment-stream.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -91,7 +91,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
91
91
|
}, marko_component);
|
|
92
92
|
|
|
93
93
|
marko_template.meta = {
|
|
94
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
94
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/comment-stream.marko",
|
|
95
95
|
component: "./comment-stream.marko",
|
|
96
96
|
tags: [
|
|
97
97
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/context.marko",
|
|
6
6
|
marko_component = require("./context.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
|
|
@@ -49,7 +49,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
49
49
|
}, marko_component);
|
|
50
50
|
|
|
51
51
|
marko_template.meta = {
|
|
52
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
52
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/context.marko",
|
|
53
53
|
component: "./context.marko",
|
|
54
54
|
tags: [
|
|
55
55
|
"@mindful-web/marko-core/components/resolve.marko"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/form-access.marko",
|
|
6
6
|
marko_component = require("./form-access.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
|
|
@@ -179,7 +179,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
179
179
|
}, marko_component);
|
|
180
180
|
|
|
181
181
|
marko_template.meta = {
|
|
182
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
182
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-access.marko",
|
|
183
183
|
component: "./form-access.marko",
|
|
184
184
|
tags: [
|
|
185
185
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/form-authenticate.marko",
|
|
6
6
|
marko_component = require("./form-authenticate.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -81,7 +81,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
81
81
|
}, marko_component);
|
|
82
82
|
|
|
83
83
|
marko_template.meta = {
|
|
84
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
84
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-authenticate.marko",
|
|
85
85
|
component: "./form-authenticate.marko",
|
|
86
86
|
tags: [
|
|
87
87
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/form-change-email.marko",
|
|
6
6
|
marko_component = require("./form-change-email.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -59,7 +59,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
59
59
|
}, marko_component);
|
|
60
60
|
|
|
61
61
|
marko_template.meta = {
|
|
62
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
62
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-change-email.marko",
|
|
63
63
|
component: "./form-change-email.marko",
|
|
64
64
|
tags: [
|
|
65
65
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/form-login.marko",
|
|
6
6
|
marko_component = require("./form-login.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -92,7 +92,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
92
92
|
}, marko_component);
|
|
93
93
|
|
|
94
94
|
marko_template.meta = {
|
|
95
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
95
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-login.marko",
|
|
96
96
|
component: "./form-login.marko",
|
|
97
97
|
tags: [
|
|
98
98
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/form-logout.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
|
|
8
8
|
defaultValue = module_defaultValue.default || module_defaultValue,
|
|
@@ -29,7 +29,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
marko_template.meta = {
|
|
32
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
32
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-logout.marko",
|
|
33
33
|
tags: [
|
|
34
34
|
"@mindful-web/marko-web/components/browser-component.marko"
|
|
35
35
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/form-profile.marko",
|
|
6
6
|
marko_component = require("./form-profile.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -84,7 +84,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
84
84
|
}, marko_component);
|
|
85
85
|
|
|
86
86
|
marko_template.meta = {
|
|
87
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
87
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-profile.marko",
|
|
88
88
|
component: "./form-profile.marko",
|
|
89
89
|
tags: [
|
|
90
90
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/form-register.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
marko_assign = require("marko/dist/runtime/helpers/assign"),
|
|
8
8
|
marko_web_identity_x_form_login_template = require("./form-login.marko"),
|
|
@@ -25,7 +25,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
marko_template.meta = {
|
|
28
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
28
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-register.marko",
|
|
29
29
|
tags: [
|
|
30
30
|
"./form-login.marko"
|
|
31
31
|
]
|
|
@@ -2,6 +2,7 @@ $ const { req } = out.global;
|
|
|
2
2
|
$ const config = req.identityX && req.identityX.config;
|
|
3
3
|
$ const { clientId, autoPrompt } = config ? config.getAsObject('googleAuth') : {};
|
|
4
4
|
$ const callbackName = 'handleGoogleOneTapCredential';
|
|
5
|
+
$ const loginUrl = config ? config.getEndpointFor('login') : '/user/login';
|
|
5
6
|
$ const initScript = `
|
|
6
7
|
(function() {
|
|
7
8
|
window.${callbackName} = function(response) {
|
|
@@ -12,24 +13,94 @@ $ const initScript = `
|
|
|
12
13
|
// GIS 'select_by' distinguishes the surface: a 'btn*' value means the
|
|
13
14
|
// rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
|
|
14
15
|
// prompt. Used to label the p1events authentication event accordingly.
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
var isButton = String(response.select_by || '').indexOf('btn') === 0;
|
|
17
|
+
var eventLabel = isButton ? 'Google Sign In' : 'Google One Tap';
|
|
18
|
+
|
|
19
|
+
// Every failure path routes through here. The site-wide floating One-Tap
|
|
20
|
+
// prompt renders outside any form, so there is no '#google-sign-in-error'
|
|
21
|
+
// element on the page — previously that meant a failed sign-in did nothing
|
|
22
|
+
// at all: GIS closed the prompt, the user assumed success, and only found
|
|
23
|
+
// out they were still anonymous on the next page load. Always log, and
|
|
24
|
+
// fall back to the login page when there's nowhere to show the message.
|
|
25
|
+
var authenticated = false;
|
|
26
|
+
function fail(message, detail) {
|
|
27
|
+
if (window.console && window.console.error) {
|
|
28
|
+
window.console.error('[identity-x google-auth] ' + message, detail);
|
|
29
|
+
}
|
|
30
|
+
// There is no New Relic Browser agent on these sites, so a client-side
|
|
31
|
+
// failure reaches no backend at all unless we send one. p1events is
|
|
32
|
+
// already on every page -- mirror the success event so failed sign-ins
|
|
33
|
+
// are countable next to successful ones rather than invisible.
|
|
34
|
+
if (window.p1events) {
|
|
35
|
+
window.p1events('track', {
|
|
36
|
+
category: 'Identity',
|
|
37
|
+
action: 'Authenticate Failed',
|
|
38
|
+
label: eventLabel,
|
|
39
|
+
props: {
|
|
40
|
+
actionSource: 'google-one-tap',
|
|
41
|
+
surface: isButton ? 'button' : 'prompt',
|
|
42
|
+
message: message,
|
|
43
|
+
status: detail && detail.status ? detail.status : null,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// The session cookie is already set — a throw in the post-success
|
|
48
|
+
// analytics/redirect code must not bounce an authenticated user to login.
|
|
49
|
+
if (authenticated) return;
|
|
50
|
+
if (errorEl) {
|
|
51
|
+
errorEl.textContent = message;
|
|
52
|
+
errorEl.style.display = 'block';
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
var returnTo = window.location.pathname + window.location.search;
|
|
56
|
+
window.location.href = ${JSON.stringify(loginUrl)} + '?redirectTo=' + encodeURIComponent(returnTo);
|
|
57
|
+
}
|
|
18
58
|
|
|
19
59
|
fetch('/__idx/google', {
|
|
20
60
|
method: 'POST',
|
|
21
61
|
headers: { 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify({
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
credential: response.credential,
|
|
64
|
+
redirectTo: redirectTo,
|
|
65
|
+
// Lets the server tag its telemetry with the originating surface --
|
|
66
|
+
// both post here and are otherwise indistinguishable server-side.
|
|
67
|
+
surface: isButton ? 'button' : 'prompt',
|
|
68
|
+
}),
|
|
23
69
|
})
|
|
24
|
-
|
|
25
|
-
|
|
70
|
+
// Parse defensively: a 5xx from the idx router returns
|
|
71
|
+
// { error, status, message } (no 'ok'), and an upstream proxy can return
|
|
72
|
+
// HTML that would otherwise blow up res.json() into the generic catch.
|
|
73
|
+
.then(function(res) {
|
|
74
|
+
return res.text().then(function(text) {
|
|
75
|
+
var parsed = {};
|
|
76
|
+
try { parsed = JSON.parse(text); } catch (e) { /* non-JSON response */ }
|
|
77
|
+
return { status: res.status, redirected: res.redirected, url: res.url, data: parsed };
|
|
78
|
+
});
|
|
79
|
+
})
|
|
80
|
+
.then(function(result) {
|
|
81
|
+
var data = result.data;
|
|
26
82
|
if (!data.ok) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
83
|
+
// A 301/302 on this POST (host canonicalization — apex -> www, or
|
|
84
|
+
// http -> https) is re-issued by the browser as a GET with the body
|
|
85
|
+
// dropped. The idx router only registers POST /__idx/google, so the
|
|
86
|
+
// redirected request falls through to a 404 and the sign-in is lost.
|
|
87
|
+
// Call that out by name rather than reporting a generic failure.
|
|
88
|
+
if (result.redirected) {
|
|
89
|
+
fail(
|
|
90
|
+
'Google sign-in failed: the request was redirected to ' + result.url
|
|
91
|
+
+ ', which turns it into a GET and drops the credential. '
|
|
92
|
+
+ 'Serve this page from the canonical host.',
|
|
93
|
+
{ status: result.status }
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
30
96
|
}
|
|
97
|
+
fail(
|
|
98
|
+
data.error || data.message || 'Google sign-in failed. Please try again.',
|
|
99
|
+
{ status: result.status }
|
|
100
|
+
);
|
|
31
101
|
return;
|
|
32
102
|
}
|
|
103
|
+
authenticated = true;
|
|
33
104
|
if (window.p1events) {
|
|
34
105
|
// De-anonymize the session to the app user, then record the
|
|
35
106
|
// authentication — mirrors the identity-x conversion events.
|
|
@@ -65,11 +136,8 @@ $ const initScript = `
|
|
|
65
136
|
window.location.href = data.redirectTo || window.location.href;
|
|
66
137
|
}
|
|
67
138
|
})
|
|
68
|
-
.catch(function() {
|
|
69
|
-
|
|
70
|
-
errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
|
|
71
|
-
errorEl.style.display = 'block';
|
|
72
|
-
}
|
|
139
|
+
.catch(function(e) {
|
|
140
|
+
fail('An error occurred during Google sign-in. Please try again.', e);
|
|
73
141
|
});
|
|
74
142
|
};
|
|
75
143
|
})();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/google-init.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
marko_attr = require("marko/dist/runtime/html/helpers/attr"),
|
|
8
8
|
marko_str = require("marko/dist/runtime/helpers/to-string");
|
|
@@ -18,6 +18,8 @@ function render(input, out, __component, component, state) {
|
|
|
18
18
|
|
|
19
19
|
const callbackName = 'handleGoogleOneTapCredential';
|
|
20
20
|
|
|
21
|
+
const loginUrl = config ? config.getEndpointFor('login') : '/user/login';
|
|
22
|
+
|
|
21
23
|
const initScript = `
|
|
22
24
|
(function() {
|
|
23
25
|
window.${callbackName} = function(response) {
|
|
@@ -28,24 +30,94 @@ function render(input, out, __component, component, state) {
|
|
|
28
30
|
// GIS 'select_by' distinguishes the surface: a 'btn*' value means the
|
|
29
31
|
// rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
|
|
30
32
|
// prompt. Used to label the p1events authentication event accordingly.
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
var isButton = String(response.select_by || '').indexOf('btn') === 0;
|
|
34
|
+
var eventLabel = isButton ? 'Google Sign In' : 'Google One Tap';
|
|
35
|
+
|
|
36
|
+
// Every failure path routes through here. The site-wide floating One-Tap
|
|
37
|
+
// prompt renders outside any form, so there is no '#google-sign-in-error'
|
|
38
|
+
// element on the page — previously that meant a failed sign-in did nothing
|
|
39
|
+
// at all: GIS closed the prompt, the user assumed success, and only found
|
|
40
|
+
// out they were still anonymous on the next page load. Always log, and
|
|
41
|
+
// fall back to the login page when there's nowhere to show the message.
|
|
42
|
+
var authenticated = false;
|
|
43
|
+
function fail(message, detail) {
|
|
44
|
+
if (window.console && window.console.error) {
|
|
45
|
+
window.console.error('[identity-x google-auth] ' + message, detail);
|
|
46
|
+
}
|
|
47
|
+
// There is no New Relic Browser agent on these sites, so a client-side
|
|
48
|
+
// failure reaches no backend at all unless we send one. p1events is
|
|
49
|
+
// already on every page -- mirror the success event so failed sign-ins
|
|
50
|
+
// are countable next to successful ones rather than invisible.
|
|
51
|
+
if (window.p1events) {
|
|
52
|
+
window.p1events('track', {
|
|
53
|
+
category: 'Identity',
|
|
54
|
+
action: 'Authenticate Failed',
|
|
55
|
+
label: eventLabel,
|
|
56
|
+
props: {
|
|
57
|
+
actionSource: 'google-one-tap',
|
|
58
|
+
surface: isButton ? 'button' : 'prompt',
|
|
59
|
+
message: message,
|
|
60
|
+
status: detail && detail.status ? detail.status : null,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// The session cookie is already set — a throw in the post-success
|
|
65
|
+
// analytics/redirect code must not bounce an authenticated user to login.
|
|
66
|
+
if (authenticated) return;
|
|
67
|
+
if (errorEl) {
|
|
68
|
+
errorEl.textContent = message;
|
|
69
|
+
errorEl.style.display = 'block';
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
var returnTo = window.location.pathname + window.location.search;
|
|
73
|
+
window.location.href = ${JSON.stringify(loginUrl)} + '?redirectTo=' + encodeURIComponent(returnTo);
|
|
74
|
+
}
|
|
34
75
|
|
|
35
76
|
fetch('/__idx/google', {
|
|
36
77
|
method: 'POST',
|
|
37
78
|
headers: { 'Content-Type': 'application/json' },
|
|
38
|
-
body: JSON.stringify({
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
credential: response.credential,
|
|
81
|
+
redirectTo: redirectTo,
|
|
82
|
+
// Lets the server tag its telemetry with the originating surface --
|
|
83
|
+
// both post here and are otherwise indistinguishable server-side.
|
|
84
|
+
surface: isButton ? 'button' : 'prompt',
|
|
85
|
+
}),
|
|
39
86
|
})
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
// Parse defensively: a 5xx from the idx router returns
|
|
88
|
+
// { error, status, message } (no 'ok'), and an upstream proxy can return
|
|
89
|
+
// HTML that would otherwise blow up res.json() into the generic catch.
|
|
90
|
+
.then(function(res) {
|
|
91
|
+
return res.text().then(function(text) {
|
|
92
|
+
var parsed = {};
|
|
93
|
+
try { parsed = JSON.parse(text); } catch (e) { /* non-JSON response */ }
|
|
94
|
+
return { status: res.status, redirected: res.redirected, url: res.url, data: parsed };
|
|
95
|
+
});
|
|
96
|
+
})
|
|
97
|
+
.then(function(result) {
|
|
98
|
+
var data = result.data;
|
|
42
99
|
if (!data.ok) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
// A 301/302 on this POST (host canonicalization — apex -> www, or
|
|
101
|
+
// http -> https) is re-issued by the browser as a GET with the body
|
|
102
|
+
// dropped. The idx router only registers POST /__idx/google, so the
|
|
103
|
+
// redirected request falls through to a 404 and the sign-in is lost.
|
|
104
|
+
// Call that out by name rather than reporting a generic failure.
|
|
105
|
+
if (result.redirected) {
|
|
106
|
+
fail(
|
|
107
|
+
'Google sign-in failed: the request was redirected to ' + result.url
|
|
108
|
+
+ ', which turns it into a GET and drops the credential. '
|
|
109
|
+
+ 'Serve this page from the canonical host.',
|
|
110
|
+
{ status: result.status }
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
46
113
|
}
|
|
114
|
+
fail(
|
|
115
|
+
data.error || data.message || 'Google sign-in failed. Please try again.',
|
|
116
|
+
{ status: result.status }
|
|
117
|
+
);
|
|
47
118
|
return;
|
|
48
119
|
}
|
|
120
|
+
authenticated = true;
|
|
49
121
|
if (window.p1events) {
|
|
50
122
|
// De-anonymize the session to the app user, then record the
|
|
51
123
|
// authentication — mirrors the identity-x conversion events.
|
|
@@ -81,11 +153,8 @@ function render(input, out, __component, component, state) {
|
|
|
81
153
|
window.location.href = data.redirectTo || window.location.href;
|
|
82
154
|
}
|
|
83
155
|
})
|
|
84
|
-
.catch(function() {
|
|
85
|
-
|
|
86
|
-
errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
|
|
87
|
-
errorEl.style.display = 'block';
|
|
88
|
-
}
|
|
156
|
+
.catch(function(e) {
|
|
157
|
+
fail('An error occurred during Google sign-in. Please try again.', e);
|
|
89
158
|
});
|
|
90
159
|
};
|
|
91
160
|
})();
|
|
@@ -108,5 +177,5 @@ marko_template._ = marko_renderer(render, {
|
|
|
108
177
|
});
|
|
109
178
|
|
|
110
179
|
marko_template.meta = {
|
|
111
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
180
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/google-init.marko"
|
|
112
181
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/google-sign-in-button.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
|
|
8
8
|
marko_escapeXml = helpers_escape_xml.x,
|
|
@@ -40,5 +40,5 @@ marko_template._ = marko_renderer(render, {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
marko_template.meta = {
|
|
43
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
43
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/google-sign-in-button.marko"
|
|
44
44
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/identify.marko",
|
|
6
6
|
marko_component = require("./identify.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -50,7 +50,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
50
50
|
}, marko_component);
|
|
51
51
|
|
|
52
52
|
marko_template.meta = {
|
|
53
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
53
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/identify.marko",
|
|
54
54
|
component: "./identify.marko",
|
|
55
55
|
tags: [
|
|
56
56
|
"@mindful-web/marko-web-gtm/components/push.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/non-auth-identify.marko",
|
|
6
6
|
marko_component = require("./non-auth-identify.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
|
|
@@ -45,7 +45,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
45
45
|
}, marko_component);
|
|
46
46
|
|
|
47
47
|
marko_template.meta = {
|
|
48
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
48
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/non-auth-identify.marko",
|
|
49
49
|
component: "./non-auth-identify.marko",
|
|
50
50
|
tags: [
|
|
51
51
|
"@mindful-web/marko-core/components/resolve.marko"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/components/subscribe.marko",
|
|
6
6
|
marko_component = require("./subscribe.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -73,7 +73,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
73
73
|
}, marko_component);
|
|
74
74
|
|
|
75
75
|
marko_template.meta = {
|
|
76
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
76
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.7/components/subscribe.marko",
|
|
77
77
|
component: "./subscribe.marko",
|
|
78
78
|
tags: [
|
|
79
79
|
"@mindful-web/marko-web/components/browser-component.marko",
|
package/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { get, getAsArray, getAsObject } = require('@mindful-web/object-path');
|
|
2
2
|
const { isFunction: isFn } = require('@mindful-web/utils');
|
|
3
3
|
const validHooks = require('./hooks');
|
|
4
|
+
const defaultAddRequestAttributes = require('./utils/add-custom-attributes');
|
|
4
5
|
|
|
5
6
|
const { log } = console;
|
|
6
7
|
|
|
@@ -15,7 +16,17 @@ class IdentityXConfiguration {
|
|
|
15
16
|
* @param {string[]} [options.requiredCreateFieldRows] Required fields, when creating a new user.
|
|
16
17
|
* @param {string[]} [options.activeCustomFieldIds] Limit displayed custom fields, if present.
|
|
17
18
|
* @param {string[]} [options.hiddenFields] The fields to hide from the profile.
|
|
18
|
-
* @param {function} [options.onHookError]
|
|
19
|
+
* @param {function} [options.onHookError] Reporter for non-fatal errors. Called as
|
|
20
|
+
* `(error, customAttributes)` — matches `newrelic.noticeError`.
|
|
21
|
+
* @param {function} [options.addRequestAttributes] Sink for per-request telemetry,
|
|
22
|
+
* called with a flat object of primitives. **Defaults to tagging the current New
|
|
23
|
+
* Relic transaction** when the app has already loaded the agent (every marko-web
|
|
24
|
+
* site does, at boot) — so the attributes ride on both the `Transaction` and any
|
|
25
|
+
* `TransactionError` it produces, and a 500 arrives already saying which step
|
|
26
|
+
* failed and for whom. No site configuration required; inert without New Relic.
|
|
27
|
+
* Deliberately not `noticeError` — that would record a second error alongside the
|
|
28
|
+
* one the error handler already captures, and double the counts. Pass your own
|
|
29
|
+
* function to override, or a no-op to disable.
|
|
19
30
|
* @param {object} [options.googleAuth] Google Sign-In config. When `clientId` is
|
|
20
31
|
* set, the One-Tap prompt / "Sign in with Google" button render in the auth
|
|
21
32
|
* templates and `POST /__idx/google` accepts credentials. Dormant otherwise.
|
|
@@ -40,6 +51,7 @@ class IdentityXConfiguration {
|
|
|
40
51
|
gtmUserFields = {},
|
|
41
52
|
googleAuth = {},
|
|
42
53
|
onHookError,
|
|
54
|
+
addRequestAttributes,
|
|
43
55
|
...rest
|
|
44
56
|
} = {}) {
|
|
45
57
|
if (!appId) throw new Error('Unable to configure IdentityX: no Application ID was provided.');
|
|
@@ -58,13 +70,18 @@ class IdentityXConfiguration {
|
|
|
58
70
|
booleanQuestionsLabel,
|
|
59
71
|
gtmUserFields,
|
|
60
72
|
googleAuth,
|
|
61
|
-
|
|
73
|
+
// Extra args are forwarded so callers can pass structured context —
|
|
74
|
+
// `newrelic.noticeError(error, customAttributes)` takes it natively.
|
|
75
|
+
onHookError: (e, ...args) => {
|
|
62
76
|
if (process.env.NODE_ENV === 'development') {
|
|
63
|
-
log('ERROR IN IDENTITY-X HOOK', e);
|
|
77
|
+
log('ERROR IN IDENTITY-X HOOK', e, ...args);
|
|
64
78
|
if (e.networkError) log('Network Error', get(e, 'networkError.result.errors.0'));
|
|
65
79
|
}
|
|
66
|
-
if (isFn(onHookError)) onHookError(e);
|
|
80
|
+
if (isFn(onHookError)) onHookError(e, ...args);
|
|
67
81
|
},
|
|
82
|
+
addRequestAttributes: isFn(addRequestAttributes)
|
|
83
|
+
? addRequestAttributes
|
|
84
|
+
: defaultAddRequestAttributes,
|
|
68
85
|
...rest,
|
|
69
86
|
};
|
|
70
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindful-web/marko-web-identity-x",
|
|
3
|
-
"version": "1.79.
|
|
3
|
+
"version": "1.79.7",
|
|
4
4
|
"description": "Marko Wrapper for IdentityX",
|
|
5
5
|
"repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-identity-x",
|
|
6
6
|
"author": "Josh Worden <josh@parameter1.com>",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "cd09a3d725bbcb04b8a1ffae70164981bbe1305e"
|
|
42
42
|
}
|
package/routes/google.js
CHANGED
|
@@ -14,6 +14,16 @@ const UPDATE_OWN_APP_USER = gql`
|
|
|
14
14
|
|
|
15
15
|
const isEmpty = (v) => v == null || v === '';
|
|
16
16
|
|
|
17
|
+
// Cache one OAuth2Client per client id. `verifyIdToken` fetches (and caches on
|
|
18
|
+
// the *instance*) Google's public signing certs, so constructing a client per
|
|
19
|
+
// request re-fetched them on every sign-in — turning a transient network blip
|
|
20
|
+
// or rate-limit at Google into a failed sign-in.
|
|
21
|
+
const oauthClients = new Map();
|
|
22
|
+
const getOAuthClient = (clientId) => {
|
|
23
|
+
if (!oauthClients.has(clientId)) oauthClients.set(clientId, new OAuth2Client(clientId));
|
|
24
|
+
return oauthClients.get(clientId);
|
|
25
|
+
};
|
|
26
|
+
|
|
17
27
|
const LOGIN_SOURCE = 'google-one-tap';
|
|
18
28
|
|
|
19
29
|
// Namespace under which the Google account id (the JWT `sub`) is stored as an
|
|
@@ -50,7 +60,21 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
50
60
|
const { config } = identityX;
|
|
51
61
|
const onError = config.get('onHookError') || defaultOnError;
|
|
52
62
|
|
|
63
|
+
// Tag the transaction as the route progresses. Whatever step is recorded when
|
|
64
|
+
// the request dies is the step that failed, so an error arrives already
|
|
65
|
+
// localized instead of being a bare GraphQL message to reverse-engineer.
|
|
66
|
+
const addAttrs = config.get('addRequestAttributes') || (() => {});
|
|
67
|
+
const setStep = (step) => addAttrs({ idxGoogleStep: step });
|
|
68
|
+
|
|
53
69
|
const { clientId, missingFieldsBehavior = 'profile-gate' } = config.getAsObject('googleAuth');
|
|
70
|
+
// `surface` is the GIS `select_by` bucket ('button' | 'prompt'), sent by the
|
|
71
|
+
// client. Both surfaces share a route, a loginSource and an impersonation
|
|
72
|
+
// method, so without it there is no way to tell from the server whether the
|
|
73
|
+
// rendered button works independently of the One-Tap prompt.
|
|
74
|
+
addAttrs({
|
|
75
|
+
idxGoogleSurface: body.surface === 'button' ? 'button' : 'prompt',
|
|
76
|
+
idxGoogleMissingFieldsBehavior: missingFieldsBehavior,
|
|
77
|
+
});
|
|
54
78
|
|
|
55
79
|
if (!clientId) {
|
|
56
80
|
res.status(400).json({ ok: false, error: 'Google Sign-In is not configured for this site.' });
|
|
@@ -62,7 +86,8 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
// Verify the Google JWT and extract user info.
|
|
65
|
-
|
|
89
|
+
setStep('verify-credential');
|
|
90
|
+
const oauthClient = getOAuthClient(clientId);
|
|
66
91
|
const ticket = await oauthClient.verifyIdToken({ idToken: credential, audience: clientId });
|
|
67
92
|
const googlePayload = ticket.getPayload();
|
|
68
93
|
const {
|
|
@@ -75,11 +100,14 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
75
100
|
if (!email) throw new Error('Google credential did not include an email address.');
|
|
76
101
|
|
|
77
102
|
// Find or create the identity-x user.
|
|
103
|
+
setStep('load-app-user');
|
|
78
104
|
let appUser = await identityX.loadAppUserByEmail(email);
|
|
79
105
|
const createdNewUser = !appUser;
|
|
80
106
|
if (!appUser) {
|
|
107
|
+
setStep('create-app-user');
|
|
81
108
|
appUser = await identityX.createAppUser({ email });
|
|
82
109
|
}
|
|
110
|
+
addAttrs({ idxUserId: appUser.id, idxCreatedNewUser: createdNewUser });
|
|
83
111
|
|
|
84
112
|
// Persist the Google account id (the JWT `sub`) as an idx external id so the
|
|
85
113
|
// user is linked to its Google identity. Email stays the primary key; this is
|
|
@@ -92,6 +120,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
92
120
|
&& extId.identifier.value === googleSub
|
|
93
121
|
));
|
|
94
122
|
if (!alreadyLinked) {
|
|
123
|
+
setStep('link-google-sub');
|
|
95
124
|
try {
|
|
96
125
|
// If this sub is already attached to a different user (e.g. the Google
|
|
97
126
|
// account's email changed and a new user was created under it), report
|
|
@@ -104,7 +133,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
104
133
|
if (existing && existing.id !== appUser.id) {
|
|
105
134
|
onError(new Error(
|
|
106
135
|
`Google sub already linked to idx user ${existing.id}, not ${appUser.id}`,
|
|
107
|
-
));
|
|
136
|
+
), { idxGoogleStep: 'link-google-sub', idxUserId: appUser.id, idxConflictUserId: existing.id });
|
|
108
137
|
} else {
|
|
109
138
|
await identityX.addExternalUserId({
|
|
110
139
|
userId: appUser.id,
|
|
@@ -114,11 +143,14 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
114
143
|
}
|
|
115
144
|
} catch (e) {
|
|
116
145
|
// Supplementary linkage — never block authentication on a failure here.
|
|
117
|
-
|
|
146
|
+
// Reported with context so these stay distinguishable from fatal errors
|
|
147
|
+
// on the same route (they arrive on a 200).
|
|
148
|
+
onError(e, { idxGoogleStep: 'link-google-sub', idxUserId: appUser.id });
|
|
118
149
|
}
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
152
|
|
|
153
|
+
setStep('impersonate-app-user');
|
|
122
154
|
// Impersonate the user with verify: true — marks them as verified and returns
|
|
123
155
|
// a session token, bypassing the normal email-link flow.
|
|
124
156
|
const { data: impersonateData } = await identityX.client.mutate({
|
|
@@ -140,9 +172,13 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
140
172
|
|
|
141
173
|
// Re-init the Apollo client with the user's own session token so that
|
|
142
174
|
// updateOwnAppUser is authenticated as this user (not as the org admin).
|
|
143
|
-
|
|
175
|
+
// `setToken` takes the token *string* — passing the `{ id, value }` object
|
|
176
|
+
// produced an `AppUser [object Object]` authorization header, which failed
|
|
177
|
+
// every subsequent user-authenticated call (see updateOwnAppUser below).
|
|
178
|
+
identityX.setToken(authToken.value);
|
|
144
179
|
|
|
145
180
|
// Set name fields from Google as verified data if not already present.
|
|
181
|
+
setStep('update-own-app-user');
|
|
146
182
|
const needsNameUpdate = !appUser.givenName || !appUser.familyName;
|
|
147
183
|
if (needsNameUpdate && (givenName || familyName)) {
|
|
148
184
|
const nameInput = {};
|
|
@@ -155,6 +191,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
155
191
|
}
|
|
156
192
|
|
|
157
193
|
// Load the refreshed user so hooks and field checks have up-to-date data.
|
|
194
|
+
setStep('load-refreshed-user');
|
|
158
195
|
const user = await identityX.findUserById(appUser.id);
|
|
159
196
|
const entity = await identityX.generateEntityId({ userId: appUser.id });
|
|
160
197
|
|
|
@@ -169,12 +206,14 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
169
206
|
];
|
|
170
207
|
requiresUserInput = requiredFields.some((key) => isEmpty(user[key]));
|
|
171
208
|
}
|
|
209
|
+
addAttrs({ idxRequiresUserInput: requiresUserInput });
|
|
172
210
|
|
|
173
211
|
// Fire onLoginLinkSent before onAuthenticationSuccess so that hooks wired to
|
|
174
212
|
// it (e.g. Omeda rapid-identify and subscription sync) run for Google users
|
|
175
213
|
// the same way they do for email-link users. This also populates
|
|
176
214
|
// user.externalIds with the Omeda encryptedCustomerId so the subsequent
|
|
177
215
|
// onAuthenticationSuccess hook can set oly_enc_id in the response.
|
|
216
|
+
setStep('hooks-login-link-sent');
|
|
178
217
|
await callHooksFor(identityX, 'onLoginLinkSent', {
|
|
179
218
|
req,
|
|
180
219
|
source: LOGIN_SOURCE,
|
|
@@ -185,6 +224,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
185
224
|
// the onLoginLinkSent hook (Omeda rapid-identify).
|
|
186
225
|
const userAfterLinkSent = await identityX.findUserById(appUser.id);
|
|
187
226
|
|
|
227
|
+
setStep('hooks-authentication-success');
|
|
188
228
|
await callHooksFor(identityX, 'onAuthenticationSuccess', {
|
|
189
229
|
req,
|
|
190
230
|
res,
|
|
@@ -194,6 +234,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
194
234
|
additionalEventData: { createdNewUser, requiresUserInput },
|
|
195
235
|
});
|
|
196
236
|
|
|
237
|
+
setStep('complete');
|
|
197
238
|
res.json({
|
|
198
239
|
ok: true,
|
|
199
240
|
requiresUserInput,
|
package/service.js
CHANGED
|
@@ -355,8 +355,9 @@ class IdentityX {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
|
+
* Sets the active app-user token and re-inits the Apollo client with it.
|
|
358
359
|
*
|
|
359
|
-
* @param {*}
|
|
360
|
+
* @param {String} token The token *value* (not the `{ id, value }` object).
|
|
360
361
|
*/
|
|
361
362
|
setToken(token) {
|
|
362
363
|
this.token = token;
|
|
@@ -379,7 +380,11 @@ class IdentityX {
|
|
|
379
380
|
});
|
|
380
381
|
const { token } = data.impersonateAppUser;
|
|
381
382
|
tokenCookie.setTo(this.res, token.value);
|
|
382
|
-
|
|
383
|
+
// `setToken` expects the token string (as the constructor supplies from the
|
|
384
|
+
// cookie) — passing the `{ id, value }` object here produced an
|
|
385
|
+
// `AppUser [object Object]` authorization header on the re-inited client,
|
|
386
|
+
// and left `this.token` as an object for `logoutAppUser`.
|
|
387
|
+
this.setToken(token.value);
|
|
383
388
|
}
|
|
384
389
|
|
|
385
390
|
async logoutAppUser() {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default sink for `config.addRequestAttributes` — tags the current New Relic
|
|
3
|
+
* transaction, with no configuration and no New Relic dependency.
|
|
4
|
+
*
|
|
5
|
+
* The agent is used **only when the application has already loaded it**. Every
|
|
6
|
+
* marko-web site requires its `newrelic.js` at boot, so by the time a request
|
|
7
|
+
* is served the module is in `require.cache` and `require('newrelic')` returns
|
|
8
|
+
* that same initialized singleton. Requiring it unconditionally would instead
|
|
9
|
+
* *start* an agent on sites that don't use New Relic, which is why this checks
|
|
10
|
+
* the cache rather than just try/catching the require.
|
|
11
|
+
*
|
|
12
|
+
* Net effect: attributes appear fleet-wide on a dependency bump alone, and the
|
|
13
|
+
* whole thing is inert on any site without New Relic. A site can still pass its
|
|
14
|
+
* own `addRequestAttributes` to override.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
let resolvedPath;
|
|
18
|
+
let lookupFailed = false;
|
|
19
|
+
let agent = null;
|
|
20
|
+
|
|
21
|
+
const getAgent = () => {
|
|
22
|
+
if (agent) return agent;
|
|
23
|
+
if (lookupFailed) return null;
|
|
24
|
+
if (!resolvedPath) {
|
|
25
|
+
try {
|
|
26
|
+
resolvedPath = require.resolve('newrelic');
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Not installed — nothing to report to.
|
|
29
|
+
lookupFailed = true;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// `newrelic` is intentionally NOT a dependency of this package: declaring it
|
|
34
|
+
// would install it on every monorail site, and requiring an uninitialized
|
|
35
|
+
// agent starts one. We only ever pick up an instance the host app already
|
|
36
|
+
// created, which is why the cache check above guards this require.
|
|
37
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
38
|
+
if (require.cache[resolvedPath]) agent = require('newrelic');
|
|
39
|
+
return agent;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports = (attributes) => {
|
|
43
|
+
const nr = getAgent();
|
|
44
|
+
if (!nr || typeof nr.addCustomAttributes !== 'function') return;
|
|
45
|
+
try {
|
|
46
|
+
nr.addCustomAttributes(attributes);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Telemetry must never break a request.
|
|
49
|
+
}
|
|
50
|
+
};
|