@sinequa/atomic-angular 1.0.17 → 1.0.19

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,13 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, inject, HostBinding, Component, Pipe, InjectionToken, computed, ChangeDetectorRef, DestroyRef, LOCALE_ID, Inject, Optional, input, output, signal, effect, assertInInjectionContext, runInInjectionContext, EnvironmentInjector, Injector, EventEmitter, Directive, viewChild, ElementRef, afterNextRender, untracked, linkedSignal, model, TemplateRef, HostListener, Renderer2, contentChildren, contentChild, booleanAttribute, ChangeDetectionStrategy, resource, ViewContainerRef, viewChildren, numberAttribute, afterRenderEffect, afterEveryRender } from '@angular/core';
3
- import { BehaviorSubject, Subscription, catchError, throwError, firstValueFrom, map, Subject, of, tap, EMPTY, filter, shareReplay, fromEvent, debounceTime, from, switchMap } from 'rxjs';
3
+ import { BehaviorSubject, Subscription, catchError, throwError, firstValueFrom, map, Subject, of, tap, EMPTY, filter, shareReplay, from, fromEvent, debounceTime, switchMap } from 'rxjs';
4
4
  import { TranslocoService, TranslocoPipe, provideTranslocoScope } from '@jsverse/transloco';
5
5
  import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, CardComponent, CardHeaderComponent, CardContentComponent, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogContentComponent, DialogFooterComponent, ListItemComponent, SwitchComponent, SelectOptionDirective, DialogService, TabsComponent, TabsListComponent, TabComponent, ChevronLeftIconComponent, ChevronsLeftIconComponent, ChevronsRightIconComponent, Separator, SheetCloseDirective, SheetService, DateRangePickerDirective, DatepickerDirective, ButtonGroup, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, SearchIcon, FilterIcon, LoadingCircleIconComponent, CircleCheckIconComponent, PopoverComponent, CardFooterComponent, BookmarkIcon, PopoverContentComponent, UserIcon, TrashIcon, FolderIcon, VerticalDividerComponent, BreakpointObserverService, HorizontalDividerComponent, FlagEnglishIconComponent, FlagFrenchIconComponent, EditIcon, UndoIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
6
6
  import highlightWords from 'highlight-words';
7
7
  import { ActivatedRoute, Router, NavigationEnd, RouterLink, RouterModule } from '@angular/router';
8
8
  import { withDevtools } from '@angular-architects/ngrx-toolkit';
9
9
  import { signalStore, signalStoreFeature, withState, withMethods, patchState, getState, withComputed } from '@ngrx/signals';
10
- import { globalConfig, EngineType, extraColumns, sysLang, getQueryParamsFromUrl, clearSessionTokens, login, info, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, fetchQuery, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
10
+ import { globalConfig, EngineType, extraColumns, sysLang, fetchPrincipal, getQueryParamsFromUrl, clearSessionTokens, login, info, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, isObject, Audit, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, fetchQuery, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
11
11
  import { HttpClient, HttpParams, httpResource, HttpResponse, HttpContextToken, HttpHeaders } from '@angular/common/http';
12
12
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
13
13
  import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, Location, NgTemplateOutlet, NgStyle, NgClass, NgComponentOutlet } from '@angular/common';
@@ -1314,26 +1314,24 @@ const PrincipalStore = signalStore({ providedIn: 'root' }, withDevtools('Princip
1314
1314
  }
1315
1315
  })), withPrincipalFeatures());
