Files
IdP/apps/frontend/public/sso-widget.js
2026-06-29 12:17:25 +03:00

275 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function lendrySsoWidget(global) {
'use strict';
var MESSAGE_TYPE = 'lendry-sso-onetap';
var DEFAULT_SCOPE = 'openid profile email';
var widgetRootId = 'lendry-sso-onetap-root';
function trimSlash(value) {
return String(value || '').replace(/\/+$/, '');
}
function resolveScriptElement() {
var current = global.document.currentScript;
if (current) return current;
var scripts = global.document.getElementsByTagName('script');
for (var i = scripts.length - 1; i >= 0; i -= 1) {
var src = scripts[i].getAttribute('src') || '';
if (src.indexOf('sso-widget.js') !== -1) {
return scripts[i];
}
}
return null;
}
function resolveIdpApiBase(script) {
var override = script && script.getAttribute('data-idp-url');
if (override) return trimSlash(override);
try {
var origin = new URL(script.src).origin;
return trimSlash(origin + '/idp-api');
} catch (_error) {
return '';
}
}
function resolveFrontendBase(script, idpApiBase) {
var override = script && script.getAttribute('data-idp-frontend-url');
if (override) return trimSlash(override);
try {
return new URL(script.src).origin;
} catch (_error) {
if (idpApiBase.indexOf('/idp-api') !== -1) {
return idpApiBase.replace(/\/idp-api$/, '');
}
return idpApiBase;
}
}
function resolveProviderName(script) {
return (script && script.getAttribute('data-provider-name')) || 'MVK ID';
}
function dispatchSuccess(payload, script) {
var callbackName = script && script.getAttribute('data-on-success');
if (callbackName && typeof global[callbackName] === 'function') {
global[callbackName](payload);
}
try {
global.dispatchEvent(new CustomEvent('lendry-sso-onetap-success', { detail: payload }));
} catch (_error) {
// IE fallback not required
}
}
function supportsFedCM() {
return 'IdentityCredential' in global && global.navigator && typeof global.navigator.credentials !== 'undefined';
}
function loginWithFedCM(idpApiBase, clientId) {
if (!supportsFedCM()) {
return Promise.resolve(null);
}
return global.navigator.credentials
.get({
identity: {
providers: [
{
configURL: idpApiBase + '/fedcm/config.json',
clientId: clientId
}
]
},
mediation: 'optional'
})
.then(function (credential) {
if (!credential || typeof credential !== 'object') return null;
var token = credential.token;
if (!token) return null;
return {
token: token,
method: 'fedcm'
};
})
.catch(function (error) {
if (global.console && typeof global.console.warn === 'function') {
global.console.warn('[MVK ID] FedCM недоступен:', error);
}
return null;
});
}
function injectWidgetStyles() {
if (global.document.getElementById('lendry-sso-onetap-style')) return;
var style = global.document.createElement('style');
style.id = 'lendry-sso-onetap-style';
style.textContent =
'#' +
widgetRootId +
'{position:fixed;top:16px;right:16px;z-index:2147483000;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}' +
'.lendry-sso-onetap-btn{display:flex;align-items:center;gap:10px;padding:10px 16px;border-radius:999px;border:1px solid #e4e8ef;background:#fff;color:#1f2430;font-size:14px;font-weight:600;line-height:1;cursor:pointer;box-shadow:0 8px 24px rgba(31,36,48,.12);transition:transform .18s ease,box-shadow .18s ease}' +
'.lendry-sso-onetap-btn:hover{transform:translateY(-1px);box-shadow:0 12px 28px rgba(31,36,48,.16)}' +
'.lendry-sso-onetap-btn:active{transform:translateY(0)}' +
'.lendry-sso-onetap-badge{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:0 8px;border-radius:999px;background:#111;color:#fff;font-size:12px;font-weight:700}';
global.document.head.appendChild(style);
}
function createWidget(providerName, onClick) {
injectWidgetStyles();
if (global.document.getElementById(widgetRootId)) return;
var root = global.document.createElement('div');
root.id = widgetRootId;
var button = global.document.createElement('button');
button.type = 'button';
button.className = 'lendry-sso-onetap-btn';
button.setAttribute('aria-label', 'Войти через ' + providerName);
var badge = global.document.createElement('span');
badge.className = 'lendry-sso-onetap-badge';
badge.textContent = 'ID';
var label = global.document.createElement('span');
label.textContent = 'Войти через ' + providerName;
button.appendChild(badge);
button.appendChild(label);
button.addEventListener('click', onClick);
root.appendChild(button);
global.document.body.appendChild(root);
}
function buildPopupUrl(frontendBase, clientId, origin, redirectUri, scope) {
var params = new URLSearchParams();
params.set('client_id', clientId);
params.set('redirect_uri', redirectUri);
params.set('response_type', 'code');
params.set('scope', scope || DEFAULT_SCOPE);
params.set('display', 'popup');
params.set('popup_origin', origin);
params.set('state', 'onetap_' + Math.random().toString(36).slice(2));
return frontendBase + '/auth/oauth/authorize?' + params.toString();
}
function openPopup(url, expectedOrigin, onToken) {
var width = 480;
var height = 640;
var left = Math.max(0, Math.round((global.screen.width - width) / 2));
var top = Math.max(0, Math.round((global.screen.height - height) / 2));
var features =
'popup=yes,width=' +
width +
',height=' +
height +
',left=' +
left +
',top=' +
top +
',noopener,noreferrer';
var popup = global.open(url, 'lendry_sso_onetap', features);
if (!popup) {
if (global.console && typeof global.console.error === 'function') {
global.console.error('[MVK ID] Не удалось открыть popup. Разрешите всплывающие окна.');
}
return function cleanup() {};
}
function handleMessage(event) {
if (!event || !event.data || event.data.type !== MESSAGE_TYPE) return;
if (expectedOrigin && event.origin !== expectedOrigin && event.origin !== trimSlash(expectedOrigin)) return;
cleanup();
onToken(event.data);
}
function cleanup() {
global.removeEventListener('message', handleMessage);
if (popup && !popup.closed) {
try {
popup.close();
} catch (_error) {
// ignore
}
}
}
global.addEventListener('message', handleMessage);
return cleanup;
}
function initOneTap(options) {
var script = resolveScriptElement();
var clientId = (options && options.clientId) || (script && script.getAttribute('data-client-id'));
if (!clientId) {
if (global.console && typeof global.console.error === 'function') {
global.console.error('[MVK ID] Укажите data-client-id в теге script или clientId в LendryIdOneTap.init().');
}
return;
}
var idpApiBase = trimSlash((options && options.idpUrl) || resolveIdpApiBase(script));
var frontendBase = trimSlash((options && options.frontendUrl) || resolveFrontendBase(script, idpApiBase));
var providerName = (options && options.providerName) || resolveProviderName(script);
var scope = (options && options.scope) || (script && script.getAttribute('data-scope')) || DEFAULT_SCOPE;
var redirectUri =
(options && options.redirectUri) ||
(script && script.getAttribute('data-redirect-uri')) ||
global.location.origin + '/auth/callback';
loginWithFedCM(idpApiBase, clientId).then(function (result) {
if (result && result.token) {
dispatchSuccess(
{
token: result.token,
method: 'fedcm'
},
script
);
return;
}
if (supportsFedCM()) {
return;
}
createWidget(providerName, function () {
var popupUrl = buildPopupUrl(frontendBase, clientId, global.location.origin, redirectUri, scope);
openPopup(popupUrl, frontendBase, function (payload) {
dispatchSuccess(
{
token: payload.token,
accessToken: payload.accessToken,
idToken: payload.idToken,
refreshToken: payload.refreshToken,
code: payload.code,
method: 'popup'
},
script
);
});
});
});
}
global.LendryIdOneTap = {
init: initOneTap,
loginWithFedCM: loginWithFedCM,
MESSAGE_TYPE: MESSAGE_TYPE
};
var scriptEl = resolveScriptElement();
if (scriptEl && scriptEl.getAttribute('data-auto-init') !== 'false') {
if (global.document.readyState === 'loading') {
global.document.addEventListener('DOMContentLoaded', function () {
initOneTap();
});
} else {
initOneTap();
}
}
})(window);