@nauth-toolkit/client-angular 0.1.57 → 0.1.59

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.
@@ -1,13 +1,14 @@
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, NgModule, inject, PLATFORM_ID } from '@angular/core';
4
+ import { InjectionToken, Injectable, Inject, inject, Optional, NgModule, PLATFORM_ID } from '@angular/core';
5
5
  import { firstValueFrom, BehaviorSubject, Subject, catchError, from, switchMap, throwError, filter as filter$1, take } from 'rxjs';
6
6
  import { filter } from 'rxjs/operators';
7
7
  import * as i1 from '@angular/common/http';
8
8
  import { HttpErrorResponse, HTTP_INTERCEPTORS, HttpClientModule, HttpClient } from '@angular/common/http';
9
9
  import * as i3 from '@angular/router';
10
10
  import { Router } from '@angular/router';
11
+ import { __decorate, __param } from 'tslib';
11
12
  import { isPlatformBrowser } from '@angular/common';
12
13
 
13
14
  /**
@@ -40,6 +41,37 @@ class AngularHttpAdapter {
40
41
  constructor(http) {
41
42
  this.http = http;
42
43
  }
44
+ /**
45
+ * Safely parse a JSON response body.
46
+ *
47
+ * Angular's fetch backend (`withFetch()`) will throw a raw `SyntaxError` if
48
+ * `responseType: 'json'` is used and the backend returns HTML (common for
49
+ * proxies, 502 pages, SSR fallbacks, or misrouted requests).
50
+ *
51
+ * To avoid crashing consumer apps, we always request as text and then parse
52
+ * JSON only when the response actually looks like JSON.
53
+ *
54
+ * @param bodyText - Raw response body as text
55
+ * @param contentType - Content-Type header value (if available)
56
+ * @returns Parsed JSON value (unknown)
57
+ * @throws {SyntaxError} When body is non-empty but not valid JSON
58
+ */
59
+ parseJsonBody(bodyText, contentType) {
60
+ const trimmed = bodyText.trim();
61
+ if (!trimmed)
62
+ return null;
63
+ // If it's clearly HTML, never attempt JSON.parse (some proxies mislabel Content-Type).
64
+ if (trimmed.startsWith('<')) {
65
+ return bodyText;
66
+ }
67
+ const looksLikeJson = trimmed.startsWith('{') || trimmed.startsWith('[');
68
+ const isJsonContentType = typeof contentType === 'string' && contentType.toLowerCase().includes('application/json');
69
+ if (!looksLikeJson && !isJsonContentType) {
70
+ // Return raw text when it doesn't look like JSON (e.g., HTML error pages).
71
+ return bodyText;
72
+ }
73
+ return JSON.parse(trimmed);
74
+ }
43
75
  /**
44
76
  * Execute HTTP request using Angular's HttpClient.
45
77
  *
@@ -49,29 +81,40 @@ class AngularHttpAdapter {
49
81
  */
