@sinequa/atomic-angular 1.5.2 → 1.5.5

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,16 +1,16 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, HostBinding, Component, Pipe, InjectionToken, computed, ChangeDetectorRef, DestroyRef, LOCALE_ID, Inject, Optional, input, output, signal, effect, assertInInjectionContext, runInInjectionContext, Injector, EventEmitter, Directive, viewChild, ElementRef, afterNextRender, untracked, linkedSignal, model, TemplateRef, HostListener, Renderer2, contentChildren, contentChild, booleanAttribute, resource, ChangeDetectionStrategy, ViewContainerRef, numberAttribute, viewChildren, afterRenderEffect, afterEveryRender } from '@angular/core';
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, resource, ChangeDetectionStrategy, ViewContainerRef, numberAttribute, viewChildren, afterRenderEffect, afterEveryRender } from '@angular/core';
3
3
  import { BehaviorSubject, Subscription, firstValueFrom, catchError, map, Subject, of, tap, EMPTY, throwError, filter, shareReplay, switchMap, from, fromEvent, debounceTime } from 'rxjs';
4
4
  import { TranslocoService, TranslocoPipe, provideTranslocoScope } from '@jsverse/transloco';
5
- import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, LinkComponent, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogContentComponent, DialogFooterComponent, ListItemComponent, SquareCheckIcon, SquareMinusIcon, SquareIcon, SwitchComponent, SelectOptionDirective, DialogService, XMarkIcon, InboxIcon, SparklesIcon, FileOutputIcon, TabsComponent, TabsListComponent, TabComponent, SidebarMenuComponent, SidebarMenuItemComponent, SidebarMenuButtonComponent, TooltipDirective, FrownIcon, ChevronLeftIcon, ChevronsLeftIcon, ChevronsRightIcon, ArrowUpAzIcon, ArrowDownZaIcon, ArrowUpRightFromSquareIcon, ToggleRightIcon, ToggleLeftIcon, Separator, SheetCloseDirective, ArrowLeftIcon, SheetService, DateRangePickerDirective, DatepickerDirective, FilterIcon, FilterXIcon, IconButtonComponent, HighlighterIcon, TagsIcon, SpinnerIcon, MagnifyingGlassIcon, LoadingCircleIcon, CircleCheckIcon, PopoverComponent, BellIcon, TrashIcon, BarsIcon, CardComponent, CardHeaderComponent, CardContentComponent, CardFooterComponent, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, DropdownDirective, SearchIcon, TriangleAlertIcon, ListFilterIcon, BreakpointObserverService, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
5
+ import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, LinkComponent, CardComponent, CardHeaderComponent, CardContentComponent, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogContentComponent, DialogFooterComponent, ListItemComponent, SquareCheckIcon, SquareMinusIcon, SquareIcon, SwitchComponent, SelectOptionDirective, DialogService, XMarkIcon, InboxIcon, SparklesIcon, FileOutputIcon, TabsComponent, TabsListComponent, TabComponent, SidebarMenuComponent, SidebarMenuItemComponent, SidebarMenuButtonComponent, TooltipDirective, FrownIcon, ChevronLeftIcon, ChevronsLeftIcon, ChevronsRightIcon, ArrowUpAzIcon, ArrowDownZaIcon, ArrowUpRightFromSquareIcon, ToggleRightIcon, ToggleLeftIcon, Separator, SheetCloseDirective, ArrowLeftIcon, SheetService, DateRangePickerDirective, DatepickerDirective, FilterIcon, FilterXIcon, IconButtonComponent, HighlighterIcon, TagsIcon, SpinnerIcon, MagnifyingGlassIcon, LoadingCircleIcon, CircleCheckIcon, PopoverComponent, BellIcon, TrashIcon, BarsIcon, CardFooterComponent, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, DropdownDirective, SearchIcon, TriangleAlertIcon, ListFilterIcon, BreakpointObserverService, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
6
6
  import highlightWords from 'highlight-words';
