@mindful-web/marko-web-identity-x 1.76.7 → 1.78.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.
- package/CLAUDE.md +139 -0
- package/README.md +48 -0
- package/browser/access.vue +13 -2
- package/browser/comments/create.vue +8 -0
- package/browser/comments/stream.vue +20 -2
- package/browser/download.vue +13 -2
- 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 +4 -0
- package/components/form-access.marko.js +7 -2
- package/components/form-authenticate.marko +2 -0
- package/components/form-authenticate.marko.js +14 -2
- package/components/form-change-email.marko.js +2 -2
- package/components/form-login.marko +2 -0
- package/components/form-login.marko.js +14 -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 +73 -0
- package/components/google-init.marko.js +95 -0
- package/components/google-sign-in-button.marko +24 -0
- package/components/google-sign-in-button.marko.js +44 -0
- package/components/identify.marko.js +2 -2
- package/components/marko.json +11 -0
- package/components/non-auth-identify.marko.js +2 -2
- package/components/subscribe.marko.js +2 -2
- package/config.js +17 -0
- package/package.json +3 -2
- package/routes/google.js +206 -0
- package/routes/index.js +2 -0
- package/routes/logout.js +5 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
$ const { req } = out.global;
|
|
2
|
+
$ const config = req.identityX && req.identityX.config;
|
|
3
|
+
$ const { clientId, autoPrompt } = config ? config.getAsObject('googleAuth') : {};
|
|
4
|
+
$ const callbackName = 'handleGoogleOneTapCredential';
|
|
5
|
+
$ const initScript = `
|
|
6
|
+
(function() {
|
|
7
|
+
window.${callbackName} = function(response) {
|
|
8
|
+
var redirectTo = window.__googleSignInRedirectTo || '';
|
|
9
|
+
var errorEl = document.getElementById('google-sign-in-error');
|
|
10
|
+
if (errorEl) errorEl.style.display = 'none';
|
|
11
|
+
|
|
12
|
+
// GIS 'select_by' distinguishes the surface: a 'btn*' value means the
|
|
13
|
+
// rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
|
|
14
|
+
// prompt. Used to label the p1events authentication event accordingly.
|
|
15
|
+
var eventLabel = String(response.select_by || '').indexOf('btn') === 0
|
|
16
|
+
? 'Google Sign In'
|
|
17
|
+
: 'Google One Tap';
|
|
18
|
+
|
|
19
|
+
fetch('/__idx/google', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({ credential: response.credential, redirectTo: redirectTo }),
|
|
23
|
+
})
|
|
24
|
+
.then(function(res) { return res.json(); })
|
|
25
|
+
.then(function(data) {
|
|
26
|
+
if (!data.ok) {
|
|
27
|
+
if (errorEl) {
|
|
28
|
+
errorEl.textContent = data.error || 'Google sign-in failed. Please try again.';
|
|
29
|
+
errorEl.style.display = 'block';
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (window.p1events) {
|
|
34
|
+
// De-anonymize the session to the app user, then record the
|
|
35
|
+
// authentication — mirrors the identity-x conversion events.
|
|
36
|
+
window.p1events('setIdentity', data.entity);
|
|
37
|
+
window.p1events('track', {
|
|
38
|
+
category: 'Identity',
|
|
39
|
+
action: 'Authenticate',
|
|
40
|
+
label: eventLabel,
|
|
41
|
+
props: { idxEntity: data.entity, actionSource: 'google-one-tap' },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (data.requiresUserInput) {
|
|
45
|
+
var destination = data.redirectTo || window.location.pathname;
|
|
46
|
+
window.location.href = '/user/profile?returnTo=' + encodeURIComponent(destination);
|
|
47
|
+
} else {
|
|
48
|
+
window.location.href = data.redirectTo || window.location.href;
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
.catch(function() {
|
|
52
|
+
if (errorEl) {
|
|
53
|
+
errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
|
|
54
|
+
errorEl.style.display = 'block';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
<if(clientId)>
|
|
62
|
+
<div
|
|
63
|
+
id="g_id_onload"
|
|
64
|
+
data-client_id=clientId
|
|
65
|
+
data-callback=callbackName
|
|
66
|
+
data-auto_prompt=(autoPrompt ? 'true' : 'false')
|
|
67
|
+
data-auto_select="false"
|
|
68
|
+
data-cancel_on_tap_outside="true"
|
|
69
|
+
data-itp_support="true"
|
|
70
|
+
/>
|
|
71
|
+
<script src="https://accounts.google.com/gsi/client" async defer />
|
|
72
|
+
<script>$!{initScript}</script>
|
|
73
|
+
</if>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Compiled using marko@4.20.2 - DO NOT EDIT
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.78.0/components/google-init.marko",
|
|
6
|
+
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
|
+
marko_attr = require("marko/dist/runtime/html/helpers/attr"),
|
|
8
|
+
marko_str = require("marko/dist/runtime/helpers/to-string");
|
|
9
|
+
|
|
10
|
+
function render(input, out, __component, component, state) {
|
|
11
|
+
var data = input;
|
|
12
|
+
|
|
13
|
+
const { req } = out.global;
|
|
14
|
+
|
|
15
|
+
const config = req.identityX && req.identityX.config;
|
|
16
|
+
|
|
17
|
+
const { clientId, autoPrompt } = config ? config.getAsObject('googleAuth') : {};
|
|
18
|
+
|
|
19
|
+
const callbackName = 'handleGoogleOneTapCredential';
|
|
20
|
+
|
|
21
|
+
const initScript = `
|
|
22
|
+
(function() {
|
|
23
|
+
window.${callbackName} = function(response) {
|
|
24
|
+
var redirectTo = window.__googleSignInRedirectTo || '';
|
|
25
|
+
var errorEl = document.getElementById('google-sign-in-error');
|
|
26
|
+
if (errorEl) errorEl.style.display = 'none';
|
|
27
|
+
|
|
28
|
+
// GIS 'select_by' distinguishes the surface: a 'btn*' value means the
|
|
29
|
+
// rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
|
|
30
|
+
// prompt. Used to label the p1events authentication event accordingly.
|
|
31
|
+
var eventLabel = String(response.select_by || '').indexOf('btn') === 0
|
|
32
|
+
? 'Google Sign In'
|
|
33
|
+
: 'Google One Tap';
|
|
34
|
+
|
|
35
|
+
fetch('/__idx/google', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ credential: response.credential, redirectTo: redirectTo }),
|
|
39
|
+
})
|
|
40
|
+
.then(function(res) { return res.json(); })
|
|
41
|
+
.then(function(data) {
|
|
42
|
+
if (!data.ok) {
|
|
43
|
+
if (errorEl) {
|
|
44
|
+
errorEl.textContent = data.error || 'Google sign-in failed. Please try again.';
|
|
45
|
+
errorEl.style.display = 'block';
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (window.p1events) {
|
|
50
|
+
// De-anonymize the session to the app user, then record the
|
|
51
|
+
// authentication — mirrors the identity-x conversion events.
|
|
52
|
+
window.p1events('setIdentity', data.entity);
|
|
53
|
+
window.p1events('track', {
|
|
54
|
+
category: 'Identity',
|
|
55
|
+
action: 'Authenticate',
|
|
56
|
+
label: eventLabel,
|
|
57
|
+
props: { idxEntity: data.entity, actionSource: 'google-one-tap' },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (data.requiresUserInput) {
|
|
61
|
+
var destination = data.redirectTo || window.location.pathname;
|
|
62
|
+
window.location.href = '/user/profile?returnTo=' + encodeURIComponent(destination);
|
|
63
|
+
} else {
|
|
64
|
+
window.location.href = data.redirectTo || window.location.href;
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch(function() {
|
|
68
|
+
if (errorEl) {
|
|
69
|
+
errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
|
|
70
|
+
errorEl.style.display = 'block';
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
})();
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
if (clientId) {
|
|
78
|
+
out.w("<div id=\"g_id_onload\"" +
|
|
79
|
+
marko_attr("data-client_id", clientId) +
|
|
80
|
+
marko_attr("data-callback", callbackName) +
|
|
81
|
+
marko_attr("data-auto_prompt", autoPrompt ? "true" : "false") +
|
|
82
|
+
" data-auto_select=\"false\" data-cancel_on_tap_outside=\"true\" data-itp_support=\"true\"></div><script src=\"https://accounts.google.com/gsi/client\" async defer></script><script>" +
|
|
83
|
+
marko_str(initScript) +
|
|
84
|
+
"</script>");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
marko_template._ = marko_renderer(render, {
|
|
89
|
+
d_: true,
|
|
90
|
+
e_: marko_componentType
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
marko_template.meta = {
|
|
94
|
+
id: "/@mindful-web/marko-web-identity-x$1.78.0/components/google-init.marko"
|
|
95
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
$ const { req } = out.global;
|
|
2
|
+
$ const config = req.identityX && req.identityX.config;
|
|
3
|
+
$ const googleAuth = config ? config.getAsObject('googleAuth') : {};
|
|
4
|
+
$ const clientId = input.clientId || googleAuth.clientId || '';
|
|
5
|
+
$ const redirectTo = input.redirectTo || '';
|
|
6
|
+
$ const label = input.label || 'Or sign in with Google';
|
|
7
|
+
$ const signInButtonEnabled = googleAuth.signInButtonEnabled !== false;
|
|
8
|
+
|
|
9
|
+
<if(clientId && signInButtonEnabled)>
|
|
10
|
+
<div class="google-sign-in">
|
|
11
|
+
<p class="google-sign-in__divider">${label}</p>
|
|
12
|
+
<div
|
|
13
|
+
class="g_id_signin"
|
|
14
|
+
data-type="standard"
|
|
15
|
+
data-theme="outline"
|
|
16
|
+
data-text="signin_with"
|
|
17
|
+
data-shape="rectangular"
|
|
18
|
+
data-logo_alignment="left"
|
|
19
|
+
data-width="300"
|
|
20
|
+
/>
|
|
21
|
+
<p id="google-sign-in-error" class="google-sign-in__error" style="display:none;" role="alert" />
|
|
22
|
+
<script>window.__googleSignInRedirectTo = $!{JSON.stringify(redirectTo)};</script>
|
|
23
|
+
</div>
|
|
24
|
+
</if>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Compiled using marko@4.20.2 - DO NOT EDIT
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.78.0/components/google-sign-in-button.marko",
|
|
6
|
+
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
|
+
helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
|
|
8
|
+
marko_escapeXml = helpers_escape_xml.x,
|
|
9
|
+
marko_escapeScript = require("marko/dist/runtime/html/helpers/escape-script-placeholder");
|
|
10
|
+
|
|
11
|
+
function render(input, out, __component, component, state) {
|
|
12
|
+
var data = input;
|
|
13
|
+
|
|
14
|
+
const { req } = out.global;
|
|
15
|
+
|
|
16
|
+
const config = req.identityX && req.identityX.config;
|
|
17
|
+
|
|
18
|
+
const googleAuth = config ? config.getAsObject('googleAuth') : {};
|
|
19
|
+
|
|
20
|
+
const clientId = input.clientId || googleAuth.clientId || '';
|
|
21
|
+
|
|
22
|
+
const redirectTo = input.redirectTo || '';
|
|
23
|
+
|
|
24
|
+
const label = input.label || 'Or sign in with Google';
|
|
25
|
+
|
|
26
|
+
const signInButtonEnabled = googleAuth.signInButtonEnabled !== false;
|
|
27
|
+
|
|
28
|
+
if (clientId && signInButtonEnabled) {
|
|
29
|
+
out.w("<div class=\"google-sign-in\"><p class=\"google-sign-in__divider\">" +
|
|
30
|
+
marko_escapeXml(label) +
|
|
31
|
+
"</p><div class=\"g_id_signin\" data-type=\"standard\" data-theme=\"outline\" data-text=\"signin_with\" data-shape=\"rectangular\" data-logo_alignment=\"left\" data-width=\"300\"></div><p id=\"google-sign-in-error\" class=\"google-sign-in__error\" style=\"display:none;\" role=\"alert\"></p><script>" +
|
|
32
|
+
marko_escapeScript(("window.__googleSignInRedirectTo = " + JSON.stringify(redirectTo)) + ";") +
|
|
33
|
+
"</script></div>");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
marko_template._ = marko_renderer(render, {
|
|
38
|
+
d_: true,
|
|
39
|
+
e_: marko_componentType
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
marko_template.meta = {
|
|
43
|
+
id: "/@mindful-web/marko-web-identity-x$1.78.0/components/google-sign-in-button.marko"
|
|
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.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.78.0/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.
|
|
53
|
+
id: "/@mindful-web/marko-web-identity-x$1.78.0/components/identify.marko",
|
|
54
54
|
component: "./identify.marko",
|
|
55
55
|
tags: [
|
|
56
56
|
"@mindful-web/marko-web-gtm/components/push.marko",
|
package/components/marko.json
CHANGED
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
"type": "string",
|
|
96
96
|
"required": true
|
|
97
97
|
},
|
|
98
|
+
"@type": "string",
|
|
99
|
+
"@content-entity": "string",
|
|
98
100
|
"@title": "string",
|
|
99
101
|
"@description": "string",
|
|
100
102
|
"@url": "string",
|
|
@@ -106,5 +108,14 @@
|
|
|
106
108
|
"<marko-web-identity-x-identify>": {
|
|
107
109
|
"template": "./identify.marko",
|
|
108
110
|
"@provider-data": "object"
|
|
111
|
+
},
|
|
112
|
+
"<marko-web-identity-x-google-init>": {
|
|
113
|
+
"template": "./google-init.marko"
|
|
114
|
+
},
|
|
115
|
+
"<marko-web-identity-x-google-sign-in-button>": {
|
|
116
|
+
"template": "./google-sign-in-button.marko",
|
|
117
|
+
"@client-id": "string",
|
|
118
|
+
"@redirect-to": "string",
|
|
119
|
+
"@label": "string"
|
|
109
120
|
}
|
|
110
121
|
}
|
|
@@ -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.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.78.0/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.
|
|
48
|
+
id: "/@mindful-web/marko-web-identity-x$1.78.0/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.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.78.0/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.
|
|
76
|
+
id: "/@mindful-web/marko-web-identity-x$1.78.0/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
|
@@ -16,6 +16,13 @@ class IdentityXConfiguration {
|
|
|
16
16
|
* @param {string[]} [options.activeCustomFieldIds] Limit displayed custom fields, if present.
|
|
17
17
|
* @param {string[]} [options.hiddenFields] The fields to hide from the profile.
|
|
18
18
|
* @param {function} [options.onHookError]
|
|
19
|
+
* @param {object} [options.googleAuth] Google Sign-In config. When `clientId` is
|
|
20
|
+
* set, the One-Tap prompt / "Sign in with Google" button render in the auth
|
|
21
|
+
* templates and `POST /__idx/google` accepts credentials. Dormant otherwise.
|
|
22
|
+
* @param {string} [options.googleAuth.clientId] GIS OAuth client id — the master gate.
|
|
23
|
+
* @param {("profile-gate"|"immediate"|"relaxed")} [options.googleAuth.missingFieldsBehavior]
|
|
24
|
+
* @param {boolean} [options.googleAuth.autoPrompt] Show the floating One-Tap overlay.
|
|
25
|
+
* @param {boolean} [options.googleAuth.signInButtonEnabled] Render the button (default true).
|
|
19
26
|
* @param {object} options.rest
|
|
20
27
|
*/
|
|
21
28
|
constructor({
|
|
@@ -31,6 +38,7 @@ class IdentityXConfiguration {
|
|
|
31
38
|
defaultCountryCode,
|
|
32
39
|
booleanQuestionsLabel,
|
|
33
40
|
gtmUserFields = {},
|
|
41
|
+
googleAuth = {},
|
|
34
42
|
onHookError,
|
|
35
43
|
...rest
|
|
36
44
|
} = {}) {
|
|
@@ -49,6 +57,7 @@ class IdentityXConfiguration {
|
|
|
49
57
|
defaultCountryCode,
|
|
50
58
|
booleanQuestionsLabel,
|
|
51
59
|
gtmUserFields,
|
|
60
|
+
googleAuth,
|
|
52
61
|
onHookError: (e) => {
|
|
53
62
|
if (process.env.NODE_ENV === 'development') {
|
|
54
63
|
log('ERROR IN IDENTITY-X HOOK', e);
|
|
@@ -141,6 +150,14 @@ class IdentityXConfiguration {
|
|
|
141
150
|
return this.getAsArray('activeCustomFieldIds');
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The Google Sign-In config block. Empty object when unconfigured, so a missing
|
|
155
|
+
* `clientId` reads as falsy and the feature stays dormant.
|
|
156
|
+
*/
|
|
157
|
+
getGoogleAuth() {
|
|
158
|
+
return this.getAsObject('googleAuth');
|
|
159
|
+
}
|
|
160
|
+
|
|
144
161
|
getAdditionalCustomFieldIds() {
|
|
145
162
|
return this.getAsArray('additionalCustomFieldIds');
|
|
146
163
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindful-web/marko-web-identity-x",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.78.0",
|
|
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>",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"cookie": "0.3.1",
|
|
25
25
|
"dayjs": "^1.11.7",
|
|
26
26
|
"express": "^4.18.2",
|
|
27
|
+
"google-auth-library": "^9.0.0",
|
|
27
28
|
"graphql-tag": "^2.12.6",
|
|
28
29
|
"i18n-iso-countries": "^4.3.1",
|
|
29
30
|
"jsonwebtoken": "^8.5.1",
|
|
@@ -37,5 +38,5 @@
|
|
|
37
38
|
"publishConfig": {
|
|
38
39
|
"access": "public"
|
|
39
40
|
},
|
|
40
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "0cedd578e969f7a2581c1dbc142351dd02b0b705"
|
|
41
42
|
}
|
package/routes/google.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const gql = require('graphql-tag');
|
|
2
|
+
const { OAuth2Client } = require('google-auth-library');
|
|
3
|
+
const { asyncRoute } = require('@mindful-web/utils');
|
|
4
|
+
const IMPERSONATE_APP_USER = require('../api/mutations/impersonate-app-user');
|
|
5
|
+
const tokenCookie = require('../utils/token-cookie');
|
|
6
|
+
const contextCookie = require('../utils/context-cookie');
|
|
7
|
+
const callHooksFor = require('../utils/call-hooks-for');
|
|
8
|
+
|
|
9
|
+
const UPDATE_OWN_APP_USER = gql`
|
|
10
|
+
mutation GoogleSignInUpdateUser($input: UpdateOwnAppUserMutationInput!) {
|
|
11
|
+
updateOwnAppUser(input: $input) { id givenName familyName }
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const isEmpty = (v) => v == null || v === '';
|
|
16
|
+
|
|
17
|
+
const LOGIN_SOURCE = 'google-one-tap';
|
|
18
|
+
|
|
19
|
+
// Namespace under which the Google account id (the JWT `sub`) is stored as an
|
|
20
|
+
// idx external id, mirroring the Omeda { provider, tenant, type } convention.
|
|
21
|
+
// These values must remain stable — changing them orphans existing links.
|
|
22
|
+
const GOOGLE_NAMESPACE = { provider: 'google', tenant: 'oauth', type: 'account' };
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
const defaultOnError = (e) => console.error('[identity-x google-auth]', e);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `POST /__idx/google` — the Google Sign-In route. Handles both the One-Tap
|
|
29
|
+
* prompt and the "Sign in with Google" button; both surfaces post here.
|
|
30
|
+
*
|
|
31
|
+
* Verifies the Google credential, finds-or-creates the IdentityX app user, links
|
|
32
|
+
* the Google account id (`sub`) as an external id, impersonates the user to mint
|
|
33
|
+
* a session token, and fires the standard `onLoginLinkSent` /
|
|
34
|
+
* `onAuthenticationSuccess` hooks so downstream integrations (e.g. Omeda
|
|
35
|
+
* rapid-identify + subscription sync) run exactly as they do for email-link
|
|
36
|
+
* users.
|
|
37
|
+
*
|
|
38
|
+
* Configuration lives on the IdentityX config under the `googleAuth` key
|
|
39
|
+
* (`{ clientId, missingFieldsBehavior, autoPrompt, signInButtonEnabled }`). The
|
|
40
|
+
* route is dormant until `clientId` is set: no client id ⇒ 400.
|
|
41
|
+
*
|
|
42
|
+
* Non-fatal errors from the supplementary `sub`-linkage are reported through the
|
|
43
|
+
* IdentityX config's `onHookError` (the same reporter sites already wire to
|
|
44
|
+
* newrelic for hook failures), falling back to `console.error`.
|
|
45
|
+
*/
|
|
46
|
+
module.exports = asyncRoute(async (req, res) => {
|
|
47
|
+
/** @type {import('../service')} */
|
|
48
|
+
const { identityX, body } = req;
|
|
49
|
+
const { credential, redirectTo } = body;
|
|
50
|
+
const { config } = identityX;
|
|
51
|
+
const onError = config.get('onHookError') || defaultOnError;
|
|
52
|
+
|
|
53
|
+
const { clientId, missingFieldsBehavior = 'profile-gate' } = config.getAsObject('googleAuth');
|
|
54
|
+
|
|
55
|
+
if (!clientId) {
|
|
56
|
+
res.status(400).json({ ok: false, error: 'Google Sign-In is not configured for this site.' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!credential) {
|
|
60
|
+
res.status(400).json({ ok: false, error: 'No Google credential was provided.' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Verify the Google JWT and extract user info.
|
|
65
|
+
const oauthClient = new OAuth2Client(clientId);
|
|
66
|
+
const ticket = await oauthClient.verifyIdToken({ idToken: credential, audience: clientId });
|
|
67
|
+
const googlePayload = ticket.getPayload();
|
|
68
|
+
const {
|
|
69
|
+
email,
|
|
70
|
+
given_name: givenName,
|
|
71
|
+
family_name: familyName,
|
|
72
|
+
sub: googleSub,
|
|
73
|
+
} = googlePayload;
|
|
74
|
+
|
|
75
|
+
if (!email) throw new Error('Google credential did not include an email address.');
|
|
76
|
+
|
|
77
|
+
// Find or create the identity-x user.
|
|
78
|
+
let appUser = await identityX.loadAppUserByEmail(email);
|
|
79
|
+
const createdNewUser = !appUser;
|
|
80
|
+
if (!appUser) {
|
|
81
|
+
appUser = await identityX.createAppUser({ email });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Persist the Google account id (the JWT `sub`) as an idx external id so the
|
|
85
|
+
// user is linked to its Google identity. Email stays the primary key; this is
|
|
86
|
+
// supplementary provenance and must never block sign-in.
|
|
87
|
+
if (googleSub) {
|
|
88
|
+
const alreadyLinked = (appUser.externalIds || []).some((extId) => (
|
|
89
|
+
extId.namespace
|
|
90
|
+
&& extId.namespace.provider === GOOGLE_NAMESPACE.provider
|
|
91
|
+
&& extId.identifier
|
|
92
|
+
&& extId.identifier.value === googleSub
|
|
93
|
+
));
|
|
94
|
+
if (!alreadyLinked) {
|
|
95
|
+
try {
|
|
96
|
+
// If this sub is already attached to a different user (e.g. the Google
|
|
97
|
+
// account's email changed and a new user was created under it), report
|
|
98
|
+
// and skip rather than fanning the sub across two users.
|
|
99
|
+
const existing = await identityX.findUserByExternalId({
|
|
100
|
+
identifier: googleSub,
|
|
101
|
+
namespace: GOOGLE_NAMESPACE,
|
|
102
|
+
type: 'sub',
|
|
103
|
+
});
|
|
104
|
+
if (existing && existing.id !== appUser.id) {
|
|
105
|
+
onError(new Error(
|
|
106
|
+
`Google sub already linked to idx user ${existing.id}, not ${appUser.id}`,
|
|
107
|
+
));
|
|
108
|
+
} else {
|
|
109
|
+
await identityX.addExternalUserId({
|
|
110
|
+
userId: appUser.id,
|
|
111
|
+
identifier: { value: googleSub, type: 'sub' },
|
|
112
|
+
namespace: GOOGLE_NAMESPACE,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// Supplementary linkage — never block authentication on a failure here.
|
|
117
|
+
onError(e);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Impersonate the user with verify: true — marks them as verified and returns
|
|
123
|
+
// a session token, bypassing the normal email-link flow.
|
|
124
|
+
const { data: impersonateData } = await identityX.client.mutate({
|
|
125
|
+
mutation: IMPERSONATE_APP_USER,
|
|
126
|
+
variables: {
|
|
127
|
+
input: {
|
|
128
|
+
id: appUser.id,
|
|
129
|
+
method: 'GOOGLE_ONE_TAP',
|
|
130
|
+
verify: true,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
context: { apiToken: identityX.getOrgUserApiToken() },
|
|
134
|
+
});
|
|
135
|
+
const { token: authToken } = impersonateData.impersonateAppUser;
|
|
136
|
+
|
|
137
|
+
tokenCookie.setTo(res, authToken.value);
|
|
138
|
+
contextCookie.setTo(res, { loginSource: LOGIN_SOURCE });
|
|
139
|
+
identityX.setIdentityCookie(appUser.id);
|
|
140
|
+
|
|
141
|
+
// Re-init the Apollo client with the user's own session token so that
|
|
142
|
+
// updateOwnAppUser is authenticated as this user (not as the org admin).
|
|
143
|
+
identityX.setToken(authToken);
|
|
144
|
+
|
|
145
|
+
// Set name fields from Google as verified data if not already present.
|
|
146
|
+
const needsNameUpdate = !appUser.givenName || !appUser.familyName;
|
|
147
|
+
if (needsNameUpdate && (givenName || familyName)) {
|
|
148
|
+
const nameInput = {};
|
|
149
|
+
if (givenName && !appUser.givenName) nameInput.givenName = givenName;
|
|
150
|
+
if (familyName && !appUser.familyName) nameInput.familyName = familyName;
|
|
151
|
+
await identityX.client.mutate({
|
|
152
|
+
mutation: UPDATE_OWN_APP_USER,
|
|
153
|
+
variables: { input: nameInput },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Load the refreshed user so hooks and field checks have up-to-date data.
|
|
158
|
+
const user = await identityX.findUserById(appUser.id);
|
|
159
|
+
const entity = await identityX.generateEntityId({ userId: appUser.id });
|
|
160
|
+
|
|
161
|
+
// For profile-gate: check whether the user is actually missing required fields
|
|
162
|
+
// rather than using forceProfileReVerification, which isn't cleared by a normal
|
|
163
|
+
// /user/profile submission and would redirect complete users on every sign-in.
|
|
164
|
+
let requiresUserInput = false;
|
|
165
|
+
if (missingFieldsBehavior === 'profile-gate') {
|
|
166
|
+
const requiredFields = [
|
|
167
|
+
...config.getRequiredServerFields(),
|
|
168
|
+
...config.getRequiredClientFields(),
|
|
169
|
+
];
|
|
170
|
+
requiresUserInput = requiredFields.some((key) => isEmpty(user[key]));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Fire onLoginLinkSent before onAuthenticationSuccess so that hooks wired to
|
|
174
|
+
// it (e.g. Omeda rapid-identify and subscription sync) run for Google users
|
|
175
|
+
// the same way they do for email-link users. This also populates
|
|
176
|
+
// user.externalIds with the Omeda encryptedCustomerId so the subsequent
|
|
177
|
+
// onAuthenticationSuccess hook can set oly_enc_id in the response.
|
|
178
|
+
await callHooksFor(identityX, 'onLoginLinkSent', {
|
|
179
|
+
req,
|
|
180
|
+
source: LOGIN_SOURCE,
|
|
181
|
+
user,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Re-fetch so onAuthenticationSuccess sees the externalIds written by
|
|
185
|
+
// the onLoginLinkSent hook (Omeda rapid-identify).
|
|
186
|
+
const userAfterLinkSent = await identityX.findUserById(appUser.id);
|
|
187
|
+
|
|
188
|
+
await callHooksFor(identityX, 'onAuthenticationSuccess', {
|
|
189
|
+
req,
|
|
190
|
+
res,
|
|
191
|
+
user: userAfterLinkSent,
|
|
192
|
+
authToken,
|
|
193
|
+
loginSource: LOGIN_SOURCE,
|
|
194
|
+
additionalEventData: { createdNewUser, requiresUserInput },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
res.json({
|
|
198
|
+
ok: true,
|
|
199
|
+
requiresUserInput,
|
|
200
|
+
redirectTo: redirectTo || null,
|
|
201
|
+
applicationId: config.getAppId(),
|
|
202
|
+
user: userAfterLinkSent,
|
|
203
|
+
loginSource: LOGIN_SOURCE,
|
|
204
|
+
entity,
|
|
205
|
+
});
|
|
206
|
+
});
|
package/routes/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const countries = require('./countries');
|
|
|
11
11
|
const createComment = require('./create-comment');
|
|
12
12
|
const download = require('./download');
|
|
13
13
|
const flagComment = require('./flag-comment');
|
|
14
|
+
const google = require('./google');
|
|
14
15
|
const login = require('./login');
|
|
15
16
|
const loginFields = require('./login-fields');
|
|
16
17
|
const logout = require('./logout');
|
|
@@ -31,6 +32,7 @@ router.post('/change-email/initiate', changeEmailInit);
|
|
|
31
32
|
router.post('/comment', createComment);
|
|
32
33
|
router.post('/comment/flag/:id', flagComment);
|
|
33
34
|
router.post('/download', download);
|
|
35
|
+
router.post('/google', google);
|
|
34
36
|
router.post('/login-fields', loginFields);
|
|
35
37
|
router.post('/login', login);
|
|
36
38
|
router.post('/logout', logout);
|
package/routes/logout.js
CHANGED
|
@@ -7,6 +7,11 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
7
7
|
/** @type {import('../middleware').IdentityXRequest} */
|
|
8
8
|
const { identityX } = req;
|
|
9
9
|
contextCookie.removeFrom(res);
|
|
10
|
+
// Clear the Google Identity Services `g_state` cookie so a subsequent Google
|
|
11
|
+
// sign-in (especially with a *different* account) isn't blocked by stale
|
|
12
|
+
// auto-select state. GIS only loads for unauthenticated users, so nothing else
|
|
13
|
+
// resets this on logout. Harmless no-op for sites not using Google Sign-In.
|
|
14
|
+
res.clearCookie('g_state', { path: '/' });
|
|
10
15
|
const token = tokenCookie.getFrom(req);
|
|
11
16
|
if (!token) {
|
|
12
17
|
await callHooksFor(identityX, 'onLogout', { req, res });
|