50
82
  async request(config) {
51
83
  try {
52
- // Use Angular's HttpClient - goes through ALL interceptors
53
- const data = await firstValueFrom(this.http.request(config.method, config.url, {
84
+ // Use Angular's HttpClient - goes through ALL interceptors.
85
+ // IMPORTANT: Use responseType 'text' to avoid raw JSON.parse crashes when
86
+ // the backend returns HTML (seen in some proxy/SSR/misroute setups).
87
+ const res = await firstValueFrom(this.http.request(config.method, config.url, {
54
88
  body: config.body,
55
89
  headers: config.headers,
56
90
  withCredentials: config.credentials === 'include',
57
- observe: 'body', // Only return body data
91
+ observe: 'response',
92
+ responseType: 'text',
58
93
  }));
94
+ const contentType = res.headers?.get('content-type');
95
+ const parsed = this.parseJsonBody(res.body ?? '', contentType);
59
96
  return {
60
- data,
61
- status: 200, // HttpClient only returns data on success
62
- headers: {}, // Can extract from observe: 'response' if needed
97
+ data: parsed,
98
+ status: res.status,
99
+ headers: {}, // Reserved for future header passthrough if needed
63
100
  };
64
101
  }
65
102
  catch (error) {
66
103
  if (error instanceof HttpErrorResponse) {
67
- // Convert Angular's HttpErrorResponse to NAuthClientError
68
- const errorData = error.error || {};
69
- const code = typeof errorData['code'] === 'string' ? errorData.code : NAuthErrorCode.INTERNAL_ERROR;
104
+ // Convert Angular's HttpErrorResponse to NAuthClientError.
105
+ // When using responseType 'text', `error.error` is typically a string.
106
+ const contentType = error.headers?.get('content-type') ?? null;
107
+ const rawBody = typeof error.error === 'string' ? error.error : '';
108
+ const parsedError = this.parseJsonBody(rawBody, contentType);
109
+ const errorData = typeof parsedError === 'object' && parsedError !== null ? parsedError : {};
110
+ const code = typeof errorData['code'] === 'string' ? errorData['code'] : NAuthErrorCode.INTERNAL_ERROR;
70
111
  const message = typeof errorData['message'] === 'string'
71
- ? errorData.message
72
- : error.message || `Request failed with status ${error.status}`;
73
- const timestamp = typeof errorData['timestamp'] === 'string' ? errorData.timestamp : undefined;
74
- const details = errorData['details'];
112
+ ? errorData['message']
113
+ : typeof parsedError === 'string' && parsedError.trim()
114
+ ? parsedError
115
+ : error.message || `Request failed with status ${error.status}`;
116
+ const timestamp = typeof errorData['timestamp'] === 'string' ? errorData['timestamp'] : undefined;
117
+ const details = typeof errorData['details'] === 'object' ? errorData['details'] : undefined;
75
118
  throw new NAuthClientError(code, message, {
76
119
  statusCode: error.status,
77
120
  timestamp,
@@ -79,8 +122,12 @@ class AngularHttpAdapter {
79
122
  isNetworkError: error.status === 0, // Network error (no response from server)
80
123
  });
81
124
  }
82
- // Re-throw non-HTTP errors
83
- throw error;
125
+ // Re-throw non-HTTP errors as an SDK error so consumers don't see raw parser crashes.
126
+ const message = error instanceof Error ? error.message : 'Unknown error';
127
+ throw new NAuthClientError(NAuthErrorCode.INTERNAL_ERROR, message, {
128
+ statusCode: 0,
129
+ isNetworkError: true,
130
+ });
84
131
  }
85
132
  }
86
133
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AngularHttpAdapter, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
@@ -220,6 +267,21 @@ class AuthService {
220
267
  getCurrentChallenge() {
221
268
  return this.challengeSubject.value;
222
269
  }
270
+ /**
271
+ * Get challenge router for manual navigation control.
272
+ * Useful for guards that need to handle errors or build custom URLs.
273
+ *
274
+ * @returns ChallengeRouter instance
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * const router = this.auth.getChallengeRouter();
279
+ * await router.navigateToError('oauth');
280
+ * ```
281
+ */
282
+ getChallengeRouter() {
283
+ return this.client.getChallengeRouter();
284
+ }
223
285
  // ============================================================================
224
286
  // Core Auth Methods
225
287
  // ============================================================================
@@ -962,6 +1024,118 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
962
1024
  args: [NAUTH_CLIENT_CONFIG]
963
1025
  }] }, { type: i1.HttpClient }, { type: AuthService }, { type: i3.Router }] });
964
1026
 