7
7
  import { ActivatedRoute, Router, NavigationEnd, RouterLink, RouterLinkActive, 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 { EngineType, fetchApp, extraColumns, sysLang, globalConfig, getQueryParamsFromUrl, clearSessionTokens, login, info, warn, error, setGlobalConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, 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 { EngineType, fetchApp, extraColumns, sysLang, globalConfig, 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 { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
12
12
  import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, Location, NgTemplateOutlet, NgStyle, NgClass, NgComponentOutlet } from '@angular/common';
13
- import { HttpClient, HttpParams, httpResource, HttpResponse, HttpHeaders, HttpContextToken } from '@angular/common/http';
13
+ import { HttpClient, HttpParams, httpResource, HttpResponse, HttpContextToken, HttpHeaders } from '@angular/common/http';
14
14
  import { Title, DomSanitizer } from '@angular/platform-browser';
15
15
  import { cva } from 'class-variance-authority';
16
16
  import * as i1 from '@angular/forms';
@@ -1525,100 +1525,174 @@ function withThemes(app, themes) {
1525
1525
  }
1526
1526
 
1527
1527
  /**
1528
- * Signs in the user by checking the global configuration for authentication method and acting accordingly.
1528
+ * Signs the user in according to the resolved {@link globalConfig.authMode}.
1529
1529
  *
1530
- * This function first clears any existing session tokens to ensure a clean authentication state. It then checks the
1531
- * global configuration to determine whether to use credentials-based authentication or Single Sign-On (SSO). If
1532
- * credentials are used, it redirects the user to the login page. If SSO is enabled, it reloads the page to trigger
1533
- * the SSO login process. If neither method is specified, it attempts a standard login and handles any errors that
1534
- * may occur during the process.
1535
- * @returns A promise resolving to a boolean indicating the success of the sign-in operation.
1530
+ * The mode is expected to be resolved beforehand (by `initializeAppConfig`, awaited in
1531
+ * `bootstrapApp`). This function clears any existing session, then:
1532
+ * - `credentials` redirect to the login form;
1533
+ * - `sso` reload the page so the browser/proxy performs the handshake;
1534
+ * - `oauth` / `saml` → delegate to `login()`, which redirects to the provider;
1535
+ * - `bearer` delegate to `login()`;
1536
+ * - `unknown` → `login()` tries SSO silently then resolves to credentials; on failure the login
1537
+ * form is shown.
1538
+ *
1539
+ * @returns A promise resolving to a boolean indicating whether the user is authenticated.
1536
1540
  */
1537
1541
  async function signIn() {
1538
1542
  assertInInjectionContext(signIn);
1539
1543
  const router = inject(Router);
1540
1544
  const lastUrlAfterNavigation = inject(NavigationService).urlAfterNavigation;
1541
- const { useCredentials, loginPath, useSSO } = globalConfig;
1545
+ const { loginPath, authMode } = globalConfig;
1542
1546
  // Always clear authentication tokens first to clear any existing session
1543
1547
  clearSessionTokens();
1544
- // If credentials are used, redirect to the login page
1545
- if (useCredentials) {
1548
+ // Credentials: show the login form.
1549
+ if (authMode?.kind === "credentials") {
1546
1550
  router.navigate([loginPath], { queryParams: { returnUrl: lastUrlAfterNavigation } });
1547
1551
  return false; // prevent further execution
1548
1552
  }
1549
- // SSO is set to true when the browser handles authentication automatically
1550
- // If SSO is used, reload the page to trigger SSO login
1551
- if (useSSO) {
1552
- // reload the page to trigger SSO login
1553
+ // SSO: the browser/proxy handles authentication reload to trigger it.
1554
+ if (authMode?.kind === "sso") {
1553
1555
  window.location.reload();
1554
1556
  return false; // prevent further execution
1555
1557
  }
1556
- // Otherwise, perform a standard login
1558
+ // oauth / saml / bearer / unknown — let login() drive the handshake.
1557
1559
  try {
1558
1560
  const response = await login();
1559
1561
  if (response) {
1560
1562
  info("Response from login", response);
1561
1563
  return true;
1562
1564
  }
1563
- else {
1564
- warn("Response from login", response);
1565
+ // Not authenticated. For provider redirects (oauth/saml) the page is already navigating away,
1566
+ // so we don't touch the router. Otherwise (unknown resolved to credentials, or bearer failed)
1567
+ // show the login form.
1568
+ if (authMode?.kind !== "oauth" && authMode?.kind !== "saml") {
1569
+ router.navigate([loginPath], { queryParams: { returnUrl: lastUrlAfterNavigation } });
1565
1570
  }
1571
+ return false;
1566
1572
  }
1567
1573
  catch (err) {
1568
1574
  error("Error during login", err);
1569
- if (err.status === 401) {
1575
+ // A 401 is recoverable: the user just needs to authenticate → show the login form.
1576
+ if (err?.status === 401) {
1570
1577
  error("Unauthorized access - please check your credentials:", err?.errorMessage);
1571
- router.navigate([loginPath]);
1578
+ router.navigate([loginPath], { queryParams: { returnUrl: lastUrlAfterNavigation } });
1579
+ return false;
1572
1580
  }
1573
- throw err;
1581
+ // Any other error (e.g. a misconfigured OAuth/SAML provider → 5xx) is fatal and not something
1582
+ // the user can resolve from the login form. Stop here: navigate straight to the error page with
1583
+ // the reason. We return false (instead of rethrowing) so the application does NOT initialize and
1584
+ // fire further authenticated API calls (e.g. usersettings → 401).
1585
+ const message = (err?.errorMessage ?? err?.message) || undefined;
1586
+ router.navigate(["/error"], { queryParams: { message } });
1587
+ return false;
1574
1588
  }
1575
- return false; // prevent further execution
1576
1589
  }
1577
1590
 
1578
1591
  /**
1579
1592
  * Bootstraps the application by ensuring the user is authenticated and initializing the application.
1580
1593
  *
1581
- * This function first attempts to sign in the user using the provided `Router`. If authentication is successful,
1582
- * it proceeds to initialize the application and create routes using the `applicationService`. Any errors during
1583
- * authentication or initialization are logged to the console, but the returned promise always resolves.
1594
+ * This function first attempts to sign in the user via `signIn()`. If authentication is successful,
1595
+ * it initializes the application (stores and, optionally, dynamic routes) through `ApplicationService`
1596
+ * and waits for the initialization to complete before resolving. Any errors during authentication or
1597
+ * initialization are logged to the console, but the returned promise never rejects.
1584
1598
  *
1585
- * @param Router - The application's router instance used for navigation and authentication.
1586
- * @param applicationService - The service responsible for initializing the application and creating routes.
1587
- * @returns A promise that resolves when the application is ready to be initialized, regardless of success or failure.
1599
+ * Note: this function relies on Angular's injection context, so it must be called within one
1600
+ * (e.g. from `provideAppInitializer`).
1601
+ *
1602
+ * @param options - Configuration options for the bootstrap process.
1603
+ * @param options.createRoutes - Whether to create routes during initialization. Defaults to `true`.
1604
+ * @returns A promise that resolves to `true` when the application has been fully initialized,
1605
+ * or `false` when the user is not authenticated or an error occurred.
1588
1606
  */
1589
- async function withBootstrapApp(applicationService, { createRoutes = true }) {
1590
- // This function is used to test if the application is ready to be initialized
1591
- // set the createRoutes flag in the global config
1607
+ async function bootstrapApp({ createRoutes = true } = {}) {
1608
+ assertInInjectionContext(bootstrapApp);
1609
+ // Capture the injector synchronously: Angular's injection context does not survive across an
1610
+ // `await`, so after awaiting `initializeAppConfig()` we must re-enter it to inject services and
1611
+ // run `signIn()` (which use `inject()` internally).
1612
+ const injector = inject(EnvironmentInjector);
1613
+ // set the createRoutes flag early so it is defined even when sign-in fails
1614
+ // (ApplicationService.initialize() sets it again when it runs)
1592
1615
  setGlobalConfig({ createRoutes });
1593
- return new Promise(resolve => {
1594
- // Check if the user is authenticated
1595
- signIn()
1596
- .then(async (response) => {
1597
- if (response) {
1598
- info('User authenticated, initializing application...');
1599
- // Initialize the application.
1600
- // Awaited so the APP_INITIALIZER does not resolve (and bootstrap does not
1601
- // complete) until initialization is done. Otherwise routed components render
1602
- // and set their page title before `initialize()` runs `setGeneralApp()`, which
1603
- // would then overwrite the page title with the bare application name.
1604
- try {
1605
- await applicationService.initialize(createRoutes);
1606
- info(`Application initialized with routes: ${createRoutes} successfully.`);
1607
- }
1608
- catch (err) {
1609
- error(`Error initializing application with routes: ${createRoutes}:`, err);
1610
- }
1611
- }
1612
- else {
1613
- info('User not authenticated, skipping application initialization.');
1614
- }
1615
- ;
1616
- })
1617
- .catch(err => {
1618
- error('Error while signing in:', err);
1619
- })
1620
- .finally(() => resolve());
1621
- });
1616
+ // Resolve the authentication mode (queries the server pre-login when needed) BEFORE signing in.
1617
+ // Doing it here rather than as a separate, concurrent APP_INITIALIZER — guarantees that
1618
+ // signIn() reads a fully resolved `authMode` and removes the long-standing bootstrap race.
1619
+ // Crucially, this also sets `backendUrl` BEFORE any service/store is constructed, so those that
1620
+ // derive their API URL from `globalConfig.backendUrl` (e.g. PrincipalStore) don't capture
1621
+ // `undefined` (which produced requests to `/undefined/api/v1/...`).
1622
+ try {
1623
+ await initializeAppConfig();
1624
+ }
1625
+ catch (err) {
1626
+ error('Error while initializing application config:', err);
1627
+ // Pre-login failed (e.g. a 500 "app not found: 'mint_rnd-yoyo'" for a bad app name). This is
1628
+ // fatal — backendUrl/authMode can't be resolved — so route to the error page with the reason
1629
+ // instead of continuing to signIn() (which would otherwise fail with a less meaningful error).
1630
+ const message = err?.errorMessage
1631
+ ?? err?.message;
1632
+ runInInjectionContext(injector, () => inject(Router).navigate(['/error'], { queryParams: { message } }));
1633
+ return false;
1634
+ }
1635
+ // Inject ApplicationService AFTER initializeAppConfig (re-entering the injection context lost
1636
+ // across the await), so its dependent services/stores are constructed with `backendUrl` set.
1637
+ const applicationService = runInInjectionContext(injector, () => inject(ApplicationService));
1638
+ let authenticated = false;
1639
+ try {
1640
+ // Re-enter the injection context lost across the await above.
1641
+ authenticated = await runInInjectionContext(injector, () => signIn());
1642
+ }
1643
+ catch (err) {
1644
+ error('Error while signing in:', err);
1645
+ return false;
1646
+ }
1647
+ if (!authenticated) {
1648
+ info('User not authenticated, skipping application initialization.');
1649
+ return false;
1650
+ }
1651
+ info('User authenticated, initializing application...');
1652
+ try {
1653
+ await applicationService.initialize(createRoutes);
1654
+ info(`Application initialized successfully (createRoutes: ${createRoutes}).`);
1655
+ return true;
1656
+ }
1657
+ catch (err) {
1658
+ error(`Error initializing application (createRoutes: ${createRoutes}):`, err);
1659
+ // Authenticated, but the application failed to initialize (e.g. fetchApp/usersettings failed).
1660
+ // Route to the error page with the failure reason instead of leaving a half-initialized app.
1661
+ const message = err?.errorMessage
1662
+ ?? err?.message;
1663
+ runInInjectionContext(injector, () => inject(Router).navigate(['/error'], { queryParams: { message } }));
1664
+ return false;
1665
+ }
1666
+ }
1667
+ /**
1668
+ * Bootstraps the application by ensuring the user is authenticated and initializing the application.
1669
+ *
1670
+ * @deprecated Use {@link bootstrapApp} instead, and let it inject `ApplicationService` itself.
1671
+ *
1672
+ * Migration — in your `app.config.ts`, replace:
1673
+ * ```ts
1674
+ * // ❌ Deprecated: eagerly injecting ApplicationService in the factory constructs it (and its
1675
+ * // dependent stores/services) BEFORE bootstrapApp resolves the config, so services that build
1676
+ * // their API URL from `globalConfig.backendUrl` capture `undefined` (→ `/undefined/api/v1/...`).
1677
+ * provideAppInitializer(() => withBootstrapApp(inject(ApplicationService), { createRoutes: true })),
1678
+ * ```
1679
+ * with:
1680
+ * ```ts
1681
+ * // ✅ bootstrapApp injects ApplicationService internally, AFTER initializeAppConfig() has set
1682
+ * // `backendUrl` and resolved the auth mode.
1683
+ * provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
1684
+ * ```
1685
+ * (Remove the now-unused `inject` / `ApplicationService` imports.)
1686
+ *
1687
+ * @param applicationService - Ignored; kept for backward compatibility. `ApplicationService` is
1688
+ * provided in root and injected internally by {@link bootstrapApp}, so the instance is the same.
1689
+ * Passing `inject(ApplicationService)` here is discouraged — see the migration note above.
1690
+ * @param options - Configuration options for the bootstrap process.
1691
+ * @param options.createRoutes - Whether to create routes during initialization. Defaults to `true`.
1692
+ * @returns A promise that resolves when the bootstrap process is complete, regardless of success or failure.
1693
+ */
1694
+ async function withBootstrapApp(_applicationService, { createRoutes = true } = {}) {
1695
+ await bootstrapApp({ createRoutes });
1622
1696
  }
1623
1697
 
1624
1698
  /**
@@ -3384,8 +3458,9 @@ class AppService {
3384
3458
  *
3385
3459
  * @remarks
3386
3460
  * This method constructs an HTTP GET request to fetch the application configuration
3387
- * using the `app` parameter from the global configuration. If the request fails,
3388
- * it logs the error to the console and returns an empty observable.
3461
+ * using the `app` parameter from the global configuration. If the request fails, it logs the
3462
+ * error and re-throws a normalized `Error` carrying the server's `errorMessage` when available
3463
+ * (e.g. "app not found: '...'"), so callers can surface the reason on the error page.
3389
3464
  *
3390
3465
  * @example
3391
3466
  * ```typescript
@@ -3397,9 +3472,19 @@ class AppService {
3397
3472
  getApp(appName) {
3398
3473
  const app = appName || globalConfig.app;
3399
3474
  const params = new HttpParams().set('app', app || '');
3400
- return this.http.get(this.API_URL + '/app', { params }).pipe(catchError(error => {
3401
- console.error('AppService.getApp failure - error: ', error);
3402
- return EMPTY;
3475
+ return this.http.get(this.API_URL + '/app', { params }).pipe(catchError((err) => {
3476
+ console.error('AppService.getApp failure - error: ', err);
3477
+ // Propagate the failure (previously swallowed with EMPTY, which hid the cause and surfaced a
3478
+ // generic "no elements in sequence" downstream). Surface the Sinequa error-envelope message
3479
+ // when present — e.g. a 500 with { errorMessage: "app not found: 'mint_rnd-yoyo'" } — so the
3480
+ // bootstrap/login flow can route to the error page with the actual reason.
3481
+ const e = err;
3482
+ const message = e?.error?.errorMessage ??
3483
+ e?.error?.message ??
3484
+ e?.errorMessage ??
3485
+ e?.message ??
3486
+ 'Failed to fetch application configuration';
3487
+ return throwError(() => new Error(message));
3403
3488
  }));
3404
3489
  }
3405
3490
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AppService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -3421,12 +3506,13 @@ function AuthGuard() {
3421
3506
  return async (_, state) => {
3422
3507
  const router = inject(Router);
3423
3508
  const principalStore = inject(PrincipalStore);
3424
- const { loginPath, useCredentials, useSSO } = globalConfig;
3509
+ const { loginPath, authMode } = globalConfig;
3425
3510
  if (state.url.startsWith("/login"))
3426
3511
  return true;
3427
3512
  // If the user is not authenticated, navigate to the login page.
3428
3513
  // The login page handles every authentication method (credentials, OAuth, SAML).
3429
- if (!isAuthenticated() && !useSSO) {
3514
+ // In SSO mode the browser/proxy carries the auth, so we let it through.
3515
+ if (!isAuthenticated() && authMode?.kind !== "sso") {
3430
3516
  router.navigate([loginPath], { queryParams: { returnUrl: state.url } });
3431
3517
  return false;
3432
3518
  }
@@ -3452,7 +3538,7 @@ function AuthGuard() {
3452
3538
  // check if the password is expired and if the partition is editable
3453
3539
  // changing password is only possible when user use credentials to auhtenticate
3454
3540
  // only in credentials mode
3455
- if (useCredentials) {
3541
+ if (authMode?.kind === "credentials") {
3456
3542
  const exp = passwordExpirationDate;
3457
3543
  const editable = !!editablePartition;
3458
3544
  if (editable && exp && isExpired(exp)) {
@@ -5278,101 +5364,160 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
5278
5364
  }], ctorParameters: () => [], propDecorators: { article: [{ type: i0.Input, args: [{ isSignal: true, alias: "article", required: true }] }], aggregation: [{ type: i0.Input, args: [{ isSignal: true, alias: "aggregation", required: true }] }], shadow: [{ type: i0.ViewChild, args: ["shadowRender", { ...{ read: ElementRef }, isSignal: true }] }], client: [{ type: i0.ViewChild, args: ["documentLocator", { ...{ read: ElementRef }, isSignal: true }] }] } });
5279
5365
 
5280
5366
  class ErrorComponent {
5367
+ route = inject(ActivatedRoute);
5281
5368
  router = inject(Router);
5282
- reload() {
5369
+ /**
5370
+ * Human-readable error detail shown on the page, taken from the `message` query param.
5371
+ * Callers navigating here can pass it, e.g. on an auth failure:
5372
+ * `router.navigate(["error"], { queryParams: { message: err.message } })`.
5373
+ */
5374
+ message = (() => {
5375
+ const value = this.route.snapshot.queryParams['message'];
5376
+ return typeof value === 'string' && value.trim().length ? value : undefined;
5377
+ })();
5378
+ /** Navigate home and re-bootstrap the application (fresh authentication attempt). */
5379
+ goHome() {
5283
5380
  this.router.navigate(['/']).then(() => window.location.reload());
5284
5381
  }
5285
5382
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ErrorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5286
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: ErrorComponent, isStandalone: true, selector: "error-component, ErrorComponent", ngImport: i0, template: `
5287
- <div class="bg-background text-foreground flex min-h-screen flex-col items-center justify-center">
5288
- <svg
5289
- class="mb-8 h-20 w-20 text-red-600"
5290
- xmlns="http://www.w3.org/2000/svg"
5291
- width="24"
5292
- height="24"
5293
- viewBox="0 0 24 24"
5294
- fill="none"
5295
- stroke="currentColor"
5296
- stroke-width="2"
5297
- stroke-linecap="round"
5298
- stroke-linejoin="round">
5299
- <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
5300
- <path d="M12 9v4" />
5301
- <path d="M12 17h.01" />
5302
- </svg>
5303
- <h1 class="mb-4 text-4xl font-bold">Oops! Something went wrong</h1>
5304
- <p class="text-muted-foreground mb-8 text-xl">We apologize for the inconvenience.</p>
5305
- <div class="flex space-x-4">
5306
- <button
5307
- (click)="reload()"
5308
- class="btn btn-outline rounded-lg border-neutral-300 text-neutral-700 transition hover:bg-neutral-200 hover:text-neutral-700">
5383
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: ErrorComponent, isStandalone: true, selector: "error-component, ErrorComponent", host: { classAttribute: "bg-background text-foreground grid min-h-dvh w-full place-content-center p-6" }, ngImport: i0, template: `
5384
+ <Card
5385
+ hover="no"
5386
+ class="bg-background border-(--popover-border) w-full max-w-md rounded-3xl border shadow-2xl">
5387
+ <CardHeader class="flex flex-col items-center gap-4 pt-8 text-center">
5388
+ <span
5389
+ class="bg-destructive/10 text-destructive inline-flex h-16 w-16 items-center justify-center rounded-full">
5309
5390
  <svg
5310
- class="mr-2 h-4 w-4"
5311
5391
  xmlns="http://www.w3.org/2000/svg"
5312
- width="24"
5313
- height="24"
5392
+ class="h-8 w-8"
5314
5393
  viewBox="0 0 24 24"
5315
5394
  fill="none"
5316
5395
  stroke="currentColor"
5317
5396
  stroke-width="2"
5318
5397
  stroke-linecap="round"
5319
- stroke-linejoin="round">
5320
- <path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
5321
- <path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
5398
+ stroke-linejoin="round"
5399
+ aria-hidden="true">
5400
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
5401
+ <path d="M12 9v4" />
5402
+ <path d="M12 17h.01" />
5322
5403
  </svg>
5323
- Go to Homepage
5324
- </button>
5325
- </div>
5326
- </div>
5327
- `, isInline: true });
5404
+ </span>
5405
+
5406
+ <div class="grid gap-1.5">
5407
+ <h1 class="text-2xl font-semibold tracking-tight">Something went wrong</h1>
5408
+ <p class="text-muted-foreground text-sm">
5409
+ We're sorry — the application ran into a problem and couldn't continue.
5410
+ </p>
5411
+ </div>
5412
+ </CardHeader>
5413
+
5414
+ <CardContent class="grid gap-5 pb-8">
5415
+ @if (message) {
5416
+ <div class="border-border bg-muted/40 grid gap-1 rounded-lg border p-3 text-left">
5417
+ <span class="text-muted-foreground text-[0.7rem] font-medium uppercase tracking-wider">
5418
+ Details
5419
+ </span>
5420
+ <p class="text-foreground text-sm break-words" role="alert">{{ message }}</p>
5421
+ </div>
5422
+ }
5423
+
5424
+ <div class="flex justify-center">
5425
+ <button variant="primary" (click)="goHome()">
5426
+ <svg
5427
+ xmlns="http://www.w3.org/2000/svg"
5428
+ class="mr-2 h-4 w-4"
5429
+ viewBox="0 0 24 24"
5430
+ fill="none"
5431
+ stroke="currentColor"
5432
+ stroke-width="2"
5433
+ stroke-linecap="round"
5434
+ stroke-linejoin="round"
5435
+ aria-hidden="true">
5436
+ <path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
5437
+ <path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
5438
+ </svg>
5439
+ Go to homepage
5440
+ </button>
5441
+ </div>
5442
+ </CardContent>
5443
+ </Card>
5444
+ `, isInline: true, dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "directive", type: CardComponent, selector: ".card, card, Card", inputs: ["class", "variant", "hover"] }, { kind: "directive", type: CardHeaderComponent, selector: ".card-header, card-header, CardHeader, cardheader", inputs: ["class"] }, { kind: "directive", type: CardContentComponent, selector: ".card-content, card-content, CardContent, cardcontent", inputs: ["class"] }] });
5328
5445
  }
5329
5446
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ErrorComponent, decorators: [{
5330
5447
  type: Component,
5331
5448
  args: [{
5332
5449
  selector: 'error-component, ErrorComponent',
5333
5450
  standalone: true,
5334
- imports: [],
5451
+ imports: [
5452
+ ButtonComponent,
5453
+ CardComponent,
5454
+ CardHeaderComponent,
5455
+ CardContentComponent
5456
+ ],
5457
+ host: {
5458
+ class: 'bg-background text-foreground grid min-h-dvh w-full place-content-center p-6'
5459
+ },
5335
5460
  template: `
5336
- <div class="bg-background text-foreground flex min-h-screen flex-col items-center justify-center">
5337
- <svg
5338
- class="mb-8 h-20 w-20 text-red-600"
5339
- xmlns="http://www.w3.org/2000/svg"
5340
- width="24"
5341
- height="24"
5342
- viewBox="0 0 24 24"
5343
- fill="none"
5344
- stroke="currentColor"
5345
- stroke-width="2"
5346
- stroke-linecap="round"
5347
- stroke-linejoin="round">
5348
- <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
5349
- <path d="M12 9v4" />
5350
- <path d="M12 17h.01" />
5351
- </svg>
5352
- <h1 class="mb-4 text-4xl font-bold">Oops! Something went wrong</h1>
5353
- <p class="text-muted-foreground mb-8 text-xl">We apologize for the inconvenience.</p>
5354
- <div class="flex space-x-4">
5355
- <button
5356
- (click)="reload()"
5357
- class="btn btn-outline rounded-lg border-neutral-300 text-neutral-700 transition hover:bg-neutral-200 hover:text-neutral-700">
5461
+ <Card
5462
+ hover="no"
5463
+ class="bg-background border-(--popover-border) w-full max-w-md rounded-3xl border shadow-2xl">
5464
+ <CardHeader class="flex flex-col items-center gap-4 pt-8 text-center">
5465
+ <span
5466
+ class="bg-destructive/10 text-destructive inline-flex h-16 w-16 items-center justify-center rounded-full">
5358
5467
  <svg
5359
- class="mr-2 h-4 w-4"
5360
5468
  xmlns="http://www.w3.org/2000/svg"
5361
- width="24"
5362
- height="24"
5469
+ class="h-8 w-8"
5363
5470
  viewBox="0 0 24 24"
5364
5471
  fill="none"
5365
5472
  stroke="currentColor"
5366
5473
  stroke-width="2"
5367
5474
  stroke-linecap="round"
5368
- stroke-linejoin="round">
5369
- <path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
5370
- <path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
5475
+ stroke-linejoin="round"
5476
+ aria-hidden="true">
5477
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
5478
+ <path d="M12 9v4" />
5479
+ <path d="M12 17h.01" />
5371
5480
  </svg>
5372
- Go to Homepage
5373
- </button>
5374
- </div>
5375
- </div>
5481
+ </span>
5482
+
5483
+ <div class="grid gap-1.5">
5484
+ <h1 class="text-2xl font-semibold tracking-tight">Something went wrong</h1>
5485
+ <p class="text-muted-foreground text-sm">
5486
+ We're sorry — the application ran into a problem and couldn't continue.
5487
+ </p>
5488
+ </div>
5489
+ </CardHeader>
5490
+
5491
+ <CardContent class="grid gap-5 pb-8">
5492
+ @if (message) {
5493
+ <div class="border-border bg-muted/40 grid gap-1 rounded-lg border p-3 text-left">
5494
+ <span class="text-muted-foreground text-[0.7rem] font-medium uppercase tracking-wider">
5495
+ Details
5496
+ </span>
5497
+ <p class="text-foreground text-sm break-words" role="alert">{{ message }}</p>
5498
+ </div>
5499
+ }
5500
+
5501
+ <div class="flex justify-center">
5502
+ <button variant="primary" (click)="goHome()">
5503
+ <svg
5504
+ xmlns="http://www.w3.org/2000/svg"
5505
+ class="mr-2 h-4 w-4"
5506
+ viewBox="0 0 24 24"
5507
+ fill="none"
5508
+ stroke="currentColor"
5509
+ stroke-width="2"
5510
+ stroke-linecap="round"
5511
+ stroke-linejoin="round"
5512
+ aria-hidden="true">
5513
+ <path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
5514
+ <path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
5515
+ </svg>
5516
+ Go to homepage
5517
+ </button>
5518
+ </div>
5519
+ </CardContent>
5520
+ </Card>
5376
5521
  `
5377
5522
  }]
5378
5523
  }] });
@@ -10588,6 +10733,96 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10588
10733
  }]
