@nauth-toolkit/client-angular 0.1.91 → 0.1.93
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-provider.mjs +52 -0
- package/esm2022/lib/recaptcha.service.mjs +371 -0
- package/esm2022/lib/social-redirect-callback.guard.mjs +28 -21
- package/esm2022/ngmodule/auth.service.mjs +94 -12
- package/esm2022/ngmodule/nauth.module.mjs +60 -27
- package/esm2022/ngmodule/tokens.mjs +1 -1
- package/esm2022/public-api.mjs +3 -1
- package/fesm2022/nauth-toolkit-client-angular.mjs +596 -58
- package/fesm2022/nauth-toolkit-client-angular.mjs.map +1 -1
- package/lib/recaptcha-provider.d.ts +27 -0
- 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/nauth.module.d.ts +17 -3
- package/ngmodule/tokens.d.ts +97 -0
- package/package.json +2 -2
- package/public-api.d.ts +2 -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, APP_INITIALIZER, NgModule, makeEnvironmentProviders } 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,374 @@ 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
|
+
providedIn: 'root',
|
|
144
|
+
factory: () => undefined,
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* Google reCAPTCHA service for Angular applications.
|
|
148
|
+
*
|
|
149
|
+
* Provides lazy loading of reCAPTCHA script and platform-aware token generation.
|
|
150
|
+
* Automatically detects platform (web, Capacitor WebView, Capacitor native, SSR)
|
|
151
|
+
* and skips reCAPTCHA in environments where it's not supported or needed.
|
|
152
|
+
*
|
|
153
|
+
* Features:
|
|
154
|
+
* - Lazy script loading (only loads when needed)
|
|
155
|
+
* - v2 (checkbox) and v3 (invisible) support
|
|
156
|
+
* - Platform detection (web, Capacitor, SSR)
|
|
157
|
+
* - Automatic skip for Capacitor native mode
|
|
158
|
+
* - Automatic skip for SSR
|
|
159
|
+
*
|
|
160
|
+
* @example v3 Automatic Mode
|
|
161
|
+
* ```typescript
|
|
162
|
+
* constructor(private recaptcha: RecaptchaService) {}
|
|
163
|
+
*
|
|
164
|
+
* async login() {
|
|
165
|
+
* const token = await this.recaptcha.execute('login');
|
|
166
|
+
* await this.auth.login(email, password, token);
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @example v2 Manual Mode
|
|
171
|
+
* ```typescript
|
|
172
|
+
* ngOnInit() {
|
|
173
|
+
* this.recaptcha.render('recaptcha-container', (token) => {
|
|
174
|
+
* this.recaptchaToken = token;
|
|
175
|
+
* });
|
|
176
|
+
* }
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
class RecaptchaService {
|
|
180
|
+
platformId;
|
|
181
|
+
config;
|
|
182
|
+
scriptLoaded = false;
|
|
183
|
+
scriptLoading = null;
|
|
184
|
+
platform;
|
|
185
|
+
widgetId = null;
|
|
186
|
+
constructor(platformId, config) {
|
|
187
|
+
this.platformId = platformId;
|
|
188
|
+
this.config = config;
|
|
189
|
+
this.platform = this.detectPlatform();
|
|
190
|
+
// Auto-preload script for v3/Enterprise so it's ready before first login/signup
|
|
191
|
+
// No-op when disabled, shouldSkip, or v2 (v2 renders on-demand)
|
|
192
|
+
if (this.config?.enabled && (this.config.version === 'v3' || this.config.version === 'enterprise')) {
|
|
193
|
+
if (!this.shouldSkip()) {
|
|
194
|
+
this.loadScript().catch(() => {
|
|
195
|
+
// Silently fail - execute() will handle errors when called
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Platform Detection
|
|
202
|
+
// ============================================================================
|
|
203
|
+
/**
|
|
204
|
+
* Detect the current platform environment.
|
|
205
|
+
*
|
|
206
|
+
* Detection priority:
|
|
207
|
+
* 1. SSR (not in browser) → 'ssr'
|
|
208
|
+
* 2. Capacitor native (no web view) → 'capacitor-native'
|
|
209
|
+
* 3. Capacitor WebView → 'capacitor-webview'
|
|
210
|
+
* 4. Web browser → 'web'
|
|
211
|
+
*
|
|
212
|
+
* @returns Detected platform type
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* const platform = this.detectPlatform();
|
|
217
|
+
* if (platform === 'capacitor-native') {
|
|
218
|
+
* // Skip reCAPTCHA, use device attestation
|
|
219
|
+
* }
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
detectPlatform() {
|
|
223
|
+
// SSR detection
|
|
224
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
225
|
+
return 'ssr';
|
|
226
|
+
}
|
|
227
|
+
// Capacitor detection (window.Capacitor exists)
|
|
228
|
+
const windowRef = window;
|
|
229
|
+
if (windowRef.Capacitor) {
|
|
230
|
+
// Capacitor native (iOS/Android app)
|
|
231
|
+
if (typeof windowRef.Capacitor.isNativePlatform === 'function' && windowRef.Capacitor.isNativePlatform()) {
|
|
232
|
+
return 'capacitor-native';
|
|
233
|
+
}
|
|
234
|
+
// Capacitor WebView
|
|
235
|
+
return 'capacitor-webview';
|
|
236
|
+
}
|
|
237
|
+
return 'web';
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get the current platform.
|
|
241
|
+
*
|
|
242
|
+
* @returns Current platform type
|
|
243
|
+
*/
|
|
244
|
+
getPlatform() {
|
|
245
|
+
return this.platform;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Check if reCAPTCHA should be skipped for current platform.
|
|
249
|
+
*
|
|
250
|
+
* Skips for:
|
|
251
|
+
* - SSR (no window object)
|
|
252
|
+
* - Capacitor native (use device attestation instead)
|
|
253
|
+
*
|
|
254
|
+
* @returns True if should skip reCAPTCHA
|
|
255
|
+
*/
|
|
256
|
+
shouldSkip() {
|
|
257
|
+
return this.platform === 'ssr' || this.platform === 'capacitor-native';
|
|
258
|
+
}
|
|
259
|
+
// ============================================================================
|
|
260
|
+
// Script Loading
|
|
261
|
+
// ============================================================================
|
|
262
|
+
/**
|
|
263
|
+
* Load Google reCAPTCHA script if not already loaded.
|
|
264
|
+
*
|
|
265
|
+
* Script URL format:
|
|
266
|
+
* - v2: https://www.google.com/recaptcha/api.js
|
|
267
|
+
* - v3: https://www.google.com/recaptcha/api.js?render={siteKey}
|
|
268
|
+
* - Enterprise: https://www.google.com/recaptcha/enterprise.js?render={siteKey}
|
|
269
|
+
*
|
|
270
|
+
* @returns Promise that resolves when script is loaded
|
|
271
|
+
*
|
|
272
|
+
* @throws Error if config is missing or script fails to load
|
|
273
|
+
*/
|
|
274
|
+
async loadScript() {
|
|
275
|
+
// Skip in SSR or Capacitor native
|
|
276
|
+
if (this.shouldSkip()) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Skip if disabled
|
|
280
|
+
if (!this.config?.enabled) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Already loaded
|
|
284
|
+
if (this.scriptLoaded) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Already loading (return existing promise)
|
|
288
|
+
if (this.scriptLoading) {
|
|
289
|
+
return this.scriptLoading;
|
|
290
|
+
}
|
|
291
|
+
// Validate config
|
|
292
|
+
if (!this.config.siteKey) {
|
|
293
|
+
throw new Error('[RecaptchaService] Site key is required');
|
|
294
|
+
}
|
|
295
|
+
// Start loading
|
|
296
|
+
this.scriptLoading = this.injectScript();
|
|
297
|
+
try {
|
|
298
|
+
await this.scriptLoading;
|
|
299
|
+
this.scriptLoaded = true;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
this.scriptLoading = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Inject the reCAPTCHA script into the DOM.
|
|
307
|
+
*
|
|
308
|
+
* @returns Promise that resolves when script loads
|
|
309
|
+
*/
|
|
310
|
+
injectScript() {
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const script = document.createElement('script');
|
|
313
|
+
script.async = true;
|
|
314
|
+
script.defer = true;
|
|
315
|
+
// Set script URL based on version
|
|
316
|
+
if (this.config.version === 'enterprise') {
|
|
317
|
+
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${this.config.siteKey}`;
|
|
318
|
+
}
|
|
319
|
+
else if (this.config.version === 'v3') {
|
|
320
|
+
script.src = `https://www.google.com/recaptcha/api.js?render=${this.config.siteKey}`;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// v2 - load without render parameter
|
|
324
|
+
let url = 'https://www.google.com/recaptcha/api.js';
|
|
325
|
+
if (this.config.language) {
|
|
326
|
+
url += `?hl=${this.config.language}`;
|
|
327
|
+
}
|
|
328
|
+
script.src = url;
|
|
329
|
+
}
|
|
330
|
+
script.onload = () => resolve();
|
|
331
|
+
script.onerror = () => reject(new Error('[RecaptchaService] Failed to load reCAPTCHA script'));
|
|
332
|
+
document.head.appendChild(script);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// ============================================================================
|
|
336
|
+
// v3/Enterprise Methods (Invisible Challenge)
|
|
337
|
+
// ============================================================================
|
|
338
|
+
/**
|
|
339
|
+
* Execute reCAPTCHA v3/Enterprise challenge (invisible).
|
|
340
|
+
*
|
|
341
|
+
* Automatically loads script if needed and generates a token.
|
|
342
|
+
* Skips automatically for SSR and Capacitor native.
|
|
343
|
+
*
|
|
344
|
+
* @param action - Action name for v3 analytics (e.g., 'login', 'signup')
|
|
345
|
+
* @returns Promise resolving to reCAPTCHA token, or undefined if skipped
|
|
346
|
+
*
|
|
347
|
+
* @throws Error if version is v2, config missing, or execution fails
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const token = await this.recaptcha.execute('login');
|
|
352
|
+
* if (token) {
|
|
353
|
+
* await this.auth.login(email, password, token);
|
|
354
|
+
* }
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
async execute(action) {
|
|
358
|
+
// Skip for platforms that don't support reCAPTCHA
|
|
359
|
+
if (this.shouldSkip()) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
// Skip if disabled
|
|
363
|
+
if (!this.config?.enabled) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
// v2 requires manual render
|
|
367
|
+
if (this.config.version === 'v2') {
|
|
368
|
+
throw new Error('[RecaptchaService] execute() is only for v3/Enterprise. Use render() for v2.');
|
|
369
|
+
}
|
|
370
|
+
// Load script if needed
|
|
371
|
+
await this.loadScript();
|
|
372
|
+
// Get grecaptcha object
|
|
373
|
+
const grecaptcha = window.grecaptcha;
|
|
374
|
+
if (!grecaptcha) {
|
|
375
|
+
throw new Error('[RecaptchaService] grecaptcha is not loaded');
|
|
376
|
+
}
|
|
377
|
+
// Execute reCAPTCHA
|
|
378
|
+
const actionName = action || this.config.action || 'submit';
|
|
379
|
+
try {
|
|
380
|
+
if (this.config.version === 'enterprise' && grecaptcha.enterprise?.execute) {
|
|
381
|
+
return await grecaptcha.enterprise.execute(this.config.siteKey, { action: actionName });
|
|
382
|
+
}
|
|
383
|
+
else if (grecaptcha.execute) {
|
|
384
|
+
return await grecaptcha.execute(this.config.siteKey, { action: actionName });
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
throw new Error('[RecaptchaService] grecaptcha.execute is not available');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
throw new Error(`[RecaptchaService] Failed to execute reCAPTCHA: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// v2 Methods (Visible Checkbox)
|
|
396
|
+
// ============================================================================
|
|
397
|
+
/**
|
|
398
|
+
* Render reCAPTCHA v2 checkbox widget.
|
|
399
|
+
*
|
|
400
|
+
* @param containerId - DOM element ID or element to render in
|
|
401
|
+
* @param callback - Callback when user completes challenge
|
|
402
|
+
* @returns Promise resolving to widget ID
|
|
403
|
+
*
|
|
404
|
+
* @throws Error if version is not v2, config missing, or render fails
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```typescript
|
|
408
|
+
* ngAfterViewInit() {
|
|
409
|
+
* this.recaptcha.render('recaptcha-container', (token) => {
|
|
410
|
+
* this.recaptchaToken = token;
|
|
411
|
+
* this.loginForm.patchValue({ recaptchaToken: token });
|
|
412
|
+
* });
|
|
413
|
+
* }
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
async render(containerId, callback) {
|
|
417
|
+
// Skip for platforms that don't support reCAPTCHA
|
|
418
|
+
if (this.shouldSkip()) {
|
|
419
|
+
throw new Error('[RecaptchaService] reCAPTCHA v2 is not supported in SSR or Capacitor native');
|
|
420
|
+
}
|
|
421
|
+
// Skip if disabled
|
|
422
|
+
if (!this.config?.enabled) {
|
|
423
|
+
throw new Error('[RecaptchaService] reCAPTCHA is not enabled');
|
|
424
|
+
}
|
|
425
|
+
// Only for v2
|
|
426
|
+
if (this.config.version !== 'v2') {
|
|
427
|
+
throw new Error('[RecaptchaService] render() is only for v2. Use execute() for v3/Enterprise.');
|
|
428
|
+
}
|
|
429
|
+
// Load script if needed
|
|
430
|
+
await this.loadScript();
|
|
431
|
+
// Get grecaptcha object
|
|
432
|
+
const grecaptcha = window.grecaptcha;
|
|
433
|
+
if (!grecaptcha?.render) {
|
|
434
|
+
throw new Error('[RecaptchaService] grecaptcha.render is not available');
|
|
435
|
+
}
|
|
436
|
+
// Render widget
|
|
437
|
+
try {
|
|
438
|
+
this.widgetId = grecaptcha.render(containerId, {
|
|
439
|
+
sitekey: this.config.siteKey,
|
|
440
|
+
callback: callback,
|
|
441
|
+
});
|
|
442
|
+
return this.widgetId;
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
throw new Error(`[RecaptchaService] Failed to render reCAPTCHA: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get response token from v2 widget.
|
|
450
|
+
*
|
|
451
|
+
* @param widgetId - Widget ID (optional, uses last rendered widget if not provided)
|
|
452
|
+
* @returns reCAPTCHA token or null if not completed
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```typescript
|
|
456
|
+
* const token = this.recaptcha.getResponse();
|
|
457
|
+
* if (token) {
|
|
458
|
+
* await this.auth.login(email, password, token);
|
|
459
|
+
* }
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
getResponse(widgetId) {
|
|
463
|
+
const grecaptcha = window.grecaptcha;
|
|
464
|
+
if (!grecaptcha?.getResponse) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const id = widgetId !== undefined ? widgetId : this.widgetId ?? undefined;
|
|
468
|
+
return grecaptcha.getResponse(id) || null;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Reset v2 widget (clear response).
|
|
472
|
+
*
|
|
473
|
+
* @param widgetId - Widget ID (optional, uses last rendered widget if not provided)
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```typescript
|
|
477
|
+
* // After failed login
|
|
478
|
+
* this.recaptcha.reset();
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
reset(widgetId) {
|
|
482
|
+
const grecaptcha = window.grecaptcha;
|
|
483
|
+
if (!grecaptcha?.reset) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const id = widgetId !== undefined ? widgetId : this.widgetId ?? undefined;
|
|
487
|
+
grecaptcha.reset(id);
|
|
488
|
+
}
|
|
489
|
+
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 });
|
|
490
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, providedIn: 'root' });
|
|
491
|
+
}
|
|
492
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, decorators: [{
|
|
493
|
+
type: Injectable,
|
|
494
|
+
args: [{
|
|
495
|
+
providedIn: 'root',
|
|
496
|
+
}]
|
|
497
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
498
|
+
type: Inject,
|
|
499
|
+
args: [PLATFORM_ID]
|
|
500
|
+
}] }, { type: undefined, decorators: [{
|
|
501
|
+
type: Optional
|
|
502
|
+
}, {
|
|
503
|
+
type: Inject,
|
|
504
|
+
args: [RECAPTCHA_CONFIG]
|
|
505
|
+
}] }] });
|
|
506
|
+
|
|
139
507
|
/**
|
|
140
508
|
* Angular wrapper around NAuthClient that provides promise-based auth methods and reactive state.
|
|
141
509
|
*
|
|
@@ -170,6 +538,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
170
538
|
*/
|
|
171
539
|
class AuthService {
|
|
172
540
|
router;
|
|
541
|
+
recaptchaService;
|
|
173
542
|
client;
|
|
174
543
|
config;
|
|
175
544
|
currentUserSubject = new BehaviorSubject(null);
|
|
@@ -181,9 +550,11 @@ class AuthService {
|
|
|
181
550
|
* @param config - Injected client configuration (required)
|
|
182
551
|
* @param httpAdapter - Angular HTTP adapter for making requests (required)
|
|
183
552
|
* @param router - Angular Router (optional, automatically used for navigation if available)
|
|
553
|
+
* @param recaptchaService - RecaptchaService (optional, for automatic token generation)
|
|
184
554
|
*/
|
|
185
|
-
constructor(config, httpAdapter, router) {
|
|
555
|
+
constructor(config, httpAdapter, router, recaptchaService) {
|
|
186
556
|
this.router = router;
|
|
557
|
+
this.recaptchaService = recaptchaService;
|
|
187
558
|
this.config = config;
|
|
188
559
|
// Use provided httpAdapter (from config or injected)
|
|
189
560
|
const adapter = config.httpAdapter ?? httpAdapter;
|
|
@@ -334,31 +705,39 @@ class AuthService {
|
|
|
334
705
|
/**
|
|
335
706
|
* Login with identifier and password.
|
|
336
707
|
*
|
|
708
|
+
* Automatically generates reCAPTCHA token if configured (v3 only).
|
|
709
|
+
* For v2 manual mode, pass the token explicitly.
|
|
710
|
+
*
|
|
337
711
|
* @param identifier - User email or username
|
|
338
712
|
* @param password - User password
|
|
713
|
+
* @param recaptchaToken - Optional reCAPTCHA token (for v2 manual mode or when auto-generation is disabled)
|
|
339
714
|
* @returns Promise with auth response or challenge
|
|
340
715
|
*
|
|
341
|
-
* @example
|
|
716
|
+
* @example Basic Login
|
|
342
717
|
* ```typescript
|
|
343
718
|
* const response = await this.auth.login('user@example.com', 'password');
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
719
|
+
* ```
|
|
720
|
+
*
|
|
721
|
+
* @example With Manual reCAPTCHA (v2)
|
|
722
|
+
* ```typescript
|
|
723
|
+
* const response = await this.auth.login('user@example.com', 'password', recaptchaToken);
|
|
349
724
|
* ```
|
|
350
725
|
*/
|
|
351
|
-
async login(identifier, password) {
|
|
352
|
-
const
|
|
726
|
+
async login(identifier, password, recaptchaToken) {
|
|
727
|
+
const token = await this.getRecaptchaToken(recaptchaToken, 'login');
|
|
728
|
+
const res = await this.client.login(identifier, password, token);
|
|
353
729
|
return this.updateChallengeState(res);
|
|
354
730
|
}
|
|
355
731
|
/**
|
|
356
732
|
* Signup with credentials.
|
|
357
733
|
*
|
|
734
|
+
* Automatically generates reCAPTCHA token if configured (v3 only).
|
|
735
|
+
* For v2 manual mode, include token in payload.
|
|
736
|
+
*
|
|
358
737
|
* @param payload - Signup request payload
|
|
359
738
|
* @returns Promise with auth response or challenge
|
|
360
739
|
*
|
|
361
|
-
* @example
|
|
740
|
+
* @example Basic Signup
|
|
362
741
|
* ```typescript
|
|
363
742
|
* const response = await this.auth.signup({
|
|
364
743
|
* email: 'new@example.com',
|
|
@@ -366,8 +745,25 @@ class AuthService {
|
|
|
366
745
|
* firstName: 'John',
|
|
367
746
|
* });
|
|
368
747
|
* ```
|
|
748
|
+
*
|
|
749
|
+
* @example With Manual reCAPTCHA (v2)
|
|
750
|
+
* ```typescript
|
|
751
|
+
* const response = await this.auth.signup({
|
|
752
|
+
* email: 'new@example.com',
|
|
753
|
+
* password: 'SecurePass123!',
|
|
754
|
+
* recaptchaToken: token,
|
|
755
|
+
* });
|
|
756
|
+
* ```
|
|
369
757
|
*/
|
|
370
758
|
async signup(payload) {
|
|
759
|
+
// Auto-generate reCAPTCHA token for v3 if configured and not already provided
|
|
760
|
+
const payloadWithRecaptcha = payload;
|
|
761
|
+
if (!payloadWithRecaptcha.recaptchaToken) {
|
|
762
|
+
const token = await this.getRecaptchaToken(undefined, 'signup');
|
|
763
|
+
if (token) {
|
|
764
|
+
payload = { ...payload, recaptchaToken: token };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
371
767
|
const res = await this.client.signup(payload);
|
|
372
768
|
return this.updateChallengeState(res);
|
|
373
769
|
}
|
|
@@ -1029,7 +1425,58 @@ class AuthService {
|
|
|
1029
1425
|
}
|
|
1030
1426
|
return response;
|
|
1031
1427
|
}
|
|
1032
|
-
|
|
1428
|
+
// ============================================================================
|
|
1429
|
+
// reCAPTCHA Helper
|
|
1430
|
+
// ============================================================================
|
|
1431
|
+
/**
|
|
1432
|
+
* Get reCAPTCHA token - auto-generate for v3 or use provided token.
|
|
1433
|
+
*
|
|
1434
|
+
* Handles platform detection:
|
|
1435
|
+
* - Web browser: Generate token if enabled and v3
|
|
1436
|
+
* - Capacitor native: Skip (use device attestation instead)
|
|
1437
|
+
* - SSR: Skip
|
|
1438
|
+
* - Manual mode (v2 or manualChallenge=true): Requires explicit token
|
|
1439
|
+
*
|
|
1440
|
+
* @param providedToken - Explicitly provided token (v2 manual mode)
|
|
1441
|
+
* @param action - Action name for v3 analytics
|
|
1442
|
+
* @returns reCAPTCHA token or undefined
|
|
1443
|
+
*
|
|
1444
|
+
* @private
|
|
1445
|
+
*/
|
|
1446
|
+
async getRecaptchaToken(providedToken, action) {
|
|
1447
|
+
// If token explicitly provided, use it (v2 manual mode)
|
|
1448
|
+
if (providedToken) {
|
|
1449
|
+
return providedToken;
|
|
1450
|
+
}
|
|
1451
|
+
// No reCAPTCHA service available
|
|
1452
|
+
if (!this.recaptchaService) {
|
|
1453
|
+
return undefined;
|
|
1454
|
+
}
|
|
1455
|
+
// Check if reCAPTCHA is configured
|
|
1456
|
+
const recaptchaConfig = this.config.recaptcha;
|
|
1457
|
+
if (!recaptchaConfig?.enabled) {
|
|
1458
|
+
return undefined;
|
|
1459
|
+
}
|
|
1460
|
+
// Skip for platforms that don't support reCAPTCHA (SSR, Capacitor native)
|
|
1461
|
+
if (this.recaptchaService.shouldSkip()) {
|
|
1462
|
+
return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
// v2 or manual mode - user must provide token explicitly
|
|
1465
|
+
if (recaptchaConfig.version === 'v2' || recaptchaConfig.manualChallenge) {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
// Auto-generate token for v3/Enterprise
|
|
1469
|
+
try {
|
|
1470
|
+
return await this.recaptchaService.execute(action);
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
// Log error but don't block authentication
|
|
1474
|
+
// Server will enforce if required
|
|
1475
|
+
console.warn('[AuthService] Failed to generate reCAPTCHA token:', error);
|
|
1476
|
+
return undefined;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
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
1480
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService });
|
|
1034
1481
|
}
|
|
1035
1482
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService, decorators: [{
|
|
@@ -1039,6 +1486,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1039
1486
|
args: [NAUTH_CLIENT_CONFIG]
|
|
1040
1487
|
}] }, { type: AngularHttpAdapter }, { type: i2.Router, decorators: [{
|
|
1041
1488
|
type: Optional
|
|
1489
|
+
}] }, { type: RecaptchaService, decorators: [{
|
|
1490
|
+
type: Optional
|
|
1042
1491
|
}] }] });
|
|
1043
1492
|
|
|
1044
1493
|
/**
|
|
@@ -1458,7 +1907,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1458
1907
|
*
|
|
1459
1908
|
* Use this for NgModule-based apps (Angular 17+ with NgModule or legacy apps).
|
|
1460
1909
|
*
|
|
1461
|
-
* @example
|
|
1910
|
+
* @example Basic Setup
|
|
1462
1911
|
* ```typescript
|
|
1463
1912
|
* // app.module.ts
|
|
1464
1913
|
* import { NAuthModule } from '@nauth-toolkit/client-angular';
|
|
@@ -1473,35 +1922,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1473
1922
|
* })
|
|
1474
1923
|
* export class AppModule {}
|
|
1475
1924
|
* ```
|
|
1925
|
+
*
|
|
1926
|
+
* @example With reCAPTCHA Enterprise
|
|
1927
|
+
* ```typescript
|
|
1928
|
+
* NAuthModule.forRoot({
|
|
1929
|
+
* baseUrl: 'http://localhost:3000/auth',
|
|
1930
|
+
* tokenDelivery: 'cookies',
|
|
1931
|
+
* recaptcha: {
|
|
1932
|
+
* enabled: true,
|
|
1933
|
+
* version: 'enterprise',
|
|
1934
|
+
* siteKey: environment.recaptchaSiteKey,
|
|
1935
|
+
* action: 'login',
|
|
1936
|
+
* },
|
|
1937
|
+
* })
|
|
1938
|
+
* ```
|
|
1476
1939
|
*/
|
|
1477
1940
|
class NAuthModule {
|
|
1478
1941
|
static forRoot(config) {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
{
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const router = inject(Router, { optional: true });
|
|
1493
|
-
return new AuthService(config, httpAdapter, router ?? undefined);
|
|
1494
|
-
},
|
|
1495
|
-
deps: [AngularHttpAdapter],
|
|
1942
|
+
const providers = [
|
|
1943
|
+
{
|
|
1944
|
+
provide: NAUTH_CLIENT_CONFIG,
|
|
1945
|
+
useValue: config,
|
|
1946
|
+
},
|
|
1947
|
+
AngularHttpAdapter,
|
|
1948
|
+
{
|
|
1949
|
+
provide: AuthService,
|
|
1950
|
+
useFactory: (httpAdapter, recaptchaService) => {
|
|
1951
|
+
// Try to inject Router optionally - if not available, pass undefined
|
|
1952
|
+
// Router will be undefined if not provided (e.g., in apps without routing)
|
|
1953
|
+
const router = inject(Router, { optional: true });
|
|
1954
|
+
return new AuthService(config, httpAdapter, router ?? undefined, recaptchaService);
|
|
1496
1955
|
},
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1956
|
+
deps: [AngularHttpAdapter, [new Optional(), RecaptchaService]],
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
provide: HTTP_INTERCEPTORS,
|
|
1960
|
+
useClass: AuthInterceptorClass,
|
|
1961
|
+
multi: true,
|
|
1962
|
+
},
|
|
1963
|
+
// Provide AuthGuard so it has access to NAUTH_CLIENT_CONFIG
|
|
1964
|
+
AuthGuard,
|
|
1965
|
+
];
|
|
1966
|
+
// Add reCAPTCHA providers if configured
|
|
1967
|
+
if (config.recaptcha?.enabled) {
|
|
1968
|
+
providers.push({
|
|
1969
|
+
provide: RECAPTCHA_CONFIG,
|
|
1970
|
+
// Cast because interface extends but they're compatible
|
|
1971
|
+
useValue: config.recaptcha,
|
|
1972
|
+
}, RecaptchaService, {
|
|
1973
|
+
provide: APP_INITIALIZER,
|
|
1974
|
+
useFactory: () => {
|
|
1975
|
+
// Force RecaptchaService instantiation at app startup so script preloads
|
|
1976
|
+
inject(RecaptchaService);
|
|
1977
|
+
// No-op - constructor already handles preload
|
|
1978
|
+
return () => { };
|
|
1501
1979
|
},
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1980
|
+
multi: true,
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
return {
|
|
1984
|
+
ngModule: NAuthModule,
|
|
1985
|
+
providers,
|
|
1505
1986
|
};
|
|
1506
1987
|
}
|
|
1507
1988
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
@@ -1557,6 +2038,7 @@ class AuthInterceptor {
|
|
|
1557
2038
|
* - If `exchangeToken` exists: exchanges it via backend (SDK handles navigation automatically).
|
|
1558
2039
|
* - If no `exchangeToken`: treat as cookie-success path (SDK handles navigation automatically).
|
|
1559
2040
|
* - If `error` exists: redirects to oauthError route.
|
|
2041
|
+
* - If auto-redirect is disabled (redirectUrls set to null): returns true to activate the route.
|
|
1560
2042
|
*
|
|
1561
2043
|
* @example
|
|
1562
2044
|
* ```typescript
|
|
@@ -1587,14 +2069,16 @@ const socialRedirectCallbackGuard = async () => {
|
|
|
1587
2069
|
if (appState) {
|
|
1588
2070
|
await auth.getClient().storeOauthState(appState);
|
|
1589
2071
|
}
|
|
1590
|
-
// Provider error: redirect to oauthError
|
|
2072
|
+
// Provider error: redirect to oauthError (or activate route if auto-redirect disabled)
|
|
1591
2073
|
if (error) {
|
|
1592
2074
|
await router.navigateToError('oauth');
|
|
1593
|
-
|
|
2075
|
+
// Return true to activate route if oauthError redirect is disabled
|
|
2076
|
+
return router.isErrorRedirectDisabled('oauth');
|
|
1594
2077
|
}
|
|
1595
2078
|
// No exchangeToken: cookie success path; hydrate then navigate to success.
|
|
1596
2079
|
//
|
|
1597
|
-
// Note: we do not "activate" the callback route to avoid
|
|
2080
|
+
// Note: When auto-redirect is enabled, we do not "activate" the callback route to avoid
|
|
2081
|
+
// consumers needing to render a page. When disabled, we activate the route.
|
|
1598
2082
|
if (!exchangeToken) {
|
|
1599
2083
|
// ============================================================================
|
|
1600
2084
|
// Cookies mode: hydrate user state before redirecting
|
|
@@ -1636,34 +2120,88 @@ const socialRedirectCallbackGuard = async () => {
|
|
|
1636
2120
|
appState: appState ?? undefined,
|
|
1637
2121
|
});
|
|
1638
2122
|
}
|
|
1639
|
-
catch (
|
|
2123
|
+
catch (error) {
|
|
1640
2124
|
// Only treat auth failures (401/403) as OAuth errors
|
|
1641
2125
|
// 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);
|
|
2126
|
+
// Type guard: check if error is NAuthClientError
|
|
2127
|
+
if (error instanceof NAuthClientError) {
|
|
2128
|
+
const isAuthError = error.statusCode === 401 ||
|
|
2129
|
+
error.statusCode === 403 ||
|
|
2130
|
+
error.code === NAuthErrorCode.AUTH_TOKEN_INVALID ||
|
|
2131
|
+
error.code === NAuthErrorCode.AUTH_SESSION_EXPIRED ||
|
|
2132
|
+
error.code === NAuthErrorCode.AUTH_SESSION_NOT_FOUND;
|
|
2133
|
+
if (isAuthError) {
|
|
2134
|
+
// Cookies weren't set properly - OAuth failed
|
|
2135
|
+
await router.navigateToError('oauth');
|
|
2136
|
+
return router.isErrorRedirectDisabled('oauth');
|
|
2137
|
+
}
|
|
1656
2138
|
}
|
|
2139
|
+
// For network errors or other non-auth issues, proceed to success route
|
|
2140
|
+
// The auth guard will handle authentication state on the next route
|
|
2141
|
+
await router.navigateToSuccess(appState ? { appState } : undefined);
|
|
1657
2142
|
}
|
|
1658
|
-
|
|
2143
|
+
// Return true if success redirect is disabled, allowing the callback component to render
|
|
2144
|
+
return router.isSuccessRedirectDisabled({ source: 'social', appState: appState ?? undefined });
|
|
1659
2145
|
}
|
|
1660
2146
|
// Exchange token - SDK handles navigation automatically
|
|
1661
2147
|
// Note: appState will be passed via query params when navigateToSuccess is called
|
|
1662
2148
|
// by the challenge router after successful exchange
|
|
1663
2149
|
await auth.exchangeSocialRedirect(exchangeToken);
|
|
1664
|
-
|
|
2150
|
+
// Return true if success redirect is disabled, allowing the callback component to render
|
|
2151
|
+
// We use 'social' as source since this is the social OAuth callback flow
|
|
2152
|
+
return router.isSuccessRedirectDisabled({ source: 'social', appState: appState ?? undefined });
|
|
1665
2153
|
};
|
|
1666
2154
|
|
|
2155
|
+
/**
|
|
2156
|
+
* Provides reCAPTCHA configuration and automatic script preloading.
|
|
2157
|
+
*
|
|
2158
|
+
* Sets up `RECAPTCHA_CONFIG` and forces `RecaptchaService` instantiation at app
|
|
2159
|
+
* startup so the reCAPTCHA script preloads before the user clicks login/signup.
|
|
2160
|
+
*
|
|
2161
|
+
* @param config - reCAPTCHA configuration (enabled, version, siteKey, action)
|
|
2162
|
+
* @returns Environment providers for reCAPTCHA
|
|
2163
|
+
*
|
|
2164
|
+
* @example
|
|
2165
|
+
* ```typescript
|
|
2166
|
+
* export const appConfig: ApplicationConfig = {
|
|
2167
|
+
* providers: [
|
|
2168
|
+
* provideRecaptcha({
|
|
2169
|
+
* enabled: true,
|
|
2170
|
+
* version: 'enterprise',
|
|
2171
|
+
* siteKey: environment.recaptchaSiteKey,
|
|
2172
|
+
* action: 'login',
|
|
2173
|
+
* }),
|
|
2174
|
+
* // ... other providers
|
|
2175
|
+
* ],
|
|
2176
|
+
* };
|
|
2177
|
+
* ```
|
|
2178
|
+
*/
|
|
2179
|
+
function provideRecaptcha(config) {
|
|
2180
|
+
return makeEnvironmentProviders([
|
|
2181
|
+
{
|
|
2182
|
+
provide: RECAPTCHA_CONFIG,
|
|
2183
|
+
useValue: config,
|
|
2184
|
+
},
|
|
2185
|
+
RecaptchaService,
|
|
2186
|
+
{
|
|
2187
|
+
provide: APP_INITIALIZER,
|
|
2188
|
+
useFactory: () => {
|
|
2189
|
+
const recaptcha = inject(RecaptchaService);
|
|
2190
|
+
// Return initialization function that ensures script starts loading
|
|
2191
|
+
return () => {
|
|
2192
|
+
// Trigger script load (fire-and-forget, don't block app startup)
|
|
2193
|
+
if (config.enabled && (config.version === 'v3' || config.version === 'enterprise')) {
|
|
2194
|
+
recaptcha.loadScript().catch(() => {
|
|
2195
|
+
// Silent fail - execute() will retry when called
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
},
|
|
2200
|
+
multi: true,
|
|
2201
|
+
},
|
|
2202
|
+
]);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1667
2205
|
/**
|
|
1668
2206
|
* Public API Surface of @nauth-toolkit/client-angular (NgModule)
|
|
1669
2207
|
*
|
|
@@ -1676,5 +2214,5 @@ const socialRedirectCallbackGuard = async () => {
|
|
|
1676
2214
|
* Generated bundle index. Do not edit.
|
|
1677
2215
|
*/
|
|
1678
2216
|
|
|
1679
|
-
export { AngularHttpAdapter, AuthGuard, AuthInterceptor, AuthInterceptorClass, AuthService, NAUTH_CLIENT_CONFIG, NAuthModule, authGuard, authInterceptor, socialRedirectCallbackGuard };
|
|
2217
|
+
export { AngularHttpAdapter, AuthGuard, AuthInterceptor, AuthInterceptorClass, AuthService, NAUTH_CLIENT_CONFIG, NAuthModule, RECAPTCHA_CONFIG, RecaptchaService, authGuard, authInterceptor, provideRecaptcha, socialRedirectCallbackGuard };
|
|
1680
2218
|
//# sourceMappingURL=nauth-toolkit-client-angular.mjs.map
|