@codex-ts/core-lib 1.0.16 → 1.0.17

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();
@@ -167,17 +205,26 @@ class KeycloakService {
167
205
  return '';
168
206
  return `${window.location.origin}/node_modules/@codex-ts/core-lib/assets/keycloak/silent-check-sso.html`;
169
207
  }
208
+ /**
209
+ * Initialize Keycloak authentication
210
+ * @returns Promise resolving to true if initialization is successful
211
+ * @throws KeycloakError if initialization fails
212
+ */
170
213
  async initialize() {
171
214
  if (!this.isBrowser)
172
215
  return false;
173
216
  try {
217
+ this.validateConfig();
174
218
  await this.loadKeycloakScript();
175
219
  const keycloak = new Keycloak(this.config);
176
220
  this.#keycloakInstance.set(keycloak);
177
- const authenticated = await keycloak.init({
178
- onLoad: 'check-sso',
179
- silentCheckSsoRedirectUri: this.getSilentCheckUrl()
180
- });
221
+ const initOptions = {
222
+ onLoad: 'login-required',
223
+ silentCheckSsoRedirectUri: this.getSilentCheckUrl(),
224
+ checkLoginIframe: false,
225
+ ...this.config.initOptions
226
+ };
227
+ const authenticated = await keycloak.init(initOptions);
181
228
  this.#isAuthenticated.set(authenticated);
182
229
  if (authenticated) {
183
230
  await this.loadUserProfile();
@@ -185,45 +232,78 @@ class KeycloakService {
185
232
  return authenticated;
186
233
  }
187
234
  catch (error) {
188
- console.error('Failed to initialize Keycloak:', error);
189
- return false;
235
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Failed to initialize Keycloak', error);
190
236
  }
191
237
  }
238
+ /**
239
+ * Redirect to Keycloak login page
240
+ * @param options Additional login options
241
+ * @returns Promise resolving when login is complete
242
+ */
192
243
  login(options = {}) {
244
+ const instance = this.#keycloakInstance();
245
+ if (!instance) {
246
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Keycloak not initialized');
247
+ }
193
248
  const loginOptions = {
194
249
  ...this.config.loginOptions,
195
250
  ...options
196
251
  };
197
- return this.#keycloakInstance()?.login(loginOptions);
252
+ return instance.login(loginOptions);
198
253
  }
254
+ /**
255
+ * Logout the current user
256
+ * @returns Promise resolving when logout is complete
257
+ */
199
258
  logout() {
200
- return this.#keycloakInstance()?.logout();
259
+ const instance = this.#keycloakInstance();
260
+ if (!instance) {
261
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Keycloak not initialized');
262
+ }
263
+ return instance.logout();
201
264
  }
265
+ /**
266
+ * Get the current access token
267
+ * @returns Promise resolving to the access token or null
268
+ */
202
269
  async getToken() {
203
270
  try {
204
271
  await this.updateToken(this.config.tokenValidityInSeconds || 60);
205
- return this.#keycloakInstance()?.token;
272
+ return this.#keycloakInstance()?.token ?? null;
206
273
  }
207
274
  catch (error) {
208
- console.error('Failed to get token:', error);
209
- return null;
275
+ throw new KeycloakError(KeycloakErrorType.TOKEN_REFRESH, 'Failed to get token', error);
210
276
  }
211
277
  }
278
+ /**
279
+ * Get the current user's roles
280
+ * @returns Array of role names
281
+ */
212
282
  getRoles() {
213
283
  const keycloak = this.#keycloakInstance();
214
- if (!keycloak?.realmAccess?.roles) {
215
- return [];
284
+ if (!keycloak) {
285
+ throw new KeycloakError(KeycloakErrorType.INIT, 'Keycloak not initialized');
216
286
  }
217
- return keycloak.realmAccess.roles;
287
+ return keycloak.realmAccess?.roles ?? [];
218
288
  }
289
+ /**
290
+ * Check if the current user has a specific role
291
+ * @param role Role name to check
292
+ * @returns true if user has the role
293
+ */
219
294
  hasRole(role) {
220
295
  return this.getRoles().includes(role);
221
296
  }
297
+ /**
298
+ * Update the access token if it's close to expiring
299
+ * @param minValidity Minimum validity time in seconds
300
+ * @returns Promise resolving to true if token was refreshed
301
+ */
222
302
  updateToken(minValidity = 60) {
223
303
  return new Promise((resolve, reject) => {
224
304
  const keycloak = this.#keycloakInstance();
225
305
  if (!keycloak?.token) {
226
- resolve(false);
306
+ reject(new KeycloakError(KeycloakErrorType.TOKEN_REFRESH, 'No token available'));
227
307
  return;
228
308
  }
229
309
  keycloak.updateToken(minValidity)
@@ -231,8 +311,7 @@ class KeycloakService {
231
311
  resolve(refreshed);
232
312
  })
233
313
  .catch((error) => {
234
- console.error('Failed to update token:', error);
235
- reject(error);
314
+ reject(new KeycloakError(KeycloakErrorType.TOKEN_REFRESH, 'Failed to refresh token', error));
236
315
  });
237
316
  });
238
317
  }
@@ -256,6 +335,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImpor
256
335
  args: [{ providedIn: 'root' }]
257
336
  }] });