10589
10734
  }], 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"] }] } });
10590
10735
 
10736
+ /**
10737
+ * Post-logout confirmation shown on the `/logout` route.
10738
+ *
10739
+ * Rendering this view (instead of the sign-in form) is essential for the external-auth modes
10740
+ * (`sso` / `oauth` / `saml`): the sign-in form auto-initiates the handshake when
10741
+ * `authMode` is external, so showing it on `/logout` would immediately re-authenticate the user
10742
+ * (a spinner that loops back in). This view gives a clear "signed out" state and a single explicit
10743
+ * action to sign in again.
10744
+ */
10745
+ class SignedOutComponent {
10746
+ router = inject(Router);
10747
+ /**
10748
+ * Navigate to the login screen, which then drives the normal sign-in handshake.
10749
+ *
10750
+ * A `returnUrl` is required: in the external-auth modes the sign-in screen only navigates away
10751
+ * once the handshake completes IF a `returnUrl` is present (otherwise it stays on the loader).
10752
+ * We send the user back to the app root after signing in again.
10753
+ */
10754
+ signInAgain() {
10755
+ this.router.navigate([globalConfig.loginPath ?? "/login"], {
10756
+ queryParams: { returnUrl: "/" }
10757
+ });
10758
+ }
10759
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SignedOutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10760
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: SignedOutComponent, isStandalone: true, selector: "signed-out, SignedOut, signedout", providers: [provideTranslocoScope("login")], ngImport: i0, template: `
10761
+ <Card hover="no" class="bg-card rounded-xl shadow-sm">
10762
+ <CardHeader class="flex flex-col items-center gap-3">
10763
+ <img class="h-12 content-(--logo-large)" alt="logo" />
10764
+ <h2 class="text-lg font-semibold">
10765
+ {{ 'login.signedOutTitle' | transloco }}
10766
+ </h2>
10767
+ <p class="text-xs text-muted-foreground text-center">
10768
+ {{ 'login.signedOutDescription' | transloco }}
10769
+ </p>
10770
+ </CardHeader>
10771
+
10772
+ <CardContent />
10773
+
10774
+ <CardFooter class="mt-4 flex justify-center">
10775
+ <button
10776
+ type="button"
10777
+ class="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
10778
+ (click)="signInAgain()"
10779
+ >
10780
+ {{ 'login.connect' | transloco }}
10781
+ </button>
10782
+ </CardFooter>
10783
+ </Card>
10784
+ `, isInline: true, dependencies: [{ kind: "directive", type: CardComponent, selector: ".card, card, Card", inputs: ["class", "variant", "hover"] }, { kind: "directive", type: CardHeaderComponent, selector: ".card-header, card-header, CardHeader, cardheader", inputs: ["class"] }, { kind: "directive", type: CardContentComponent, selector: ".card-content, card-content, CardContent, cardcontent", inputs: ["class"] }, { kind: "directive", type: CardFooterComponent, selector: ".card-footer, card-footer, CardFooter, cardfooter", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
10785
+ }
10786
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SignedOutComponent, decorators: [{
10787
+ type: Component,
10788
+ args: [{
10789
+ selector: "signed-out, SignedOut, signedout",
10790
+ imports: [
10791
+ TranslocoPipe,
10792
+ CardComponent,
10793
+ CardHeaderComponent,
10794
+ CardContentComponent,
10795
+ CardFooterComponent
10796
+ ],
10797
+ providers: [provideTranslocoScope("login")],
10798
+ template: `
10799
+ <Card hover="no" class="bg-card rounded-xl shadow-sm">
10800
+ <CardHeader class="flex flex-col items-center gap-3">
10801
+ <img class="h-12 content-(--logo-large)" alt="logo" />
10802
+ <h2 class="text-lg font-semibold">
10803
+ {{ 'login.signedOutTitle' | transloco }}
10804
+ </h2>
10805
+ <p class="text-xs text-muted-foreground text-center">
10806
+ {{ 'login.signedOutDescription' | transloco }}
10807
+ </p>
10808
+ </CardHeader>
10809
+
10810
+ <CardContent />
10811
+
10812
+ <CardFooter class="mt-4 flex justify-center">
10813
+ <button
10814
+ type="button"
10815
+ class="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
10816
+ (click)="signInAgain()"
10817
+ >
10818
+ {{ 'login.connect' | transloco }}
10819
+ </button>
10820
+ </CardFooter>
10821
+ </Card>
10822
+ `
10823
+ }]
10824
+ }] });
10825
+
10591
10826
  /**
10592
10827
  * Represents the LoginComponent class, which is responsible for handling the login functionality.
10593
10828
  * This component is used to authenticate users and manage the user's authentication status.
@@ -10598,13 +10833,18 @@ class SignInComponent {
10598
10833
  config = globalConfig;
10599
10834
  /**
10600
10835
  * True when authentication is handled outside the credentials form — i.e. by the
10601
- * browser/proxy (`useSSO`) or by an auto-configured OAuth/SAML provider. In those
10602
- * modes this screen shows a loader instead of a login form and initiates the
10603
- * handshake automatically by calling `handleLogin()`.
10604
- */
10605
- externalAuth = !!(globalConfig.useSSO ||
10606
- globalConfig.autoOAuthProvider ||
10607
- globalConfig.autoSAMLProvider);
10836
+ * browser/proxy (`sso`) or by an auto-configured OAuth/SAML provider. In those modes
10837
+ * this screen shows a loader instead of a login form and initiates the handshake
10838
+ * automatically by calling `handleLogin()`.
10839
+ *
10840
+ * Note: the ambiguous `unknown` mode is intentionally excluded — it is resolved upstream
10841
+ * (in `login()`/`signIn()`) to either `sso` or `credentials` before this screen renders,
10842
+ * so reaching here in `unknown` should still show the form, never a dead-end loader.
10843
+ */
10844
+ externalAuth = (() => {
10845
+ const kind = globalConfig.authMode?.kind;
10846
+ return kind === "sso" || kind === "oauth" || kind === "saml";
10847
+ })();
10608
10848
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
10609
10849
  forgotPassword = output();