1316
1316
  function withPrincipalFeatures() {
1317
- return signalStoreFeature(withMethods((store) => {
1318
- const http = inject(HttpClient);
1319
- const API_URL = `${globalConfig.backendUrl}/api/v1`;
1320
- return {
1321
- initialize() {
1322
- patchState(store, { state: 'loading' });
1323
- const params = new HttpParams()
1324
- .set('action', 'get')
1325
- .set('noAuthentication', 'true');
1326
- return firstValueFrom(http.get(`${API_URL}/principal`, { params }).pipe(catchError((error) => {
1327
- console.error('Principal fetch failed', error);
1328
- patchState(store, { state: 'error' });
1329
- throw error;
1330
- }), map((principal) => {
1331
- const { userOverrideActive = false } = globalConfig;
1332
- patchState(store, { ...initialPrincipal, ...principal, userOverrideActive, state: 'loaded' });
1333
- })));
1317
+ return signalStoreFeature(withMethods((store) => ({
1318
+ async initialize() {
1319
+ patchState(store, { state: 'loading' });
1320
+ try {
1321
+ // Use the SDK's fetchPrincipal so the principal load goes through the same auth handling
1322
+ // as the rest of atomic (credentials: "include" + the authMode-aware noAutoAuthentication
1323
+ // flag). In SSO mode it sends noAutoAuthentication=false, letting IIS/Windows authenticate.
1324
+ const principal = await fetchPrincipal();
1325
+ const { userOverrideActive = false } = globalConfig;
1326
+ patchState(store, { ...initialPrincipal, ...principal, userOverrideActive, state: 'loaded' });
1334
1327
  }
1335
- };
1336
- }));
1328
+ catch (error) {
1329
+ console.error('Principal fetch failed', error);
1330
+ patchState(store, { state: 'error' });
1331
+ throw error;
1332
+ }
1333
+ }
1334
+ })));
1337
1335
  }
1338
1336
 
1339
1337
  /**
@@ -4587,17 +4585,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4587
4585
  * @deprecated This service is deprecated and should not be used directly. Please use the PrincipalStore instead.
4588
4586
  */
4589
4587
  class PrincipalService {
4590
- http = inject(HttpClient);
4591
- API_URL = `${globalConfig.backendUrl}/api/v1`;
4592
4588
  /**
4593
4589
  * Retrieves the principal information from the server.
4594
4590
  *
4595
4591
  * @returns Observable<Principal> An observable that emits the principal information.
4596
4592
  *
4597
4593
  * @remarks
4598
- * This method sends a GET request to the API endpoint to fetch the principal data.
4599
- * It includes query parameters to specify the action and to indicate that no authentication is required.
4600
- * In case of an error, it logs the error to the console and returns an empty observable.
4594
+ * Delegates to the SDK's fetchPrincipal so the request goes through atomic's auth handling
4595
+ * (credentials + authMode-aware noAutoAuthentication). On error it logs and returns EMPTY.
4601
4596
  *
4602
4597
  * @example
4603
4598
  * ```typescript
@@ -4607,9 +4602,7 @@ class PrincipalService {
4607
4602
  * ```
4608
4603
  */
4609
4604
  getPrincipal() {
4610
- const params = new HttpParams().set('action', 'get');
4611
- params.append('noAuthentication', 'true');
4612
- return this.http.get(this.API_URL + '/principal', { params }).pipe(catchError(error => {
4605
+ return from(fetchPrincipal()).pipe(catchError(error => {
4613
4606
  console.error('PrincipalService.getPrincipal failure - error: ', error);
4614
4607
  return EMPTY;
4615
4608
  }));
@@ -10937,6 +10930,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10937
10930
  }]
10938
10931
  }], propDecorators: { cancel: [{ type: i0.Output, args: ["cancel"] }], success: [{ type: i0.Output, args: ["success"] }], userName: [{ type: i0.Input, args: [{ isSignal: true, alias: "userName", required: false }] }, { type: i0.Output, args: ["userNameChange"] }] } });
10939
10932
 