258
337
 
338
+ class KeycloakAuthInterceptor {
339
+ keycloakService;
340
+ constructor(keycloakService) {
341
+ this.keycloakService = keycloakService;
342
+ }
343
+ intercept(req, next) {
344
+ // Skip bearer token for excluded URLs
345
+ if (this.keycloakService.isUrlExcluded(req.url)) {
346
+ return next.handle(req);
347
+ }
348
+ return from(this.keycloakService.getToken()).pipe(mergeMap(token => {
349
+ if (token) {
350
+ const authReq = req.clone({
351
+ headers: req.headers.set('Authorization', `Bearer ${token}`)
352
+ });
353
+ return next.handle(authReq);
354
+ }
355
+ return next.handle(req);
356
+ }), catchError((error) => {
357
+ // Handle 401/403 errors by refreshing token or redirecting to login
358
+ if (error.status === 401 || error.status === 403) {
359
+ return from(this.keycloakService.updateToken()).pipe(mergeMap(refreshed => {
360
+ if (refreshed) {
361
+ return from(this.keycloakService.getToken()).pipe(mergeMap(newToken => {
362
+ const authReq = req.clone({
363
+ headers: req.headers.set('Authorization', `Bearer ${newToken}`)
364
+ });
365
+ return next.handle(authReq);
366
+ }));
367
+ }
368
+ else {
369
+ // Token refresh failed, redirect to login
370
+ this.keycloakService.login();
371
+ return throwError(() => error);
372
+ }
373
+ }));
374
+ }
375
+ return throwError(() => error);
376
+ }));
377
+ }
378
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: KeycloakAuthInterceptor, deps: [{ token: KeycloakService }], target: i0.ɵɵFactoryTarget.Injectable });
379
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: KeycloakAuthInterceptor });
380
+ }
381
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: KeycloakAuthInterceptor, decorators: [{
382
+ type: Injectable
383
+ }], ctorParameters: () => [{ type: KeycloakService }] });
384
+
385
+ function initializeKeycloak(keycloak) {
386
+ return () => keycloak.initialize();
387
+ }
388
+ /**
389
+ * Provides Keycloak authentication and authorization services
390
+ * @param config Keycloak configuration
391
+ * @returns Array of providers for Keycloak integration
392
+ */
259
393
  function provideKeycloak(config) {
260
394
  return [
261
395
  importProvidersFrom(HttpClientModule),
@@ -263,7 +397,18 @@ function provideKeycloak(config) {
263
397
  provide: KEYCLOAK_CONFIG,
264
398
  useValue: config
265
399
  },
266
- KeycloakService
400
+ KeycloakService,
401
+ {
402
+ provide: APP_INITIALIZER,
403
+ useFactory: initializeKeycloak,
404
+ multi: true,
405
+ deps: [KeycloakService]
406
+ },
407
+ {
408
+ provide: HTTP_INTERCEPTORS,
409
+ useClass: KeycloakAuthInterceptor,
410
+ multi: true
411
+ }
267
412
  ];
268
413
  }
269
414
 
@@ -334,7 +479,7 @@ class HttpUtilityService {
334
479
  if (options?.timeoutMs) {
335
480
  request$ = request$.pipe(timeout(options.timeoutMs));
336
481
  }
337
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
482
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
338
483
  }