10610
10850
  username = model("", ...(ngDevMode ? [{ debugName: "username" }] : []));
@@ -10700,10 +10940,13 @@ class SignInComponent {
10700
10940
  this.auditService.notifyLogin();
10701
10941
  }
10702
10942
  return result;
10703
- }).catch(error => {
10704
- warn("An error occurred while logging in", error);
10943
+ }).catch((err) => {
10944
+ warn("An error occurred while logging in", err);
10705
10945
  this.auditService.notify({ type: 'Login_Denied' });
10706
- this.router.navigate(["error"]);
10946
+ // Surface the failure reason on the error page (e.g. "OAuth provider not found: identity-dev")
10947
+ // so the user/admin knows what to fix, instead of a bare generic error screen.
10948
+ const message = (err?.errorMessage ?? err?.message) || undefined;
10949
+ this.router.navigate(["error"], { queryParams: { message } });
10707
10950
  return false;
10708
10951
  });
10709
10952
  }
@@ -10718,7 +10961,16 @@ class SignInComponent {
10718
10961
  }
10719
10962
  this.auditService.notifyLogin();
10720
10963
  const { createRoutes = false } = globalConfig;
10721
- await this.applicationService.initialize(createRoutes);
10964
+ try {
10965
+ await this.applicationService.initialize(createRoutes);
10966
+ }
10967
+ catch (initErr) {
10968
+ // Authenticated, but the application failed to initialize (e.g. fetchApp failed). Surface the
10969
+ // reason on the error page rather than leaving the user stuck on the login form.
10970
+ const { errorMessage, message } = (initErr ?? {});
10971
+ this.router.navigate(["/error"], { queryParams: { message: errorMessage ?? message } });
10972
+ return;
10973
+ }
10722
10974
  this.checkPasswordExpiresSoon();