10933
+ /**
10934
+ * Mirrors @sinequa/atomic's `AUTH_REDIRECT_ATTEMPT_KEY` (src/authentication/constants.ts): the
10935
+ * sessionStorage flag set right before a provider full-page redirect (`window.location.href`).
10936
+ * Reading it lets us tell "an IdP redirect is under way" from "no session", without a blind timer.
10937
+ */
10938
+ const AUTH_REDIRECT_ATTEMPT_KEY = "sinequa-auth-redirect-attempt";
10940
10939
  /**
10941
10940
  * Represents the LoginComponent class, which is responsible for handling the login functionality.
10942
10941
  * This component is used to authenticate users and manage the user's authentication status.
@@ -10978,6 +10977,13 @@ class SignInComponent {
10978
10977
  authenticated = signal(isAuthenticated(), ...(ngDevMode ? [{ debugName: "authenticated" }] : []));
10979
10978
  user = signal(getState(this.principalStore), ...(ngDevMode ? [{ debugName: "user" }] : []));
10980
10979
  expiresSoonNotified = signal(false, ...(ngDevMode ? [{ debugName: "expiresSoonNotified" }] : []));
10980
+ /**
10981
+ * Forces the credentials form to show even while `externalAuth` is true. Set when a silent
10982
+ * SSO/browser auth attempt fails and no OAuth/SAML provider is configured: in that case
10983
+ * `useSSO` only meant "unknown — try SSO, then credentials", so we fall back to the form
10984
+ * instead of routing to /error.
10985
+ */
10986
+ showCredentialsFallback = signal(false, ...(ngDevMode ? [{ debugName: "showCredentialsFallback" }] : []));
10981
10987
  constructor(destroyRef) {
10982
10988
  this.destroyRef = destroyRef;
10983
10989
  // If the user is already authenticated when landing here (e.g. page refresh on
@@ -10987,21 +10993,45 @@ class SignInComponent {
10987
10993
  const url = this.route.snapshot.queryParams["returnUrl"] || "/";
10988
10994
  this.router.navigateByUrl(url);
10989
10995
  }
10990
- // When authentication is delegated to the browser/proxy (SSO) or an OAuth/SAML
10991
- // provider, no credentials form is shown: this screen shows a loader and initiates
10992
- // the handshake automatically by calling `handleLogin()`. If the handshake never
10993
- // completes, fall back to /error after 5s; the fallback is cancelled as soon as
10994
- // the login succeeds (the `authenticated` event then drives navigation).
10996
+ // When authentication is delegated to the browser/proxy (SSO) or an OAuth/SAML provider, no
10997
+ // credentials form is shown: this screen shows a loader and starts the handshake automatically.
10998
+ // Drive what happens next from the OUTCOME of `login()`, never a blind timer racing the network
10999
+ // call (the old 5s timer fired while a slow but legitimate provider call — up to its own 10s
11000
+ // network timeout was still in flight, flashing /error just before the IdP redirect):
11001
+ // • rejected → real failure (network timeout, 5xx, callback loop) → /error WITH the reason;
11002
+ // • resolved true → authenticated → the `authenticated` event / returnUrl drives navigation;
11003
+ // • resolved false → either an OAuth/SAML full-page redirect is under way (the redirect flag was
11004
+ // set just before `window.location.href`; the page is about to unload, so do
11005
+ // nothing and let it leave for the IdP), or no provider is configured
11006
+ // (ambiguous SSO → show the credentials form), or the provider returned no
11007
+ // redirect at all (→ /error).
10995
11008
  if (this.externalAuth && !this.authenticated()) {
10996
- const timeout = setTimeout(() => {
11009
+ login()
11010
+ .then(result => {
11011
+ if (result) {
11012
+ this.auditService.notifyLogin();
11013
+ return;
11014
+ }
11015
+ const hasAutoProvider = !!(globalConfig.autoOAuthProvider || globalConfig.autoSAMLProvider);
11016
+ if (!hasAutoProvider) {
11017
+ this.showCredentialsFallback.set(true);
11018
+ return;
11019
+ }
11020
+ // A full-page redirect to the IdP was initiated → the page is about to unload. Routing to
11021
+ // /error here is exactly what caused the transient "error" flash, so skip it.
11022
+ if (sessionStorage.getItem(AUTH_REDIRECT_ATTEMPT_KEY))
11023
+ return;
11024
+ // Provider configured but the server returned no redirectUrl and there is no session → error.
10997
11025
  this.router.navigate(["/error"], {
10998
11026
  queryParams: { returnUrl: this.route.snapshot.queryParams["returnUrl"] }
10999
11027
  });
11000
- }, 5000);
11001
- destroyRef.onDestroy(() => clearTimeout(timeout));
11002
- this.handleLogin().then(result => {
11003
- if (result)
11004
- clearTimeout(timeout);
11028
+ })
11029
+ .catch((err) => {
11030
+ warn("An error occurred while logging in", err);
11031
+ this.auditService.notify({ type: "Login_Denied" });
11032
+ // Surface the failure reason on the error page (e.g. "OAuth provider not found: identity-dev").
11033
+ const message = (err?.errorMessage ?? err?.message) || undefined;
11034
+ this.router.navigate(["error"], { queryParams: { message } });
11005
11035
  });
11006
11036
  }
11007
11037
  effect(() => {
@@ -11113,7 +11143,7 @@ class SignInComponent {
11113
11143
  }
11114
11144
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SignInComponent, deps: [{ token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
11115
11145
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SignInComponent, isStandalone: true, selector: "signIn, signin, sign-in", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, username: { classPropertyName: "username", publicName: "username", isSignal: true, isRequired: false, transformFunction: null }, password: { classPropertyName: "password", publicName: "password", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { forgotPassword: "forgotPassword", username: "usernameChange", password: "passwordChange" }, host: { properties: { "class": "cn('grid h-dvh w-full place-content-center', class())" } }, providers: [provideTranslocoScope("login")], ngImport: i0, template: `
11116
- @if (!authenticated() && !externalAuth) {
11146
+ @if (!authenticated() && (!externalAuth || showCredentialsFallback())) {
11117
11147
  <Card
11118
11148
  hover="no"
11119
11149
  cdkTrapFocus
@@ -11186,7 +11216,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11186
11216
  CardHeaderComponent,
11187
11217
  CardContentComponent
11188
11218
  ], providers: [provideTranslocoScope("login")], template: `
11189
- @if (!authenticated() && !externalAuth) {
11219
+ @if (!authenticated() && (!externalAuth || showCredentialsFallback())) {
11190
11220
  <Card
11191
11221
  hover="no"
11192
11222
  cdkTrapFocus