@nauth-toolkit/client-angular 0.1.90 → 0.1.92
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/esm2022/lib/recaptcha.service.mjs +359 -0
- package/esm2022/lib/social-redirect-callback.guard.mjs +28 -21
- package/esm2022/ngmodule/auth.service.mjs +94 -12
- package/esm2022/ngmodule/tokens.mjs +1 -1
- package/esm2022/public-api.mjs +2 -1
- package/fesm2022/nauth-toolkit-client-angular.mjs +477 -33
- package/fesm2022/nauth-toolkit-client-angular.mjs.map +1 -1
- package/lib/recaptcha.service.d.ts +195 -0
- package/lib/social-redirect-callback.guard.d.ts +1 -0
- package/ngmodule/auth.service.d.ts +45 -10
- package/ngmodule/tokens.d.ts +97 -0
- package/package.json +2 -2
- package/public-api.d.ts +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NAuthErrorCode, NAuthClientError, NAuthClient } from '@nauth-toolkit/client';
|
|
2
2
|
export * from '@nauth-toolkit/client';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { InjectionToken, Injectable, Inject, Optional, inject, NgModule
|
|
4
|
+
import { InjectionToken, Injectable, PLATFORM_ID, Inject, Optional, inject, NgModule } from '@angular/core';
|
|
5
5
|
import { firstValueFrom, BehaviorSubject, Subject, from, switchMap, of, map, catchError, throwError, finalize, shareReplay } from 'rxjs';
|
|
6
6
|
import { filter } from 'rxjs/operators';
|
|
7
7
|
import * as i1 from '@angular/common/http';
|
|
@@ -136,6 +136,362 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
136
136
|
type: Injectable
|
|
137
137
|
}], ctorParameters: () => [{ type: i1.HttpClient }] });
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Injection token for reCAPTCHA configuration
|
|
141
|
+
*/
|
|
142
|
+
const RECAPTCHA_CONFIG = new InjectionToken('RECAPTCHA_CONFIG');
|
|
143
|
+
/**
|
|
144
|
+
* Google reCAPTCHA service for Angular applications.
|
|
145
|
+
*
|
|
146
|
+
* Provides lazy loading of reCAPTCHA script and platform-aware token generation.
|
|
147
|
+
* Automatically detects platform (web, Capacitor WebView, Capacitor native, SSR)
|
|
148
|
+
* and skips reCAPTCHA in environments where it's not supported or needed.
|
|
149
|
+
*
|
|
150
|
+
* Features:
|
|
151
|
+
* - Lazy script loading (only loads when needed)
|
|
152
|
+
* - v2 (checkbox) and v3 (invisible) support
|
|
153
|
+
* - Platform detection (web, Capacitor, SSR)
|
|
154
|
+
* - Automatic skip for Capacitor native mode
|
|
155
|
+
* - Automatic skip for SSR
|
|
156
|
+
*
|
|
157
|
+
* @example v3 Automatic Mode
|
|
158
|
+
* ```typescript
|
|
159
|
+
* constructor(private recaptcha: RecaptchaService) {}
|
|
160
|
+
*
|
|
161
|
+
* async login() {
|
|
162
|
+
* const token = await this.recaptcha.execute('login');
|
|
163
|
+
* await this.auth.login(email, password, token);
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* @example v2 Manual Mode
|
|
168
|
+
* ```typescript
|
|
169
|
+
* ngOnInit() {
|
|
170
|
+
* this.recaptcha.render('recaptcha-container', (token) => {
|
|
171
|
+
* this.recaptchaToken = token;
|
|
172
|
+
* });
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
class RecaptchaService {
|
|
177
|
+
platformId;
|
|
178
|
+
config;
|
|
179
|
+
scriptLoaded = false;
|
|
180
|
+
scriptLoading = null;
|
|
181
|
+
platform;
|
|
182
|
+
widgetId = null;
|
|
183
|
+
constructor(platformId, config) {
|
|
184
|
+
this.platformId = platformId;
|
|
185
|
+
this.config = config;
|
|
186
|
+
this.platform = this.detectPlatform();
|
|
187
|
+
}
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Platform Detection
|
|
190
|
+
// ============================================================================
|
|
191
|
+
/**
|
|
192
|
+
* Detect the current platform environment.
|
|
193
|
+
*
|
|
194
|
+
* Detection priority:
|
|
195
|
+
* 1. SSR (not in browser) → 'ssr'
|
|
196
|
+
* 2. Capacitor native (no web view) → 'capacitor-native'
|
|
197
|
+
* 3. Capacitor WebView → 'capacitor-webview'
|
|
198
|
+
* 4. Web browser → 'web'
|
|
199
|
+
*
|
|
200
|
+
* @returns Detected platform type
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const platform = this.detectPlatform();
|
|
205
|
+
* if (platform === 'capacitor-native') {
|
|
206
|
+
* // Skip reCAPTCHA, use device attestation
|
|
207
|
+
* }
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
detectPlatform() {
|
|
211
|
+
// SSR detection
|
|
212
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
213
|
+
return 'ssr';
|
|
214
|
+
}
|
|
215
|
+
// Capacitor detection (window.Capacitor exists)
|
|
216
|
+
const windowRef = window;
|
|
217
|
+
if (windowRef.Capacitor) {
|
|
218
|
+
// Capacitor native (iOS/Android app)
|
|
219
|
+
if (typeof windowRef.Capacitor.isNativePlatform === 'function' && windowRef.Capacitor.isNativePlatform()) {
|
|
220
|
+
return 'capacitor-native';
|
|
221
|
+
}
|
|
222
|
+
// Capacitor WebView
|
|
223
|
+
return 'capacitor-webview';
|
|
224
|
+
}
|
|
225
|
+
return 'web';
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get the current platform.
|
|
229
|
+
*
|
|
230
|
+
* @returns Current platform type
|
|
231
|
+
*/
|
|
232
|
+
getPlatform() {
|
|
233
|
+
return this.platform;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Check if reCAPTCHA should be skipped for current platform.
|
|
237
|
+
*
|
|
238
|
+
* Skips for:
|
|
239
|
+
* - SSR (no window object)
|
|
240
|
+
* - Capacitor native (use device attestation instead)
|
|
241
|
+
*
|
|
242
|
+
* @returns True if should skip reCAPTCHA
|
|
243
|
+
*/
|
|
244
|
+
shouldSkip() {
|
|
245
|
+
return this.platform === 'ssr' || this.platform === 'capacitor-native';
|
|
246
|
+
}
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// Script Loading
|
|
249
|
+
// ============================================================================
|
|
250
|
+
/**
|
|
251
|
+
* Load Google reCAPTCHA script if not already loaded.
|
|
252
|
+
*
|
|
253
|
+
* Script URL format:
|
|
254
|
+
* - v2: https://www.google.com/recaptcha/api.js
|
|
255
|
+
* - v3: https://www.google.com/recaptcha/api.js?render={siteKey}
|
|
256
|
+
* - Enterprise: https://www.google.com/recaptcha/enterprise.js?render={siteKey}
|
|
257
|
+
*
|
|
258
|
+
* @returns Promise that resolves when script is loaded
|
|
259
|
+
*
|
|
260
|
+
* @throws Error if config is missing or script fails to load
|
|
261
|
+
*/
|
|
262
|
+
async loadScript() {
|
|
263
|
+
// Skip in SSR or Capacitor native
|
|
264
|
+
if (this.shouldSkip()) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Skip if disabled
|
|
268
|
+
if (!this.config?.enabled) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Already loaded
|
|
272
|
+
if (this.scriptLoaded) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Already loading (return existing promise)
|
|
276
|
+
if (this.scriptLoading) {
|
|
277
|
+
return this.scriptLoading;
|
|
278
|
+
}
|
|
279
|
+
// Validate config
|
|
280
|
+
if (!this.config.siteKey) {
|
|
281
|
+
throw new Error('[RecaptchaService] Site key is required');
|
|
282
|
+
}
|
|
283
|
+
// Start loading
|
|
284
|
+
this.scriptLoading = this.injectScript();
|
|
285
|
+
try {
|
|
286
|
+
await this.scriptLoading;
|
|
287
|
+
this.scriptLoaded = true;
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
this.scriptLoading = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Inject the reCAPTCHA script into the DOM.
|
|
295
|
+
*
|
|
296
|
+
* @returns Promise that resolves when script loads
|
|
297
|
+
*/
|
|
298
|
+
injectScript() {
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
const script = document.createElement('script');
|
|
301
|
+
script.async = true;
|
|
302
|
+
script.defer = true;
|
|
303
|
+
// Set script URL based on version
|
|
304
|
+
if (this.config.version === 'enterprise') {
|
|
305
|
+
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${this.config.siteKey}`;
|
|
306
|
+
}
|
|
307
|
+
else if (this.config.version === 'v3') {
|
|
308
|
+
script.src = `https://www.google.com/recaptcha/api.js?render=${this.config.siteKey}`;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// v2 - load without render parameter
|
|
312
|
+
let url = 'https://www.google.com/recaptcha/api.js';
|
|
313
|
+
if (this.config.language) {
|
|
314
|
+
url += `?hl=${this.config.language}`;
|
|
315
|
+
}
|
|
316
|
+
script.src = url;
|
|
317
|
+
}
|
|
318
|
+
script.onload = () => resolve();
|
|
319
|
+
script.onerror = () => reject(new Error('[RecaptchaService] Failed to load reCAPTCHA script'));
|
|
320
|
+
document.head.appendChild(script);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// v3/Enterprise Methods (Invisible Challenge)
|
|
325
|
+
// ============================================================================
|
|
326
|
+
/**
|
|
327
|
+
* Execute reCAPTCHA v3/Enterprise challenge (invisible).
|
|
328
|
+
*
|
|
329
|
+
* Automatically loads script if needed and generates a token.
|
|
330
|
+
* Skips automatically for SSR and Capacitor native.
|
|
331
|
+
*
|
|
332
|
+
* @param action - Action name for v3 analytics (e.g., 'login', 'signup')
|
|
333
|
+
* @returns Promise resolving to reCAPTCHA token, or undefined if skipped
|
|
334
|
+
*
|
|
335
|
+
* @throws Error if version is v2, config missing, or execution fails
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* const token = await this.recaptcha.execute('login');
|
|
340
|
+
* if (token) {
|
|
341
|
+
* await this.auth.login(email, password, token);
|
|
342
|
+
* }
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
async execute(action) {
|
|
346
|
+
// Skip for platforms that don't support reCAPTCHA
|
|
347
|
+
if (this.shouldSkip()) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
// Skip if disabled
|
|
351
|
+
if (!this.config?.enabled) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
// v2 requires manual render
|
|
355
|
+
if (this.config.version === 'v2') {
|
|
356
|
+
throw new Error('[RecaptchaService] execute() is only for v3/Enterprise. Use render() for v2.');
|
|
357
|
+
}
|
|
358
|
+
// Load script if needed
|
|
359
|
+
await this.loadScript();
|
|
360
|
+
// Get grecaptcha object
|
|
361
|
+
const grecaptcha = window.grecaptcha;
|
|
362
|
+
if (!grecaptcha) {
|
|
363
|
+
throw new Error('[RecaptchaService] grecaptcha is not loaded');
|
|
364
|
+
}
|
|
365
|
+
// Execute reCAPTCHA
|
|
366
|
+
const actionName = action || this.config.action || 'submit';
|
|
367
|
+
try {
|
|
368
|
+
if (this.config.version === 'enterprise' && grecaptcha.enterprise?.execute) {
|
|
369
|
+
return await grecaptcha.enterprise.execute(this.config.siteKey, { action: actionName });
|
|
370
|
+
}
|
|
371
|
+
else if (grecaptcha.execute) {
|
|
372
|
+
return await grecaptcha.execute(this.config.siteKey, { action: actionName });
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
throw new Error('[RecaptchaService] grecaptcha.execute is not available');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
throw new Error(`[RecaptchaService] Failed to execute reCAPTCHA: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// ============================================================================
|
|
383
|
+
// v2 Methods (Visible Checkbox)
|
|
384
|
+
// ============================================================================
|
|
385
|
+
/**
|
|
386
|
+
* Render reCAPTCHA v2 checkbox widget.
|
|
387
|
+
*
|
|
388
|
+
* @param containerId - DOM element ID or element to render in
|
|
389
|
+
* @param callback - Callback when user completes challenge
|
|
390
|
+
* @returns Promise resolving to widget ID
|
|
391
|
+
*
|
|
392
|
+
* @throws Error if version is not v2, config missing, or render fails
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```typescript
|
|
396
|
+
* ngAfterViewInit() {
|
|
397
|
+
* this.recaptcha.render('recaptcha-container', (token) => {
|
|
398
|
+
* this.recaptchaToken = token;
|
|
399
|
+
* this.loginForm.patchValue({ recaptchaToken: token });
|
|
400
|
+
* });
|
|
401
|
+
* }
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
async render(containerId, callback) {
|
|
405
|
+
// Skip for platforms that don't support reCAPTCHA
|
|
406
|
+
if (this.shouldSkip()) {
|
|
407
|
+
throw new Error('[RecaptchaService] reCAPTCHA v2 is not supported in SSR or Capacitor native');
|
|
408
|
+
}
|
|
409
|
+
// Skip if disabled
|
|
410
|
+
if (!this.config?.enabled) {
|
|
411
|
+
throw new Error('[RecaptchaService] reCAPTCHA is not enabled');
|
|
412
|
+
}
|
|
413
|
+
// Only for v2
|
|
414
|
+
if (this.config.version !== 'v2') {
|
|
415
|
+
throw new Error('[RecaptchaService] render() is only for v2. Use execute() for v3/Enterprise.');
|
|
416
|
+
}
|
|
417
|
+
// Load script if needed
|
|
418
|
+
await this.loadScript();
|
|
419
|
+
// Get grecaptcha object
|
|
420
|
+
const grecaptcha = window.grecaptcha;
|
|
421
|
+
if (!grecaptcha?.render) {
|
|
422
|
+
throw new Error('[RecaptchaService] grecaptcha.render is not available');
|
|
423
|
+
}
|
|
424
|
+
// Render widget
|
|
425
|
+
try {
|
|
426
|
+
this.widgetId = grecaptcha.render(containerId, {
|
|
427
|
+
sitekey: this.config.siteKey,
|
|
428
|
+
callback: callback,
|
|
429
|
+
});
|
|
430
|
+
return this.widgetId;
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
throw new Error(`[RecaptchaService] Failed to render reCAPTCHA: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get response token from v2 widget.
|
|
438
|
+
*
|
|
439
|
+
* @param widgetId - Widget ID (optional, uses last rendered widget if not provided)
|
|
440
|
+
* @returns reCAPTCHA token or null if not completed
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* const token = this.recaptcha.getResponse();
|
|
445
|
+
* if (token) {
|
|
446
|
+
* await this.auth.login(email, password, token);
|
|
447
|
+
* }
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
getResponse(widgetId) {
|
|
451
|
+
const grecaptcha = window.grecaptcha;
|
|
452
|
+
if (!grecaptcha?.getResponse) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
const id = widgetId !== undefined ? widgetId : this.widgetId ?? undefined;
|
|
456
|
+
return grecaptcha.getResponse(id) || null;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Reset v2 widget (clear response).
|
|
460
|
+
*
|
|
461
|
+
* @param widgetId - Widget ID (optional, uses last rendered widget if not provided)
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```typescript
|
|
465
|
+
* // After failed login
|
|
466
|
+
* this.recaptcha.reset();
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
reset(widgetId) {
|
|
470
|
+
const grecaptcha = window.grecaptcha;
|
|
471
|
+
if (!grecaptcha?.reset) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const id = widgetId !== undefined ? widgetId : this.widgetId ?? undefined;
|
|
475
|
+
grecaptcha.reset(id);
|
|
476
|
+
}
|
|
477
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, deps: [{ token: PLATFORM_ID }, { token: RECAPTCHA_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
478
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, providedIn: 'root' });
|
|
479
|
+
}
|
|
480
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, decorators: [{
|
|
481
|
+
type: Injectable,
|
|
482
|
+
args: [{
|
|
483
|
+
providedIn: 'root',
|
|
484
|
+
}]
|
|
485
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
486
|
+
type: Inject,
|
|
487
|
+
args: [PLATFORM_ID]
|
|
488
|
+
}] }, { type: undefined, decorators: [{
|
|
489
|
+
type: Optional
|
|
490
|
+
}, {
|
|
491
|
+
type: Inject,
|
|
492
|
+
args: [RECAPTCHA_CONFIG]
|
|
493
|
+
}] }] });
|
|
494
|
+
|
|
139
495
|
/**
|
|
140
496
|
* Angular wrapper around NAuthClient that provides promise-based auth methods and reactive state.
|
|
141
497
|
*
|
|
@@ -170,6 +526,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
170
526
|
*/
|
|
171
527
|
class AuthService {
|
|
172
528
|
router;
|
|
529
|
+
recaptchaService;
|
|
173
530
|
client;
|
|
174
531
|
config;
|
|
175
532
|
currentUserSubject = new BehaviorSubject(null);
|
|
@@ -181,9 +538,11 @@ class AuthService {
|
|
|
181
538
|
* @param config - Injected client configuration (required)
|
|
182
539
|
* @param httpAdapter - Angular HTTP adapter for making requests (required)
|
|
183
540
|
* @param router - Angular Router (optional, automatically used for navigation if available)
|
|
541
|
+
* @param recaptchaService - RecaptchaService (optional, for automatic token generation)
|
|
184
542
|
*/
|
|
185
|
-
constructor(config, httpAdapter, router) {
|
|
543
|
+
constructor(config, httpAdapter, router, recaptchaService) {
|
|
186
544
|
this.router = router;
|
|
545
|
+
this.recaptchaService = recaptchaService;
|
|
187
546
|
this.config = config;
|
|
188
547
|
// Use provided httpAdapter (from config or injected)
|
|
189
548
|
const adapter = config.httpAdapter ?? httpAdapter;
|
|
@@ -334,31 +693,39 @@ class AuthService {
|
|
|
334
693
|
/**
|
|
335
694
|
* Login with identifier and password.
|
|
336
695
|
*
|
|
696
|
+
* Automatically generates reCAPTCHA token if configured (v3 only).
|
|
697
|
+
* For v2 manual mode, pass the token explicitly.
|
|
698
|
+
*
|
|
337
699
|
* @param identifier - User email or username
|
|
338
700
|
* @param password - User password
|
|
701
|
+
* @param recaptchaToken - Optional reCAPTCHA token (for v2 manual mode or when auto-generation is disabled)
|
|
339
702
|
* @returns Promise with auth response or challenge
|
|
340
703
|
*
|
|
341
|
-
* @example
|
|
704
|
+
* @example Basic Login
|
|
342
705
|
* ```typescript
|
|
343
706
|
* const response = await this.auth.login('user@example.com', 'password');
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
707
|
+
* ```
|
|
708
|
+
*
|
|
709
|
+
* @example With Manual reCAPTCHA (v2)
|
|
710
|
+
* ```typescript
|
|
711
|
+
* const response = await this.auth.login('user@example.com', 'password', recaptchaToken);
|
|
349
712
|
* ```
|
|
350
713
|
*/
|
|
351
|
-
async login(identifier, password) {
|
|
352
|
-
const
|
|
714
|
+
async login(identifier, password, recaptchaToken) {
|
|
715
|
+
const token = await this.getRecaptchaToken(recaptchaToken, 'login');
|
|
716
|
+
const res = await this.client.login(identifier, password, token);
|
|
353
717
|
return this.updateChallengeState(res);
|
|
354
718
|
}
|
|
355
719
|
/**
|
|
356
720
|
* Signup with credentials.
|
|
357
721
|
*
|
|
722
|
+
* Automatically generates reCAPTCHA token if configured (v3 only).
|
|
723
|
+
* For v2 manual mode, include token in payload.
|
|
724
|
+
*
|
|
358
725
|
* @param payload - Signup request payload
|
|
359
726
|
* @returns Promise with auth response or challenge
|
|
360
727
|
*
|
|
361
|
-
* @example
|
|
728
|
+
* @example Basic Signup
|
|
362
729
|
* ```typescript
|
|
363
730
|
* const response = await this.auth.signup({
|
|
364
731
|
* email: 'new@example.com',
|
|
@@ -366,8 +733,25 @@ class AuthService {
|
|
|
366
733
|
* firstName: 'John',
|
|
367
734
|
* });
|
|
368
735
|
* ```
|
|
736
|
+
*
|
|
737
|
+
* @example With Manual reCAPTCHA (v2)
|
|
738
|
+
* ```typescript
|
|
739
|
+
* const response = await this.auth.signup({
|
|
740
|
+
* email: 'new@example.com',
|
|
741
|
+
* password: 'SecurePass123!',
|
|
742
|
+
* recaptchaToken: token,
|
|
743
|
+
* });
|
|
744
|
+
* ```
|
|
369
745
|
*/
|
|
370
746
|
async signup(payload) {
|
|
747
|
+
// Auto-generate reCAPTCHA token for v3 if configured and not already provided
|
|
748
|
+
const payloadWithRecaptcha = payload;
|
|
749
|
+
if (!payloadWithRecaptcha.recaptchaToken) {
|
|
750
|
+
const token = await this.getRecaptchaToken(undefined, 'signup');
|
|
751
|
+
if (token) {
|
|
752
|
+
payload = { ...payload, recaptchaToken: token };
|
|
753
|
+
}
|
|
754
|
+
}
|
|
371
755
|
const res = await this.client.signup(payload);
|
|
372
756
|
return this.updateChallengeState(res);
|
|
373
757
|
}
|
|
@@ -1029,7 +1413,58 @@ class AuthService {
|
|
|
1029
1413
|
}
|
|
1030
1414
|
return response;
|
|
1031
1415
|
}
|
|
1032
|
-
|
|
1416
|
+
// ============================================================================
|
|
1417
|
+
// reCAPTCHA Helper
|
|
1418
|
+
// ============================================================================
|
|
1419
|
+
/**
|
|
1420
|
+
* Get reCAPTCHA token - auto-generate for v3 or use provided token.
|
|
1421
|
+
*
|
|
1422
|
+
* Handles platform detection:
|
|
1423
|
+
* - Web browser: Generate token if enabled and v3
|
|
1424
|
+
* - Capacitor native: Skip (use device attestation instead)
|
|
1425
|
+
* - SSR: Skip
|
|
1426
|
+
* - Manual mode (v2 or manualChallenge=true): Requires explicit token
|
|
1427
|
+
*
|
|
1428
|
+
* @param providedToken - Explicitly provided token (v2 manual mode)
|
|
1429
|
+
* @param action - Action name for v3 analytics
|
|
1430
|
+
* @returns reCAPTCHA token or undefined
|
|
1431
|
+
*
|
|
1432
|
+
* @private
|
|
1433
|
+
*/
|
|
1434
|
+
async getRecaptchaToken(providedToken, action) {
|
|
1435
|
+
// If token explicitly provided, use it (v2 manual mode)
|
|
1436
|
+
if (providedToken) {
|
|
1437
|
+
return providedToken;
|
|
1438
|
+
}
|
|
1439
|
+
// No reCAPTCHA service available
|
|
1440
|
+
if (!this.recaptchaService) {
|
|
1441
|
+
return undefined;
|
|
1442
|
+
}
|
|
1443
|
+
// Check if reCAPTCHA is configured
|
|
1444
|
+
const recaptchaConfig = this.config.recaptcha;
|
|
1445
|
+
if (!recaptchaConfig?.enabled) {
|
|
1446
|
+
return undefined;
|
|
1447
|
+
}
|
|
1448
|
+
// Skip for platforms that don't support reCAPTCHA (SSR, Capacitor native)
|
|
1449
|
+
if (this.recaptchaService.shouldSkip()) {
|
|
1450
|
+
return undefined;
|
|
1451
|
+
}
|
|
1452
|
+
// v2 or manual mode - user must provide token explicitly
|
|
1453
|
+
if (recaptchaConfig.version === 'v2' || recaptchaConfig.manualChallenge) {
|
|
1454
|
+
return undefined;
|
|
1455
|
+
}
|
|
1456
|
+
// Auto-generate token for v3/Enterprise
|
|
1457
|
+
try {
|
|
1458
|
+
return await this.recaptchaService.execute(action);
|
|
1459
|
+
}
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
// Log error but don't block authentication
|
|
1462
|
+
// Server will enforce if required
|
|
1463
|
+
console.warn('[AuthService] Failed to generate reCAPTCHA token:', error);
|
|
1464
|
+
return undefined;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService, deps: [{ token: NAUTH_CLIENT_CONFIG }, { token: AngularHttpAdapter }, { token: i2.Router, optional: true }, { token: RecaptchaService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1033
1468
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService });
|
|
1034
1469
|
}
|
|
1035
1470
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService, decorators: [{
|
|
@@ -1039,6 +1474,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1039
1474
|
args: [NAUTH_CLIENT_CONFIG]
|
|
1040
1475
|
}] }, { type: AngularHttpAdapter }, { type: i2.Router, decorators: [{
|
|
1041
1476
|
type: Optional
|
|
1477
|
+
}] }, { type: RecaptchaService, decorators: [{
|
|
1478
|
+
type: Optional
|
|
1042
1479
|
}] }] });
|
|
1043
1480
|
|
|
1044
1481
|
/**
|
|
@@ -1557,6 +1994,7 @@ class AuthInterceptor {
|
|
|
1557
1994
|
* - If `exchangeToken` exists: exchanges it via backend (SDK handles navigation automatically).
|
|
1558
1995
|
* - If no `exchangeToken`: treat as cookie-success path (SDK handles navigation automatically).
|
|
1559
1996
|
* - If `error` exists: redirects to oauthError route.
|
|
1997
|
+
* - If auto-redirect is disabled (redirectUrls set to null): returns true to activate the route.
|
|
1560
1998
|
*
|
|
1561
1999
|
* @example
|
|
1562
2000
|
* ```typescript
|
|
@@ -1587,14 +2025,16 @@ const socialRedirectCallbackGuard = async () => {
|
|
|
1587
2025
|
if (appState) {
|
|
1588
2026
|
await auth.getClient().storeOauthState(appState);
|
|
1589
2027
|
}
|
|
1590
|
-
// Provider error: redirect to oauthError
|
|
2028
|
+
// Provider error: redirect to oauthError (or activate route if auto-redirect disabled)
|
|
1591
2029
|
if (error) {
|
|
1592
2030
|
await router.navigateToError('oauth');
|
|
1593
|
-
|
|
2031
|
+
// Return true to activate route if oauthError redirect is disabled
|
|
2032
|
+
return router.isErrorRedirectDisabled('oauth');
|
|
1594
2033
|
}
|
|
1595
2034
|
// No exchangeToken: cookie success path; hydrate then navigate to success.
|
|
1596
2035
|
//
|
|
1597
|
-
// Note: we do not "activate" the callback route to avoid
|
|
2036
|
+
// Note: When auto-redirect is enabled, we do not "activate" the callback route to avoid
|
|
2037
|
+
// consumers needing to render a page. When disabled, we activate the route.
|
|
1598
2038
|
if (!exchangeToken) {
|
|
1599
2039
|
// ============================================================================
|
|
1600
2040
|
// Cookies mode: hydrate user state before redirecting
|
|
@@ -1636,32 +2076,36 @@ const socialRedirectCallbackGuard = async () => {
|
|
|
1636
2076
|
appState: appState ?? undefined,
|
|
1637
2077
|
});
|
|
1638
2078
|
}
|
|
1639
|
-
catch (
|
|
2079
|
+
catch (error) {
|
|
1640
2080
|
// Only treat auth failures (401/403) as OAuth errors
|
|
1641
2081
|
// Network errors or other issues might be temporary - still try success route
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
// The auth guard will handle authentication state on the next route
|
|
1655
|
-
await router.navigateToSuccess(appState ? { appState } : undefined);
|
|
2082
|
+
// Type guard: check if error is NAuthClientError
|
|
2083
|
+
if (error instanceof NAuthClientError) {
|
|
2084
|
+
const isAuthError = error.statusCode === 401 ||
|
|
2085
|
+
error.statusCode === 403 ||
|
|
2086
|
+
error.code === NAuthErrorCode.AUTH_TOKEN_INVALID ||
|
|
2087
|
+
error.code === NAuthErrorCode.AUTH_SESSION_EXPIRED ||
|
|
2088
|
+
error.code === NAuthErrorCode.AUTH_SESSION_NOT_FOUND;
|
|
2089
|
+
if (isAuthError) {
|
|
2090
|
+
// Cookies weren't set properly - OAuth failed
|
|
2091
|
+
await router.navigateToError('oauth');
|
|
2092
|
+
return router.isErrorRedirectDisabled('oauth');
|
|
2093
|
+
}
|
|
1656
2094
|
}
|
|
2095
|
+
// For network errors or other non-auth issues, proceed to success route
|
|
2096
|
+
// The auth guard will handle authentication state on the next route
|
|
2097
|
+
await router.navigateToSuccess(appState ? { appState } : undefined);
|
|
1657
2098
|
}
|
|
1658
|
-
|
|
2099
|
+
// Return true if success redirect is disabled, allowing the callback component to render
|
|
2100
|
+
return router.isSuccessRedirectDisabled({ source: 'social', appState: appState ?? undefined });
|
|
1659
2101
|
}
|
|
1660
2102
|
// Exchange token - SDK handles navigation automatically
|
|
1661
2103
|
// Note: appState will be passed via query params when navigateToSuccess is called
|
|
1662
2104
|
// by the challenge router after successful exchange
|
|
1663
2105
|
await auth.exchangeSocialRedirect(exchangeToken);
|
|
1664
|
-
|
|
2106
|
+
// Return true if success redirect is disabled, allowing the callback component to render
|
|
2107
|
+
// We use 'social' as source since this is the social OAuth callback flow
|
|
2108
|
+
return router.isSuccessRedirectDisabled({ source: 'social', appState: appState ?? undefined });
|
|
1665
2109
|
};
|
|
1666
2110
|
|
|
1667
2111
|
/**
|
|
@@ -1676,5 +2120,5 @@ const socialRedirectCallbackGuard = async () => {
|
|
|
1676
2120
|
* Generated bundle index. Do not edit.
|
|
1677
2121
|
*/
|
|
1678
2122
|
|
|
1679
|
-
export { AngularHttpAdapter, AuthGuard, AuthInterceptor, AuthInterceptorClass, AuthService, NAUTH_CLIENT_CONFIG, NAuthModule, authGuard, authInterceptor, socialRedirectCallbackGuard };
|
|
2123
|
+
export { AngularHttpAdapter, AuthGuard, AuthInterceptor, AuthInterceptorClass, AuthService, NAUTH_CLIENT_CONFIG, NAuthModule, RECAPTCHA_CONFIG, RecaptchaService, authGuard, authInterceptor, socialRedirectCallbackGuard };
|
|
1680
2124
|
//# sourceMappingURL=nauth-toolkit-client-angular.mjs.map
|