10723
10975
  const url = this.route.snapshot.queryParams["returnUrl"] || "/";
10724
10976
  this.router.navigateByUrl(url);
@@ -10885,10 +11137,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10885
11137
 
10886
11138
  class AuthPageComponent {
10887
11139
  mode = input(...(ngDevMode ? [undefined, { debugName: "mode" }] : []));
10888
- view = linkedSignal(() => this.mode() ?? "signin", ...(ngDevMode ? [{ debugName: "view" }] : []));
11140
+ view = linkedSignal(() => this.mode() ?? this.routeView(), ...(ngDevMode ? [{ debugName: "view" }] : []));
10889
11141
  username = signal("", ...(ngDevMode ? [{ debugName: "username" }] : []));
10890
11142
  alert = signal(undefined, ...(ngDevMode ? [{ debugName: "alert" }] : []));
10891
11143
  route = inject(ActivatedRoute);
11144
+ /**
11145
+ * Default view derived from the route. The `/logout` route renders the "signed out" confirmation
11146
+ * (NOT the sign-in form): in external-auth modes the form would auto-restart the handshake and
11147
+ * re-authenticate the user, defeating the logout. Everything else defaults to the sign-in form.
11148
+ */
11149
+ routeView() {
11150
+ return this.route.snapshot.routeConfig?.path === "logout" ? "signedout" : "signin";
11151
+ }
10892
11152
  constructor() {
10893
11153
  const params = this.route.snapshot.queryParams;
10894
11154
  const u = params["username"] || "";
@@ -10915,6 +11175,8 @@ class AuthPageComponent {
10915
11175
  (cancel)="view.set('signin')"
10916
11176
  (success)="view.set('signin')"
10917
11177
  />
11178
+ } @else if (view() === 'signedout') {
11179
+ <signed-out />
10918
11180
  } @else {
10919
11181
  <sign-in
10920
11182
  (changePassword)="view.set('changepassword')"
@@ -10922,14 +11184,14 @@ class AuthPageComponent {
10922
11184
  />
10923
11185
  }
10924
11186
  </div>
10925
- `, isInline: true, dependencies: [{ kind: "component", type: SignInComponent, selector: "signIn, signin, sign-in", inputs: ["class", "username", "password"], outputs: ["forgotPassword", "usernameChange", "passwordChange"] }, { kind: "component", type: ChangePasswordComponent, selector: "change-password, ChangePassword, changepassword", inputs: ["username", "alert", "redirectAfterSuccess", "redirectAfterCancel", "currentPassword", "newPassword", "confirmPassword"], outputs: ["success", "cancel", "currentPasswordChange", "newPasswordChange", "confirmPasswordChange"] }, { kind: "component", type: ForgotPasswordComponent, selector: "forgot-password, ForgotPassword, forgotpassword", inputs: ["userName"], outputs: ["cancel", "success", "userNameChange"] }] });
11187
+ `, isInline: true, dependencies: [{ kind: "component", type: SignInComponent, selector: "signIn, signin, sign-in", inputs: ["class", "username", "password"], outputs: ["forgotPassword", "usernameChange", "passwordChange"] }, { kind: "component", type: ChangePasswordComponent, selector: "change-password, ChangePassword, changepassword", inputs: ["username", "alert", "redirectAfterSuccess", "redirectAfterCancel", "currentPassword", "newPassword", "confirmPassword"], outputs: ["success", "cancel", "currentPasswordChange", "newPasswordChange", "confirmPasswordChange"] }, { kind: "component", type: ForgotPasswordComponent, selector: "forgot-password, ForgotPassword, forgotpassword", inputs: ["userName"], outputs: ["cancel", "success", "userNameChange"] }, { kind: "component", type: SignedOutComponent, selector: "signed-out, SignedOut, signedout" }] });
10926
11188
  }
