@marsaude/devtools-shell 0.1.0 → 0.1.2
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.
|
@@ -1138,6 +1138,11 @@ class DevtoolsApiService {
|
|
|
1138
1138
|
this.config = inject(DEVTOOLS_CONFIG);
|
|
1139
1139
|
this.utils = inject(DevtoolsUtils);
|
|
1140
1140
|
this.typeform = inject(DevtoolsTypeform);
|
|
1141
|
+
// ---- reCAPTCHA (v2 Invisible) -----------------------------------------
|
|
1142
|
+
/** Cached invisible-widget id: rendered once, re-executed per request. */
|
|
1143
|
+
this.recaptchaWidgetId = null;
|
|
1144
|
+
/** Resolver for the in-flight execute() — the widget callback fires this. */
|
|
1145
|
+
this.recaptchaResolver = null;
|
|
1141
1146
|
}
|
|
1142
1147
|
get api() {
|
|
1143
1148
|
return this.config.apiBaseUrl;
|
|
@@ -1296,14 +1301,43 @@ class DevtoolsApiService {
|
|
|
1296
1301
|
return of([]);
|
|
1297
1302
|
return forkJoin(valid.map((id) => this.deleteAuthStatement(id)));
|
|
1298
1303
|
}
|
|
1299
|
-
// ---- reCAPTCHA --------------------------------------------------------
|
|
1300
1304
|
recaptcha() {
|
|
1301
1305
|
const key = this.config.recaptchaSiteKey;
|
|
1302
|
-
|
|
1303
|
-
|
|
1306
|
+
if (!key) {
|
|
1307
|
+
console.warn('[devtools] recaptchaSiteKey not configured — request will be sent without a reCAPTCHA token');
|
|
1304
1308
|
return of('');
|
|
1305
|
-
|
|
1306
|
-
|
|
1309
|
+
}
|
|
1310
|
+
return from(ensureRecaptchaScript()
|
|
1311
|
+
.then((grecaptcha) => new Promise((resolve) => {
|
|
1312
|
+
// v2 Invisible delivers the token via callback, not a promise.
|
|
1313
|
+
// Point the shared resolver at THIS request before executing.
|
|
1314
|
+
this.recaptchaResolver = (token) => {
|
|
1315
|
+
this.recaptchaResolver = null;
|
|
1316
|
+
try {
|
|
1317
|
+
grecaptcha.reset(this.recaptchaWidgetId);
|
|
1318
|
+
}
|
|
1319
|
+
catch {
|
|
1320
|
+
/* ignore */
|
|
1321
|
+
}
|
|
1322
|
+
resolve(token || '');
|
|
1323
|
+
};
|
|
1324
|
+
if (this.recaptchaWidgetId == null) {
|
|
1325
|
+
const host = document.createElement('div');
|
|
1326
|
+
host.style.display = 'none';
|
|
1327
|
+
document.body.appendChild(host);
|
|
1328
|
+
this.recaptchaWidgetId = grecaptcha.render(host, {
|
|
1329
|
+
sitekey: key,
|
|
1330
|
+
size: 'invisible',
|
|
1331
|
+
callback: (token) => this.recaptchaResolver?.(token),
|
|
1332
|
+
'error-callback': () => this.recaptchaResolver?.(''),
|
|
1333
|
+
'expired-callback': () => this.recaptchaResolver?.(''),
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
grecaptcha.execute(this.recaptchaWidgetId);
|
|
1337
|
+
}))
|
|
1338
|
+
.catch((e) => {
|
|
1339
|
+
console.error('[devtools] failed to load reCAPTCHA', e);
|
|
1340
|
+
return '';
|
|
1307
1341
|
}));
|
|
1308
1342
|
}
|
|
1309
1343
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -1312,6 +1346,42 @@ class DevtoolsApiService {
|
|
|
1312
1346
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsApiService, decorators: [{
|
|
1313
1347
|
type: Injectable
|
|
1314
1348
|
}] });
|
|
1349
|
+
const RECAPTCHA_SCRIPT_ID = 'dc-recaptcha-v2-invisible';
|
|
1350
|
+
/**
|
|
1351
|
+
* Lazy-load the reCAPTCHA v2 (explicit render) script (once) and resolve with
|
|
1352
|
+
* the `grecaptcha` API. The host only passes `recaptchaSiteKey`; the package
|
|
1353
|
+
* injects the script itself so nothing depends on the host having it loaded.
|
|
1354
|
+
*/
|
|
1355
|
+
function ensureRecaptchaScript() {
|
|
1356
|
+
const w = window;
|
|
1357
|
+
if (w.grecaptcha?.render)
|
|
1358
|
+
return Promise.resolve(w.grecaptcha);
|
|
1359
|
+
return new Promise((resolve, reject) => {
|
|
1360
|
+
const onReady = () => {
|
|
1361
|
+
if (w.grecaptcha?.render)
|
|
1362
|
+
resolve(w.grecaptcha);
|
|
1363
|
+
else
|
|
1364
|
+
reject(new Error('grecaptcha unavailable after script load'));
|
|
1365
|
+
};
|
|
1366
|
+
const existing = document.getElementById(RECAPTCHA_SCRIPT_ID);
|
|
1367
|
+
if (existing) {
|
|
1368
|
+
if (w.grecaptcha?.render)
|
|
1369
|
+
return resolve(w.grecaptcha);
|
|
1370
|
+
existing.addEventListener('load', onReady, { once: true });
|
|
1371
|
+
existing.addEventListener('error', () => reject(new Error('reCAPTCHA script failed to load')), { once: true });
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const script = document.createElement('script');
|
|
1375
|
+
script.id = RECAPTCHA_SCRIPT_ID;
|
|
1376
|
+
// `render=explicit` so we control the (invisible) widget creation ourselves.
|
|
1377
|
+
script.src = 'https://www.google.com/recaptcha/api.js?render=explicit';
|
|
1378
|
+
script.async = true;
|
|
1379
|
+
script.defer = true;
|
|
1380
|
+
script.onload = onReady;
|
|
1381
|
+
script.onerror = () => reject(new Error('reCAPTCHA script failed to load'));
|
|
1382
|
+
document.head.appendChild(script);
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1315
1385
|
function onlyDigits(v) {
|
|
1316
1386
|
return (v ?? '').replace(/\D/g, '');
|
|
1317
1387
|
}
|
|
@@ -1348,11 +1418,15 @@ class DevtoolsAuthService {
|
|
|
1348
1418
|
/** Begin Google OAuth for the admin (full-page redirect). */
|
|
1349
1419
|
loginGoogleRedirect() {
|
|
1350
1420
|
const clientId = this.config.googleClientId;
|
|
1351
|
-
const redirect = this.config.googleRedirectUri
|
|
1421
|
+
const redirect = this.config.googleRedirectUri;
|
|
1352
1422
|
if (!clientId) {
|
|
1353
1423
|
console.error('[devtools] googleClientId not configured');
|
|
1354
1424
|
return;
|
|
1355
1425
|
}
|
|
1426
|
+
if (!redirect) {
|
|
1427
|
+
console.error('[devtools] googleRedirectUri not configured — set it to a URI whitelisted in the Google OAuth client');
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1356
1430
|
try {
|
|
1357
1431
|
sessionStorage.setItem(OAUTH_FLAG, '1');
|
|
1358
1432
|
}
|
|
@@ -1377,11 +1451,17 @@ class DevtoolsAuthService {
|
|
|
1377
1451
|
catch {
|
|
1378
1452
|
/* ignore */
|
|
1379
1453
|
}
|
|
1380
|
-
if (!pending
|
|
1454
|
+
if (!pending)
|
|
1381
1455
|
return;
|
|
1382
|
-
|
|
1456
|
+
// Implicit flow returns everything in the URL fragment. We read the query
|
|
1457
|
+
// too so a misconfigured response_type still surfaces an error instead of
|
|
1458
|
+
// failing silently.
|
|
1459
|
+
const params = new URLSearchParams(window.location.hash.replace(/^#/, '') || window.location.search.replace(/^\?/, ''));
|
|
1383
1460
|
const googleToken = params.get('access_token');
|
|
1384
|
-
|
|
1461
|
+
const error = params.get('error');
|
|
1462
|
+
// Neither token nor error here => this isn't our OAuth landing yet; keep the
|
|
1463
|
+
// flag so the real return (the whitelisted redirect_uri) can consume it.
|
|
1464
|
+
if (!googleToken && !error)
|
|
1385
1465
|
return;
|
|
1386
1466
|
try {
|
|
1387
1467
|
sessionStorage.removeItem(OAUTH_FLAG);
|
|
@@ -1389,18 +1469,25 @@ class DevtoolsAuthService {
|
|
|
1389
1469
|
catch {
|
|
1390
1470
|
/* ignore */
|
|
1391
1471
|
}
|
|
1472
|
+
if (error) {
|
|
1473
|
+
console.error('[devtools] google oauth returned an error:', error, params.get('error_description') ?? '');
|
|
1474
|
+
this.cleanOAuthUrl();
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1392
1477
|
this.exchangeGoogle(googleToken).subscribe({
|
|
1393
|
-
next: () =>
|
|
1394
|
-
try {
|
|
1395
|
-
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
1396
|
-
}
|
|
1397
|
-
catch {
|
|
1398
|
-
/* ignore */
|
|
1399
|
-
}
|
|
1400
|
-
},
|
|
1478
|
+
next: () => this.cleanOAuthUrl(),
|
|
1401
1479
|
error: (e) => console.error('[devtools] admin google login failed', e),
|
|
1402
1480
|
});
|
|
1403
1481
|
}
|
|
1482
|
+
/** Strip the OAuth fragment/query so a reload doesn't re-trigger the return. */
|
|
1483
|
+
cleanOAuthUrl() {
|
|
1484
|
+
try {
|
|
1485
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
1486
|
+
}
|
|
1487
|
+
catch {
|
|
1488
|
+
/* ignore */
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1404
1491
|
exchangeGoogle(googleToken) {
|
|
1405
1492
|
return this.api.exchangeGoogle(googleToken).pipe(tap((tokens) => this.setTokens(tokens)), tap(() => this.api.self(this._token() ?? undefined).subscribe((p) => this.storage.set(ADMIN_USER, p))));
|
|
1406
1493
|
}
|