@codex-ts/core-lib 1.0.16 → 1.0.18

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,14 +1,14 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, PLATFORM_ID, signal, computed, Injectable, importProvidersFrom, Component, NgModule, Directive, Input, ViewChild, Pipe } from '@angular/core';
2
+ import { InjectionToken, inject, PLATFORM_ID, signal, computed, Injectable, importProvidersFrom, APP_INITIALIZER, Component, NgModule, Directive, Input, ViewChild, Pipe } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
- import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http';
4
+ import { HttpClient, HTTP_INTERCEPTORS, HttpClientModule, HttpParams } from '@angular/common/http';
5
5
  import * as i1$2 from '@angular/common';
6
6
  import { isPlatformBrowser, CommonModule, formatDate } from '@angular/common';
7
+ import { from, mergeMap, catchError, throwError, map, BehaviorSubject, finalize, Observable, forkJoin, of, isObservable, Subject, take as take$1, tap as tap$1, takeUntil as takeUntil$1, ReplaySubject, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
7
8
  import * as i1$3 from '@angular/router';
8
9
  import { Router } from '@angular/router';
9
10
  import { toObservable } from '@angular/core/rxjs-interop';
10
- import { map, BehaviorSubject, finalize, Observable, forkJoin, throwError, of, isObservable, Subject, take as take$1, tap as tap$1, catchError as catchError$1, takeUntil as takeUntil$1, ReplaySubject, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
11
- import { retry, timeout, catchError, take, map as map$1, finalize as finalize$1, switchMap, takeUntil, mergeMap, tap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
11
+ import { retry, timeout, catchError as catchError$1, take, map as map$1, finalize as finalize$1, switchMap, takeUntil, mergeMap as mergeMap$1, tap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
12
12
  import * as i4 from 'primeng/api';
13
13
  import * as i1$1 from '@angular/forms';
14
14
  import { FormGroup, ReactiveFormsModule } from '@angular/forms';
@@ -138,6 +138,28 @@ var TaskRole;
138
138
 
139
139
  const KEYCLOAK_CONFIG = new InjectionToken('KEYCLOAK_CONFIG');
140
140
 
141
+ /** Keycloak error types */
142
+ var KeycloakErrorType;
143
+ (function (KeycloakErrorType) {
144
+ KeycloakErrorType["SCRIPT_LOAD"] = "SCRIPT_LOAD_ERROR";
145
+ KeycloakErrorType["INIT"] = "INIT_ERROR";
146
+ KeycloakErrorType["TOKEN_REFRESH"] = "TOKEN_REFRESH_ERROR";
147
+ KeycloakErrorType["PROFILE_LOAD"] = "PROFILE_LOAD_ERROR";
148
+ })(KeycloakErrorType || (KeycloakErrorType = {}));
149
+ /** Keycloak error with type and details */
150
+ class KeycloakError extends Error {
151
+ type;
152
+ originalError;
153
+ constructor(type, message, originalError) {
154
+ super(message);
155
+ this.type = type;
156
+ this.originalError = originalError;
157
+ this.name = 'KeycloakError';
158
+ }
159
+ }
160
+ /**
161
+ * Service for handling Keycloak authentication and authorization
162
+ */
141
163
  class KeycloakService {
142
164
  http = inject(HttpClient);
143
165
  platformId = inject(PLATFORM_ID);
@@ -148,6 +170,22 @@ class KeycloakService {
148
170
  #userProfile = signal(null);
149
171
  isAuthenticated$ = computed(() => this.#isAuthenticated());
150
172
  userProfile$ = computed(() => this.#userProfile());
173
+ /** Check if URL should be excluded from bearer token */
174
+ isUrlExcluded(url) {
175
+ return this.config.bearerExcludedUrls?.some(excluded => url.startsWith(excluded) || new RegExp(excluded).test(url)) ?? false;
176
+ }
177
+ /** Validate Keycloak configuration */
178
+ validateConfig() {
179
+ if (!this.config.authServerUrl) {
180
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Auth server URL is required');
181
+ }
182
+ if (!this.config.realm) {
183
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Realm is required');
184
+ }
185
+ if (!this.config.clientId) {
186
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Client ID is required');
187
+ }
188
+ }
151
189
  loadKeycloakScript() {
152
190
  if (!this.isBrowser)
153
191
  return Promise.resolve();
@@ -165,19 +203,30 @@ class KeycloakService {
165
203
  getSilentCheckUrl() {
166
204
  if (!this.isBrowser)
167
205
  return '';
168
- return `${window.location.origin}/node_modules/@codex-ts/core-lib/assets/keycloak/silent-check-sso.html`;
206
+ // Use the configured URL or fallback to a default location
207
+ return this.config.silentCheckSsoRedirectUri ??
208
+ `${window.location.origin}/assets/silent-check-sso.html`;
169
209
  }
210
+ /**
211
+ * Initialize Keycloak authentication
212
+ * @returns Promise resolving to true if initialization is successful
213
+ * @throws KeycloakError if initialization fails
214
+ */
170
215
  async initialize() {
171
216
  if (!this.isBrowser)
172
217
  return false;
173
218
  try {
219
+ this.validateConfig();
174
220
  await this.loadKeycloakScript();
175
221
  const keycloak = new Keycloak(this.config);
176
222
  this.#keycloakInstance.set(keycloak);
177
- const authenticated = await keycloak.init({
178
- onLoad: 'check-sso',
179
- silentCheckSsoRedirectUri: this.getSilentCheckUrl()
180
- });
223
+ const initOptions = {
224
+ onLoad: 'login-required',
225
+ silentCheckSsoRedirectUri: this.getSilentCheckUrl(),
226
+ checkLoginIframe: false,
227
+ ...this.config.initOptions
228
+ };
229
+ const authenticated = await keycloak.init(initOptions);
181
230
  this.#isAuthenticated.set(authenticated);
182
231
  if (authenticated) {
183
232
  await this.loadUserProfile();
@@ -185,45 +234,78 @@ class KeycloakService {
185
234
  return authenticated;
186
235
  }
187
236
  catch (error) {
188
- console.error('Failed to initialize Keycloak:', error);
189
- return false;
237
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Failed to initialize Keycloak', error);
190
238
  }
191
239
  }
240
+ /**
241
+ * Redirect to Keycloak login page
242
+ * @param options Additional login options
243
+ * @returns Promise resolving when login is complete
244
+ */
192
245
  login(options = {}) {
246
+ const instance = this.#keycloakInstance();
247
+ if (!instance) {
248
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Keycloak not initialized');
249
+ }
193
250
  const loginOptions = {
194
251
  ...this.config.loginOptions,
195
252
  ...options
196
253
  };
197
- return this.#keycloakInstance()?.login(loginOptions);
254
+ return instance.login(loginOptions);
198
255
  }
256
+ /**
257
+ * Logout the current user
258
+ * @returns Promise resolving when logout is complete
259
+ */
199
260
  logout() {
200
- return this.#keycloakInstance()?.logout();
261
+ const instance = this.#keycloakInstance();
262
+ if (!instance) {
263
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Keycloak not initialized');
264
+ }
265
+ return instance.logout();
201
266
  }
267
+ /**
268
+ * Get the current access token
269
+ * @returns Promise resolving to the access token or null
270
+ */
202
271
  async getToken() {
203
272
  try {
204
273
  await this.updateToken(this.config.tokenValidityInSeconds || 60);
205
- return this.#keycloakInstance()?.token;
274
+ return this.#keycloakInstance()?.token ?? null;
206
275
  }
207
276
  catch (error) {
208
- console.error('Failed to get token:', error);
209
- return null;
277
+ throw new KeycloakError(KeycloakErrorType.TOKEN_REFRESH, 'Failed to get token', error);
210
278
  }
211
279
  }
280
+ /**
281
+ * Get the current user's roles
282
+ * @returns Array of role names
283
+ */
212
284
  getRoles() {
213
285
  const keycloak = this.#keycloakInstance();
214
- if (!keycloak?.realmAccess?.roles) {
215
- return [];
286
+ if (!keycloak) {
287
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Keycloak not initialized');
216
288
  }
217
- return keycloak.realmAccess.roles;
289
+ return keycloak.realmAccess?.roles ?? [];
218
290
  }
291
+ /**
292
+ * Check if the current user has a specific role
293
+ * @param role Role name to check
294
+ * @returns true if user has the role
295
+ */
219
296
  hasRole(role) {
220
297
  return this.getRoles().includes(role);
221
298
  }
299
+ /**
300
+ * Update the access token if it's close to expiring
301
+ * @param minValidity Minimum validity time in seconds
302
+ * @returns Promise resolving to true if token was refreshed
303
+ */
222
304
  updateToken(minValidity = 60) {
223
305
  return new Promise((resolve, reject) => {
224
306
  const keycloak = this.#keycloakInstance();
225
307
  if (!keycloak?.token) {
226
- resolve(false);
308
+ reject(new KeycloakError(KeycloakErrorType.TOKEN_REFRESH, 'No token available'));
227
309
  return;
228
310
  }
229
311
  keycloak.updateToken(minValidity)
@@ -231,8 +313,7 @@ class KeycloakService {
231
313
  resolve(refreshed);
232
314
  })
233
315
  .catch((error) => {
234
- console.error('Failed to update token:', error);
235
- reject(error);
316
+ reject(new KeycloakError(KeycloakErrorType.TOKEN_REFRESH, 'Failed to refresh token', error));
236
317
  });
237
318
  });
238
319
  }
@@ -256,6 +337,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImpor
256
337
  args: [{ providedIn: 'root' }]
257
338
  }] });
258
339
 
340
+ class KeycloakAuthInterceptor {
341
+ keycloakService;
342
+ constructor(keycloakService) {
343
+ this.keycloakService = keycloakService;
344
+ }
345
+ intercept(req, next) {
346
+ // Skip bearer token for excluded URLs
347
+ if (this.keycloakService.isUrlExcluded(req.url)) {
348
+ return next.handle(req);
349
+ }
350
+ return from(this.keycloakService.getToken()).pipe(mergeMap(token => {
351
+ if (token) {
352
+ const authReq = req.clone({
353
+ headers: req.headers.set('Authorization', `Bearer ${token}`)
354
+ });
355
+ return next.handle(authReq);
356
+ }
357
+ return next.handle(req);
358
+ }), catchError((error) => {
359
+ // Handle 401/403 errors by refreshing token or redirecting to login
360
+ if (error.status === 401 || error.status === 403) {
361
+ return from(this.keycloakService.updateToken()).pipe(mergeMap(refreshed => {
362
+ if (refreshed) {
363
+ return from(this.keycloakService.getToken()).pipe(mergeMap(newToken => {
364
+ const authReq = req.clone({
365
+ headers: req.headers.set('Authorization', `Bearer ${newToken}`)
366
+ });
367
+ return next.handle(authReq);
368
+ }));
369
+ }
370
+ else {
371
+ // Token refresh failed, redirect to login
372
+ this.keycloakService.login();
373
+ return throwError(() => error);
374
+ }
375
+ }));
376
+ }
377
+ return throwError(() => error);
378
+ }));
379
+ }
380
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: KeycloakAuthInterceptor, deps: [{ token: KeycloakService }], target: i0.ɵɵFactoryTarget.Injectable });
381
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: KeycloakAuthInterceptor });
382
+ }
383
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: KeycloakAuthInterceptor, decorators: [{
384
+ type: Injectable
385
+ }], ctorParameters: () => [{ type: KeycloakService }] });
386
+
387
+ function initializeKeycloak(keycloak) {
388
+ return () => keycloak.initialize();
389
+ }
390
+ /**
391
+ * Provides Keycloak authentication and authorization services
392
+ * @param config Keycloak configuration
393
+ * @returns Array of providers for Keycloak integration
394
+ */
259
395
  function provideKeycloak(config) {
260
396
  return [
261
397
  importProvidersFrom(HttpClientModule),
@@ -263,7 +399,18 @@ function provideKeycloak(config) {
263
399
  provide: KEYCLOAK_CONFIG,
264
400
  useValue: config
265
401
  },
266
- KeycloakService
402
+ KeycloakService,
403
+ {
404
+ provide: APP_INITIALIZER,
405
+ useFactory: initializeKeycloak,
406
+ multi: true,
407
+ deps: [KeycloakService]
408
+ },
409
+ {
410
+ provide: HTTP_INTERCEPTORS,
411
+ useClass: KeycloakAuthInterceptor,
412
+ multi: true
413
+ }
267
414
  ];
268
415
  }
269
416
 
@@ -334,7 +481,7 @@ class HttpUtilityService {
334
481
  if (options?.timeoutMs) {
335
482
  request$ = request$.pipe(timeout(options.timeoutMs));
336
483
  }
337
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
484
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
338
485
  }
339
486
  /**
340
487
  * Perform a POST request with enhanced options
@@ -357,7 +504,7 @@ class HttpUtilityService {
357
504
  if (options?.timeoutMs) {
358
505
  request$ = request$.pipe(timeout(options.timeoutMs));
359
506
  }
360
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
507
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
361
508
  }
362
509
  /**
363
510
  * Perform a PUT request with enhanced options
@@ -380,7 +527,7 @@ class HttpUtilityService {
380
527
  if (options?.timeoutMs) {
381
528
  request$ = request$.pipe(timeout(options.timeoutMs));
382
529
  }
383
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
530
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
384
531
  }
385
532
  /**
386
533
  * Perform a DELETE request with enhanced options
@@ -402,7 +549,7 @@ class HttpUtilityService {
402
549
  if (options?.timeoutMs) {
403
550
  request$ = request$.pipe(timeout(options.timeoutMs));
404
551
  }
405
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
552
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
406
553
  }
407
554
  /**
408
555
  * Upload a file with optional additional data
@@ -465,7 +612,7 @@ class HttpUtilityService {
465
612
  });
466
613
  }
467
614
  // Standard request without progress tracking
468
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
615
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
469
616
  }
470
617
  /**
471
618
  * Execute multiple HTTP requests in parallel
@@ -474,7 +621,7 @@ class HttpUtilityService {
474
621
  */
475
622
  batchRequests(requests) {
476
623
  this.loading.next(true);
477
- return forkJoin(requests).pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
624
+ return forkJoin(requests).pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
478
625
  }
479
626
  /**
480
627
  * Download a file from the server
@@ -493,7 +640,7 @@ class HttpUtilityService {
493
640
  if (options?.timeoutMs) {
494
641
  request$ = request$.pipe(timeout(options.timeoutMs));
495
642
  }
496
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
643
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
497
644
  }
498
645
  /**
499
646
  * Create parameter string from an object
@@ -1814,7 +1961,7 @@ class BaseFormComponent extends BasePageComponent {
1814
1961
  }));
1815
1962
  }
1816
1963
  loadFormData() {
1817
- return this.route.queryParams.pipe(take(1), mergeMap(params => {
1964
+ return this.route.queryParams.pipe(take(1), mergeMap$1(params => {
1818
1965
  const state = this.getRouterState();
1819
1966
  if (this.state.mode === AppConstants.PAGE_MODE.CREATE) {
1820
1967
  return of(void 0);
@@ -2407,7 +2554,7 @@ class BaseTabbedFormComponent extends BasePageComponent {
2407
2554
  activeTab: tabs[0]?.value || ''
2408
2555
  };
2409
2556
  // Then initialize form
2410
- this.route.queryParams.pipe(take$1(1), tap$1(() => this.state = { ...this.state, loading: true }), tap$1(params => this.initializeForm(params)), map(() => void 0), catchError$1(error => {
2557
+ this.route.queryParams.pipe(take$1(1), tap$1(() => this.state = { ...this.state, loading: true }), tap$1(params => this.initializeForm(params)), map(() => void 0), catchError(error => {
2411
2558
  console.error('Error initializing form:', error);
2412
2559
  this.showError(AppMessages.FORM.ERROR.INIT);
2413
2560
  return of(void 0);
@@ -2469,7 +2616,7 @@ class BaseTabbedFormComponent extends BasePageComponent {
2469
2616
  throw new Error(`UUID is required for ${this.state.mode} mode when no formData is provided`);
2470
2617
  }
2471
2618
  return uuid;
2472
- }), tap$1(() => this.state = { ...this.state, loading: true }), tap$1(uuid => this.loadEntity(uuid)), catchError$1(error => {
2619
+ }), tap$1(() => this.state = { ...this.state, loading: true }), tap$1(uuid => this.loadEntity(uuid)), catchError(error => {
2473
2620
  console.error('Error loading UUID:', error);
2474
2621
  this.showError(`${AppMessages.FORM.ERROR.LOAD} UUID`);
2475
2622
  return of(void 0);
@@ -2670,7 +2817,7 @@ class BaseListTabbedFormComponent extends BasePageComponent {
2670
2817
  this.setupRouteParamsSubscription();
2671
2818
  }
2672
2819
  initializeFormFromQueryParams() {
2673
- this.route.queryParams.pipe(take$1(1), catchError$1(error => {
2820
+ this.route.queryParams.pipe(take$1(1), catchError(error => {
2674
2821
  console.error('Error initializing form:', error);
2675
2822
  this.showError(AppMessages.FORM.ERROR.INIT);
2676
2823
  throw error;
@@ -3120,5 +3267,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImpor
3120
3267
  * Generated bundle index. Do not edit.
3121
3268
  */
3122
3269
 
3123
- export { AppConstants, AppMessages, BaseCrudService, BaseFormComponent, BaseListPageComponent, BaseListTabbedFormComponent, BasePageComponent, BaseTabbedFormComponent, BaseTaskDashboardComponent, BaseTaskDetailsComponent, BaseTaskService, ComponentContext, CoreFormlyModule, FormService, FormlyConfigService, HttpUtilityService, IndianDatePipe, KEYCLOAK_CONFIG, KeycloakAuthGuard, KeycloakService, NotificationService, TabbedFormType, TaskRole, UtilsService, provideKeycloak };
3270
+ export { AppConstants, AppMessages, BaseCrudService, BaseFormComponent, BaseListPageComponent, BaseListTabbedFormComponent, BasePageComponent, BaseTabbedFormComponent, BaseTaskDashboardComponent, BaseTaskDetailsComponent, BaseTaskService, ComponentContext, CoreFormlyModule, FormService, FormlyConfigService, HttpUtilityService, IndianDatePipe, KEYCLOAK_CONFIG, KeycloakAuthGuard, KeycloakError, KeycloakErrorType, KeycloakService, NotificationService, TabbedFormType, TaskRole, UtilsService, provideKeycloak };
3124
3271
  //# sourceMappingURL=codex-ts-core-lib.mjs.map