10927
11189
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AuthPageComponent, decorators: [{
10928
11190
  type: Component,
10929
11191
  args: [{
10930
11192
  selector: "auth-page, AuthPage, authpage",
10931
11193
  providers: [provideTranslocoScope("login")],
10932
- imports: [SignInComponent, ChangePasswordComponent, ForgotPasswordComponent],
11194
+ imports: [SignInComponent, ChangePasswordComponent, ForgotPasswordComponent, SignedOutComponent],
10933
11195
  host: { class: "min-h-screen grid place-items-center p-6 bg-background" },
10934
11196
  template: `
10935
11197
  <div class="w-full max-w-md">
@@ -10944,6 +11206,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10944
11206
  (cancel)="view.set('signin')"
10945
11207
  (success)="view.set('signin')"
10946
11208
  />
11209
+ } @else if (view() === 'signedout') {
11210
+ <signed-out />
10947
11211
  } @else {
10948
11212
  <sign-in
10949
11213
  (changePassword)="view.set('changepassword')"
@@ -11272,50 +11536,29 @@ class OverrideUserDialogComponent {
11272
11536
  }
11273
11537
  }
11274
11538
  handleOverrideUser(username, domain) {
11275
- const { useSSO, createRoutes, useCredentials } = globalConfig;
11539
+ const { createRoutes } = globalConfig;
11276
11540
  if (username === undefined || domain === undefined) {
11277
11541
  setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11278
11542
  }
11279
11543
  else {
11280
11544
  setGlobalConfig({ userOverrideActive: true, userOverride: { username, domain } });
11281
11545
  }
11282
- // Login with the new user
11283
- if (useSSO && !useCredentials) {
11284
- this.appService
11285
- .initialize(createRoutes)
11286
- .then(() => {
11287
- const fullName = this.principalStore.fullName();
11288
- notify.success(`Welcome back ${fullName}!`, { duration: 2000 });
11289
- })
11290
- .catch((err) => {
11291
- error("An error occured while overriding (SSO - initialize)", err);
11292
- notify.error("An error occurred while overriding (SSO - initialize)", { duration: 2000 });
11293
- setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11294
- });
11295
- }
11296
- else {
11297
- login()
11298
- .then((value) => {
11299
- if (value) {
11300
- this.appService
11301
- .initialize(createRoutes)
11302
- .then(() => {
11303
- const fullName = this.principalStore.fullName();
11304
- notify.success(`Welcome back ${fullName}!`, { duration: 2000 });
11305
- })
11306
- .catch((err) => {
11307
- error("An error occured while overriding (initialize)", err);
11308
- notify.error(err.message, { duration: 2000 });
11309
- setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11310
- });
11311
- }
11312
- })
11313
- .catch((err) => {
11314
- error("An error occured while overriding (login)", err);
11315
- notify.error("An error occured while overriding (login)", { duration: 2000 });
11316
- setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11317
- });
11318
- }
11546
+ // Impersonation is header-driven: `createHeaders` adds `sinequa-override-user`/`-domain` to every
11547
+ // request while `userOverrideActive`, on top of the current (admin) session. We therefore do NOT
11548
+ // need to re-authenticate — re-initializing the stores refetches the principal/usersettings as the
11549
+ // overridden user. This works in every auth mode, including `credentials` where `login()` (without
11550
+ // credentials) is intentionally a no-op in atomic 2.0 and would otherwise skip initialization.
11551
+ this.appService
11552
+ .initialize(createRoutes)
11553
+ .then(() => {
11554
+ const fullName = this.principalStore.fullName();
11555
+ notify.success(`Welcome back ${fullName}!`, { duration: 2000 });
11556
+ })
11557
+ .catch((err) => {
11558
+ error("An error occurred while overriding (initialize)", err);
11559
+ notify.error("An error occurred while overriding (initialize)", { duration: 2000 });
11560
+ setGlobalConfig({ userOverrideActive: false, userOverride: undefined });
11561
+ });
11319
11562
  }
11320
11563
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: OverrideUserDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11321
11564
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.18", type: OverrideUserDialogComponent, isStandalone: true, selector: "override-user-dialog", inputs: { overrideUser: { classPropertyName: "overrideUser", publicName: "overrideUser", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { overrideUser: "overrideUserChange" }, providers: [provideTranslocoScope("dialogs")], viewQueries: [{ propertyName: "dialog", first: true, predicate: DialogComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
@@ -16006,8 +16249,18 @@ const bodyInterceptorFn = (request, next) => {
16006
16249
  };
16007
16250
 
16008
16251
  /**
16009
- * Interceptor function that handles HTTP 401 errors by refreshing authentication
16010
- * and retrying the original request. For 403 errors, logs out the user.
16252
+ * Marks a request that has already been retried once after a re-authentication attempt.
16253
+ * Carried on the request's HttpContext (not sent to the server) so a second 401 on the retry
16254
+ * propagates instead of triggering another sign-in — which would loop forever.
16255
+ */
16256
+ const AUTH_RETRIED = new HttpContextToken(() => false);
16257
+ /**
16258
+ * Interceptor function that handles HTTP 401 errors by refreshing authentication and retrying the
16259
+ * original request once. For 403 errors, the error is propagated as a permanent auth failure.
16260
+ *
16261
+ * The retry happens ONLY when `signIn()` reports the user is authenticated, and AT MOST once per
16262
+ * request. Without these two guards a persistent 401 (credentials required, or an endpoint that
16263
+ * keeps rejecting even after a successful CSRF handshake) would retry endlessly.
16011
16264
  *
16012
16265
  * @param request - The HTTP request object.
16013
16266
  * @param next - The HTTP handler function.
@@ -16020,14 +16273,23 @@ const errorInterceptorFn = (request, next) => {
16020
16273
  }
16021
16274
  return next(request).pipe(catchError$1((err) => {
16022
16275
  if (err.status === 401) {
16023
- error("ErrorInterceptor: 401 error detected, attempting to refresh auth and retry");
16024
- // Handle 401 by refreshing auth and retrying the request
16025
- return runInInjectionContext(injector, () => from(signIn()).pipe(switchMap$1(() => {
16026
- error("Auth refreshed, retrying original request");
16027
- // Retry the original request with cloned copy
16028
- const headersObj = createHeaders();
16029
- const headers = new HttpHeaders(headersObj);
16030
- return next(request.clone({ headers }));
16276
+ // Already retried once after a re-auth — give up to avoid an infinite loop.
16277
+ if (request.context.get(AUTH_RETRIED)) {
16278
+ error("ErrorInterceptor: 401 again after re-auth retry, giving up");
16279
+ return throwError(() => err);
16280
+ }
16281
+ error("ErrorInterceptor: 401 detected, attempting to refresh auth");
16282
+ return runInInjectionContext(injector, () => from(signIn()).pipe(switchMap$1((authenticated) => {
16283
+ // signIn() could not authenticate (e.g. credentials required → redirected to login,
16284
+ // or a provider redirect is in progress). Do NOT retry — propagate the 401 instead,
16285
+ // otherwise every subsequent 401 re-triggers sign-in and loops.
16286
+ if (!authenticated) {
16287
+ error("ErrorInterceptor: re-auth did not authenticate, not retrying");
16288
+ return throwError(() => err);
16289
+ }
16290
+ error("Auth refreshed, retrying original request once");
16291
+ const headers = new HttpHeaders(createHeaders());
16292
+ return next(request.clone({ headers, context: request.context.set(AUTH_RETRIED, true) }));
16031
16293
  }), catchError$1((signInErr) => {
16032
16294
  error("Failed to refresh auth, redirecting to login", signInErr);
16033
16295
  return throwError(() => signInErr);
@@ -16110,5 +16372,5 @@ const queryNameResolver = () => {
16110
16372
  * Generated bundle index. Do not edit.
16111
16373
  */
16112
16374
 
16113
- export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SidebarNavComponent, SignInComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectRouteNavigation, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withPrincipalFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
16375
+ export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchComponent, AggregationComponent, AggregationDateComponent, AggregationDateRangeDialogComponent, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, ArticleEntities, ArticleExtracts, ArticleLabels, ArticleSimilarDocuments, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, DRAWER_COMPONENT, DRAWER_STACK_MAX_COUNT, DateComponent, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, DrawerAdvancedFiltersComponent, DrawerComponent, DrawerNavbarComponent, DrawerPreviewComponent, DrawerService, DrawerStackComponent, DrawerStackService, DropdownInputComponent, DropdownListComponent, ErrorComponent, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, HIGHLIGHTS, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardNavigatorDirective, LabelService, LabelsEditDialog, LoadingComponent, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUserDialogComponent, PREVIEW_CONFIG, PagerComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearchDialog, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectArticleOnClickDirective, SelectionHistoryService, SelectionService, SelectionStore, ShowBookmarkDirective, SidebarNavComponent, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserProfileDialog, UserProfileFormComponent, UserProfileService, UserSettingsStore, applyThemeToNativeElement, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildQuery, debouncedSignal, errorInterceptorFn, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectRouteNavigation, processCssVars, queryNameResolver, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, toastInterceptorFn, withAggregationsFeatures, withAlertsFeatures, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withPrincipalFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
16114
16376
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map