339
484
  /**
340
485
  * Perform a POST request with enhanced options
@@ -357,7 +502,7 @@ class HttpUtilityService {
357
502
  if (options?.timeoutMs) {
358
503
  request$ = request$.pipe(timeout(options.timeoutMs));
359
504
  }
360
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
505
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
361
506
  }
362
507
  /**
363
508
  * Perform a PUT request with enhanced options
@@ -380,7 +525,7 @@ class HttpUtilityService {
380
525
  if (options?.timeoutMs) {
381
526
  request$ = request$.pipe(timeout(options.timeoutMs));
382
527
  }
383
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
528
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
384
529
  }
385
530
  /**
386
531
  * Perform a DELETE request with enhanced options
@@ -402,7 +547,7 @@ class HttpUtilityService {
402
547
  if (options?.timeoutMs) {
403
548
  request$ = request$.pipe(timeout(options.timeoutMs));
404
549
  }
405
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
550
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
406
551
  }
407
552
  /**
408
553
  * Upload a file with optional additional data
@@ -465,7 +610,7 @@ class HttpUtilityService {
465
610
  });
466
611
  }
467
612
  // Standard request without progress tracking
468
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
613
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
469
614
  }
470
615
  /**
471
616
  * Execute multiple HTTP requests in parallel
@@ -474,7 +619,7 @@ class HttpUtilityService {
474
619
  */
475
620
  batchRequests(requests) {
476
621
  this.loading.next(true);
477
- return forkJoin(requests).pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
622
+ return forkJoin(requests).pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
478
623
  }
479
624
  /**
480
625
  * Download a file from the server
@@ -493,7 +638,7 @@ class HttpUtilityService {
493
638
  if (options?.timeoutMs) {
494
639
  request$ = request$.pipe(timeout(options.timeoutMs));
495
640
  }
496
- return request$.pipe(catchError(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
641
+ return request$.pipe(catchError$1(this.handleError.bind(this)), finalize(() => this.loading.next(false)));
497
642
  }
498
643
  /**
499
644
  * Create parameter string from an object
@@ -1814,7 +1959,7 @@ class BaseFormComponent extends BasePageComponent {
1814
1959
  }));
1815
1960
  }
1816
1961
  loadFormData() {
1817
- return this.route.queryParams.pipe(take(1), mergeMap(params => {
1962
+ return this.route.queryParams.pipe(take(1), mergeMap$1(params => {
1818
1963
  const state = this.getRouterState();
1819
1964
  if (this.state.mode === AppConstants.PAGE_MODE.CREATE) {
1820
1965
  return of(void 0);
@@ -2407,7 +2552,7 @@ class BaseTabbedFormComponent extends BasePageComponent {
2407
2552
  activeTab: tabs[0]?.value || ''
2408
2553
  };
2409
2554
  // 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 => {
2555
+ 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
2556
  console.error('Error initializing form:', error);
2412
2557
  this.showError(AppMessages.FORM.ERROR.INIT);
2413
2558
  return of(void 0);
@@ -2469,7 +2614,7 @@ class BaseTabbedFormComponent extends BasePageComponent {
2469
2614
  throw new Error(`UUID is required for ${this.state.mode} mode when no formData is provided`);
2470
2615
  }
2471
2616
  return uuid;
2472
- }), tap$1(() => this.state = { ...this.state, loading: true }), tap$1(uuid => this.loadEntity(uuid)), catchError$1(error => {
2617
+ }), tap$1(() => this.state = { ...this.state, loading: true }), tap$1(uuid => this.loadEntity(uuid)), catchError(error => {
2473
2618
  console.error('Error loading UUID:', error);
2474
2619
  this.showError(`${AppMessages.FORM.ERROR.LOAD} UUID`);
2475
2620
  return of(void 0);
@@ -2670,7 +2815,7 @@ class BaseListTabbedFormComponent extends BasePageComponent {
2670
2815
  this.setupRouteParamsSubscription();
2671
2816
  }
2672
2817
  initializeFormFromQueryParams() {
2673
- this.route.queryParams.pipe(take$1(1), catchError$1(error => {
2818
+ this.route.queryParams.pipe(take$1(1), catchError(error => {
2674
2819
  console.error('Error initializing form:', error);
2675
2820
  this.showError(AppMessages.FORM.ERROR.INIT);
2676
2821
  throw error;
@@ -3120,5 +3265,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImpor
3120
3265
  * Generated bundle index. Do not edit.
3121
3266
  */
3122
3267
 
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 };
3268
+ 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
3269
  //# sourceMappingURL=codex-ts-core-lib.mjs.map