1027
+ /**
1028
+ * Functional route guard for authentication (Angular 17+).
1029
+ *
1030
+ * Protects routes by checking if user is authenticated.
1031
+ * Redirects to configured session expired route (or login) if not authenticated.
1032
+ *
1033
+ * @param redirectTo - Optional path to redirect to if not authenticated. If not provided, uses `redirects.sessionExpired` from config (defaults to '/login')
1034
+ * @returns CanActivateFn guard function
1035
+ *
1036
+ * @example
1037
+ * ```typescript
1038
+ * // In route configuration - uses config.redirects.sessionExpired
1039
+ * const routes: Routes = [
1040
+ * {
1041
+ * path: 'home',
1042
+ * component: HomeComponent,
1043
+ * canActivate: [authGuard()]
1044
+ * }
1045
+ * ];
1046
+ *
1047
+ * // Override with custom route
1048
+ * const routes: Routes = [
1049
+ * {
1050
+ * path: 'admin',
1051
+ * component: AdminComponent,
1052
+ * canActivate: [authGuard('/admin/login')]
1053
+ * }
1054
+ * ];
1055
+ * ```
1056
+ */
1057
+ function authGuard(redirectTo) {
1058
+ return () => {
1059
+ const auth = inject(AuthService);
1060
+ const router = inject(Router);
1061
+ const config = inject(NAUTH_CLIENT_CONFIG, { optional: true });
1062
+ if (auth.isAuthenticated()) {
1063
+ return true;
1064
+ }
1065
+ // Use provided redirectTo, or config.redirects.sessionExpired, or default to '/login'
1066
+ const redirectPath = redirectTo ?? config?.redirects?.sessionExpired ?? '/login';
1067
+ return router.createUrlTree([redirectPath]);
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Class-based authentication guard for NgModule compatibility.
1072
+ *
1073
+ * **Note:** When using `NAuthModule.forRoot()`, `AuthGuard` is automatically provided
1074
+ * and has access to the configuration. You don't need to add it to your module's providers.
1075
+ *
1076
+ * @example
1077
+ * ```typescript
1078
+ * // app.module.ts - AuthGuard is automatically provided by NAuthModule.forRoot()
1079
+ * @NgModule({
1080
+ * imports: [
1081
+ * NAuthModule.forRoot({
1082
+ * baseUrl: 'https://api.example.com/auth',
1083
+ * tokenDelivery: 'cookies',
1084
+ * redirects: {
1085
+ * sessionExpired: '/login?expired=true',
1086
+ * },
1087
+ * }),
1088
+ * RouterModule.forRoot([
1089
+ * {
1090
+ * path: 'home',
1091
+ * component: HomeComponent,
1092
+ * canActivate: [AuthGuard], // Uses config.redirects.sessionExpired
1093
+ * },
1094
+ * ]),
1095
+ * ],
1096
+ * })
1097
+ * export class AppModule {}
1098
+ *
1099
+ * // Or provide manually in a feature module (still has access to root config)
1100
+ * @NgModule({
1101
+ * providers: [AuthGuard],
1102
+ * })
1103
+ * export class FeatureModule {}
1104
+ * ```
1105
+ */
1106
+ let AuthGuard = class AuthGuard {
1107
+ auth;
1108
+ router;
1109
+ config;
1110
+ /**
1111
+ * @param auth - Authentication service
1112
+ * @param router - Angular router
1113
+ * @param config - Optional client configuration (injected automatically)
1114
+ */
1115
+ constructor(auth, router, config) {
1116
+ this.auth = auth;
1117
+ this.router = router;
1118
+ this.config = config;
1119
+ }
1120
+ /**
1121
+ * Check if route can be activated.
1122
+ *
1123
+ * @returns True if authenticated, otherwise redirects to configured session expired route (or '/login')
1124
+ */
1125
+ canActivate() {
1126
+ if (this.auth.isAuthenticated()) {
1127
+ return true;
1128
+ }
1129
+ // Use config.redirects.sessionExpired or default to '/login'
1130
+ const redirectPath = this.config?.redirects?.sessionExpired ?? '/login';
1131
+ return this.router.createUrlTree([redirectPath]);
1132
+ }
1133
+ };
1134
+ AuthGuard = __decorate([
1135
+ __param(2, Optional()),
1136
+ __param(2, Inject(NAUTH_CLIENT_CONFIG))
1137
+ ], AuthGuard);
1138
+
965
1139
  /**
966
1140
  * NgModule for nauth-toolkit Angular integration.
967
1141
  *
@@ -1005,6 +1179,8 @@ class NAuthModule {
1005
1179
  useClass: AuthInterceptorClass,
1006
1180
  multi: true,
1007
1181
  },
1182
+ // Provide AuthGuard so it has access to NAUTH_CLIENT_CONFIG
1183
+ AuthGuard,
1008
1184
  ],
1009
1185
  };
1010
1186
  }
@@ -1171,86 +1347,6 @@ class AuthInterceptor {
1171
1347
  }
1172
1348
  }
1173
1349
 
1174
- /**
1175
- * Functional route guard for authentication (Angular 17+).
1176
- *
1177
- * Protects routes by checking if user is authenticated.
1178
- * Redirects to login page if not authenticated.
1179
- *
1180
- * @param redirectTo - Path to redirect to if not authenticated (default: '/login')
1181
- * @returns CanActivateFn guard function
1182
- *
1183
- * @example
1184
- * ```typescript
1185
- * // In route configuration
1186
- * const routes: Routes = [
1187
- * {
1188
- * path: 'home',
1189
- * component: HomeComponent,
1190
- * canActivate: [authGuard()]
1191
- * },
1192
- * {
1193
- * path: 'admin',
1194
- * component: AdminComponent,
1195
- * canActivate: [authGuard('/admin/login')]
1196
- * }
1197
- * ];
1198
- * ```
1199
- */
1200
- function authGuard(redirectTo = '/login') {
1201
- return () => {
1202
- const auth = inject(AuthService);
1203
- const router = inject(Router);
1204
- if (auth.isAuthenticated()) {
1205
- return true;
1206
- }
1207
- return router.createUrlTree([redirectTo]);
1208
- };
1209
- }
1210
- /**
1211
- * Class-based authentication guard for NgModule compatibility.
1212
- *
1213
- * @example
1214
- * ```typescript
1215
- * // In route configuration (NgModule)
1216
- * const routes: Routes = [
1217
- * {
1218
- * path: 'home',
1219
- * component: HomeComponent,
1220
- * canActivate: [AuthGuard]
1221
- * }
1222
- * ];
1223
- *
1224
- * // In module providers
1225
- * @NgModule({
1226
- * providers: [AuthGuard]
1227
- * })
1228
- * ```
1229
- */
1230
- class AuthGuard {
1231
- auth;
1232
- router;
1233
- /**
1234
- * @param auth - Authentication service
1235
- * @param router - Angular router
1236
- */
1237
- constructor(auth, router) {
1238
- this.auth = auth;
1239
- this.router = router;
1240
- }
1241
- /**
1242
- * Check if route can be activated.
1243
- *
1244
- * @returns True if authenticated, otherwise redirects to login
1245
- */
1246
- canActivate() {
1247
- if (this.auth.isAuthenticated()) {
1248
- return true;
1249
- }
1250
- return this.router.createUrlTree(['/login']);
1251
- }
1252
- }
1253
-
1254
1350
  /**
1255
1351
  * Social redirect callback route guard.
1256
1352
  *
@@ -1261,8 +1357,8 @@ class AuthGuard {
1261
1357
  * - `error` / `error_description` (provider errors)
1262
1358
  *
1263
1359
  * Behavior:
1264
- * - If `exchangeToken` exists: exchanges it via backend and redirects to success or challenge routes.
1265
- * - If no `exchangeToken`: treat as cookie-success path and redirect to success route.
1360
+ * - If `exchangeToken` exists: exchanges it via backend (SDK handles navigation automatically).
1361
+ * - If no `exchangeToken`: treat as cookie-success path (SDK handles navigation automatically).
1266
1362
  * - If `error` exists: redirects to oauthError route.
1267
1363
  *
1268
1364
  * @example
@@ -1276,7 +1372,6 @@ class AuthGuard {
1276
1372
  */
1277
1373
  const socialRedirectCallbackGuard = async () => {
1278
1374
  const auth = inject(AuthService);
1279
- const config = inject(NAUTH_CLIENT_CONFIG);
1280
1375
  const platformId = inject(PLATFORM_ID);
1281
1376
  const isBrowser = isPlatformBrowser(platformId);
1282
1377
  if (!isBrowser) {
@@ -1285,13 +1380,13 @@ const socialRedirectCallbackGuard = async () => {
1285
1380
  const params = new URLSearchParams(window.location.search);
1286
1381
  const error = params.get('error');
1287
1382
  const exchangeToken = params.get('exchangeToken');
1383
+ const router = auth.getChallengeRouter();
1288
1384
  // Provider error: redirect to oauthError
1289
1385
  if (error) {
1290
- const errorUrl = config.redirects?.oauthError || '/login';
1291
- window.location.replace(errorUrl);
1386
+ await router.navigateToError('oauth');
1292
1387
  return false;
1293
1388
  }
1294
- // No exchangeToken: cookie success path; redirect to success.
1389
+ // No exchangeToken: cookie success path; hydrate then navigate to success.
1295
1390
  //
1296
1391
  // Note: we do not "activate" the callback route to avoid consumers needing to render a page.
1297
1392
  if (!exchangeToken) {
@@ -1305,26 +1400,31 @@ const socialRedirectCallbackGuard = async () => {
1305
1400
  // `currentUser` is still null even though cookies were set successfully.
1306
1401
  try {
1307
1402
  await auth.getProfile();
1403
+ await router.navigateToSuccess();
1308
1404
  }
1309
- catch {
1310
- const errorUrl = config.redirects?.oauthError || '/login';
1311
- window.location.replace(errorUrl);
1312
- return false;
1405
+ catch (err) {
1406
+ // Only treat auth failures (401/403) as OAuth errors
1407
+ // Network errors or other issues might be temporary - still try success route
1408
+ const isAuthError = err instanceof NAuthClientError &&
1409
+ (err.statusCode === 401 ||
1410
+ err.statusCode === 403 ||
1411
+ err.code === NAuthErrorCode.AUTH_TOKEN_INVALID ||
1412
+ err.code === NAuthErrorCode.AUTH_SESSION_EXPIRED ||
1413
+ err.code === NAuthErrorCode.AUTH_SESSION_NOT_FOUND);
1414
+ if (isAuthError) {
1415
+ // Cookies weren't set properly - OAuth failed
1416
+ await router.navigateToError('oauth');
1417
+ }
1418
+ else {
1419
+ // For network errors or other issues, proceed to success route
1420
+ // The auth guard will handle authentication state on the next route
1421
+ await router.navigateToSuccess();
1422
+ }
1313
1423
  }
1314
- const successUrl = config.redirects?.success || '/';
1315
- window.location.replace(successUrl);
1316
- return false;
1317
- }
1318
- // Exchange token and route accordingly
1319
- const response = await auth.exchangeSocialRedirect(exchangeToken);
1320
- if (response.challengeName) {
1321
- const challengeBase = config.redirects?.challengeBase || '/auth/challenge';
1322
- const challengeRoute = response.challengeName.toLowerCase().replace(/_/g, '-');
1323
- window.location.replace(`${challengeBase}/${challengeRoute}`);
1324
1424
  return false;
1325
1425
  }
1326
- const successUrl = config.redirects?.success || '/';
1327
- window.location.replace(successUrl);
1426
+ // Exchange token - SDK handles navigation automatically
1427
+ await auth.exchangeSocialRedirect(exchangeToken);
1328
1428
  return false;
1329
1429
  };
1330
1430