@sinequa/atomic-angular 1.0.11 → 1.0.12

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.
@@ -3415,14 +3415,10 @@ function AuthGuard() {
3415
3415
  const { loginPath, useCredentials, useSSO } = globalConfig;
3416
3416
  if (state.url.startsWith("/login"))
3417
3417
  return true;
3418
- // If the user is not authenticated, navigate to the login page or loading page based on the authentication method
3418
+ // If the user is not authenticated, navigate to the login page.
3419
+ // The login page handles every authentication method (credentials, OAuth, SAML).
3419
3420
  if (!isAuthenticated() && !useSSO) {
3420
- if (useCredentials) {
3421
- router.navigate([loginPath], { queryParams: { returnUrl: state.url } });
3422
- }
3423
- else {
3424
- router.navigate(["loading"], { queryParams: { returnUrl: state.url } });
3425
- }
3421
+ router.navigate([loginPath], { queryParams: { returnUrl: state.url } });
3426
3422
  return false;
3427
3423
  }
3428
3424
  // If the user is authenticated, initialize the principal store if it's in the initial state
@@ -4031,7 +4027,7 @@ class NavigationService {
4031
4027
  * - Maps all router events to `RouterEvent`.
4032
4028
  * - Filters the events to only include instances of `NavigationEnd`.
4033
4029
  * - Taps into the event stream to extract the route name from the URL and notify the audit service of route changes,
4034
- * excluding the "loading" route and duplicate navigations.
4030
+ * excluding duplicate navigations.
4035
4031
  * - Updates the `urlAfterNavigation` property with the current URL after navigation.
4036
4032
  * - Shares the replayed value with a buffer size of 1 to ensure subscribers receive the latest emitted value.
4037
4033
  *
@@ -4039,8 +4035,7 @@ class NavigationService {
4039
4035
  */
4040
4036
  navigationEnd$ = this.router.events.pipe(map((event) => event), filter((event) => event instanceof NavigationEnd), tap((event) => {
4041
4037
  const url = event.url.slice(1).split("?")[0]; // Extract route name
4042
- // TODO: use a "loading" configuration from globalConfig
4043
- if (url && url !== "loading" && url !== this.urlAfterNavigation) {
4038
+ if (url && url !== this.urlAfterNavigation) {
4044
4039
  this.auditService.notifyRouteChange(url);
4045
4040
  }
4046
4041
  }), tap((event) => {
@@ -5372,6 +5367,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
5372
5367
  }]
5373
5368
  }] });
5374
5369
 
5370
+ /**
5371
+ * @deprecated This component and its `/loading` route are no longer used.
5372
+ * Authentication is handled at bootstrap by `withBootstrapApp`/`signIn`, and the
5373
+ * `AuthGuard` now redirects unauthenticated users to the login page directly.
5374
+ * Will be removed in a future major release. Do not use in new code.
5375
+ */
5375
5376
  class LoadingComponent {
5376
5377
  state = computed(() => getState(this.application), ...(ngDevMode ? [{ debugName: "state" }] : []));
5377
5378
  application = inject(ApplicationStore);
@@ -10740,6 +10741,15 @@ class SignInComponent {
10740
10741
  destroyRef;
10741
10742
  cn = cn;
10742
10743
  config = globalConfig;
10744
+ /**
10745
+ * True when authentication is handled outside the credentials form — i.e. by the
10746
+ * browser/proxy (`useSSO`) or by an auto-configured OAuth/SAML provider. In those
10747
+ * modes this screen shows a loader instead of a login form and initiates the
10748
+ * handshake automatically by calling `handleLogin()`.
10749
+ */
10750
+ externalAuth = !!(globalConfig.useSSO ||
10751
+ globalConfig.autoOAuthProvider ||
10752
+ globalConfig.autoSAMLProvider);
10743
10753
  class = input(...(ngDevMode ? [undefined, { debugName: "class" }] : []));
10744
10754
  forgotPassword = output();
10745
10755
  username = model("", ...(ngDevMode ? [{ debugName: "username" }] : []));
@@ -10761,6 +10771,30 @@ class SignInComponent {
10761
10771
  expiresSoonNotified = signal(false, ...(ngDevMode ? [{ debugName: "expiresSoonNotified" }] : []));
10762
10772
  constructor(destroyRef) {
10763
10773
  this.destroyRef = destroyRef;
10774
+ // If the user is already authenticated when landing here (e.g. page refresh on
10775
+ // /login, or an external handshake completed before this screen was created),
10776
+ // don't sit on the loader: go straight to the returnUrl.
10777
+ if (this.authenticated()) {
10778
+ const url = this.route.snapshot.queryParams["returnUrl"] || "/";
10779
+ this.router.navigateByUrl(url);
10780
+ }
10781
+ // When authentication is delegated to the browser/proxy (SSO) or an OAuth/SAML
10782
+ // provider, no credentials form is shown: this screen shows a loader and initiates
10783
+ // the handshake automatically by calling `handleLogin()`. If the handshake never
10784
+ // completes, fall back to /error after 5s; the fallback is cancelled as soon as
10785
+ // the login succeeds (the `authenticated` event then drives navigation).
10786
+ if (this.externalAuth && !this.authenticated()) {
10787
+ const timeout = setTimeout(() => {
10788
+ this.router.navigate(["/error"], {
10789
+ queryParams: { returnUrl: this.route.snapshot.queryParams["returnUrl"] }
10790
+ });
10791
+ }, 5000);
10792
+ destroyRef.onDestroy(() => clearTimeout(timeout));
10793
+ this.handleLogin().then(result => {
10794
+ if (result)
10795
+ clearTimeout(timeout);
10796
+ });
10797
+ }
10764
10798
  effect(() => {
10765
10799
  const principal = getState(this.principalStore);
10766
10800
  if (this.authenticated() && principal && !this.expiresSoonNotified()) {
@@ -10806,14 +10840,16 @@ class SignInComponent {
10806
10840
  this.router.navigate(["/login"]);
10807
10841
  }
10808
10842
  async handleLogin() {
10809
- login().then((result) => {
10843
+ return login().then((result) => {
10810
10844
  if (result) {
10811
10845
  this.auditService.notifyLogin();
10812
10846
  }
10847
+ return result;
10813
10848
  }).catch(error => {
10814
10849
  warn("An error occurred while logging in", error);
10815
10850
  this.auditService.notify({ type: 'Login_Denied' });
10816
10851
  this.router.navigate(["error"]);
10852
+ return false;
10817
10853
  });
10818
10854
  }
10819
10855
  async handleLoginWithCredentials() {
@@ -10856,7 +10892,7 @@ class SignInComponent {
10856
10892
  }
10857
10893
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SignInComponent, deps: [{ token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
10858
10894
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", 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: `
10859
- @if (!authenticated()) {
10895
+ @if (!authenticated() && !externalAuth) {
10860
10896
  <Card
10861
10897
  hover="no"
10862
10898
  cdkTrapFocus
@@ -10867,60 +10903,54 @@ class SignInComponent {
10867
10903
  </CardHeader>
10868
10904
 
10869
10905
  <CardContent class="grid gap-4">
10870
- @let useCredentials =
10871
- !config.autoOAuthProvider && !config.autoSAMLProvider;
10872
- @if (useCredentials) {
10873
- <!-- authentication using credentials -->
10874
- <div class="grid gap-2">
10875
- <label class="text-sm font-medium" for="username">{{
10876
- "login.username" | transloco
10877
- }}</label>
10878
- <input
10879
- id="username"
10880
- type="text"
10881
- required
10882
- [(ngModel)]="username"
10883
- (keydown.enter)="handleLoginWithCredentials()" />
10884
- </div>
10885
-
10886
- <div class="grid gap-2">
10887
- <label class="text-sm font-medium" for="password">{{
10888
- "login.password" | transloco
10889
- }}</label>
10890
- <input
10891
- id="password"
10892
- type="password"
10893
- required
10894
- [(ngModel)]="password"
10895
- (keydown.enter)="handleLoginWithCredentials()" />
10896
- </div>
10897
-
10898
- <span
10899
- class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10900
- role="button"
10901
- tabindex="0"
10902
- (click)="forgotPassword.emit()"
10903
- (keydown.enter)="forgotPassword.emit()">
10904
- {{ "login.forgotPassword" | transloco }}
10905
- </span>
10906
- <button variant="primary"
10907
- [disabled]="!isValid()"
10908
- (click)="handleLoginWithCredentials()">
10909
- {{ "login.connect" | transloco }}
10910
- </button>
10911
- }
10912
- @else {
10913
- <!-- authentication using OAuth or SAML provider -->
10914
- <button (click)="handleLogin()">
10915
- {{ "login.SignInWith" | transloco : { provider: config.autoOAuthProvider ? "OAuth" : "SAML" } }}
10916
- </button>
10917
- }
10906
+ <!-- authentication using credentials -->
10907
+ <div class="grid gap-2">
10908
+ <label class="text-sm font-medium" for="username">{{
10909
+ "login.username" | transloco
10910
+ }}</label>
10911
+ <input
10912
+ id="username"
10913
+ type="text"
10914
+ required
10915
+ [(ngModel)]="username"
10916
+ (keydown.enter)="handleLoginWithCredentials()" />
10917
+ </div>
10918
+
10919
+ <div class="grid gap-2">
10920
+ <label class="text-sm font-medium" for="password">{{
10921
+ "login.password" | transloco
10922
+ }}</label>
10923
+ <input
10924
+ id="password"
10925
+ type="password"
10926
+ required
10927
+ [(ngModel)]="password"
10928
+ (keydown.enter)="handleLoginWithCredentials()" />
10929
+ </div>
10930
+
10931
+ <span
10932
+ class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10933
+ role="button"
10934
+ tabindex="0"
10935
+ (click)="forgotPassword.emit()"
10936
+ (keydown.enter)="forgotPassword.emit()">
10937
+ {{ "login.forgotPassword" | transloco }}
10938
+ </span>
10939
+ <button variant="primary"
10940
+ [disabled]="!isValid()"
10941
+ (click)="handleLoginWithCredentials()">
10942
+ {{ "login.connect" | transloco }}
10943
+ </button>
10918
10944
  </CardContent>
10919
10945
  </Card>
10920
10946
  } @else {
10921
- <app-wait />
10947
+ <div class="flex h-dvh w-full items-center justify-center">
10948
+ <div class="flex flex-col items-center space-y-4">
10949
+ <span class="loader"></span>
10950
+ </div>
10951
+ </div>
10922
10952
  }
10923
- `, isInline: true, styles: ["input{background-color:var(--background)}\n"], dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size"] }, { 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: "component", type: LoadingComponent, selector: "app-wait" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
10953
+ `, isInline: true, styles: ["input{background-color:var(--background)}.loader{--w: 96px;--h: 96px;transform:rotate(45deg);perspective:1000px;border-radius:50%;width:var(--w);height:var(--h);color:#0040bf}.loader:before,.loader:after{content:\"\";display:block;position:absolute;top:0;left:0;width:inherit;height:inherit;border-radius:50%;transform:rotateX(70deg);animation:1s spin linear infinite}.loader:after{color:#ff854a;transform:rotateY(70deg);animation-delay:.4s}@keyframes spin{0%,to{box-shadow:.4em 0 0 0 currentcolor}12%{box-shadow:.4em .4em 0 0 currentcolor}25%{box-shadow:0 .4em 0 0 currentcolor}37%{box-shadow:-.4em .4em 0 0 currentcolor}50%{box-shadow:-.4em 0 0 0 currentcolor}62%{box-shadow:-.4em -.4em 0 0 currentcolor}75%{box-shadow:0 -.4em 0 0 currentcolor}87%{box-shadow:.4em -.4em 0 0 currentcolor}}\n"], dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: InputComponent, selector: "input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"], input[type=\"tel\"], input[type=\"url\"], input[type=\"time\"], input[type=\"file\"]", inputs: ["class", "variant", "decoration"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size"] }, { 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: "pipe", type: TranslocoPipe, name: "transloco" }] });
10924
10954
  }
10925
10955
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: SignInComponent, decorators: [{
10926
10956
  type: Component,
@@ -10933,10 +10963,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10933
10963
  ButtonComponent,
10934
10964
  CardComponent,
10935
10965
  CardHeaderComponent,
10936
- CardContentComponent,
10937
- LoadingComponent
10966
+ CardContentComponent
10938
10967
  ], providers: [provideTranslocoScope("login")], template: `
10939
- @if (!authenticated()) {
10968
+ @if (!authenticated() && !externalAuth) {
10940
10969
  <Card
10941
10970
  hover="no"
10942
10971
  cdkTrapFocus
@@ -10947,62 +10976,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
10947
10976
  </CardHeader>
10948
10977
 
10949
10978
  <CardContent class="grid gap-4">
10950
- @let useCredentials =
10951
- !config.autoOAuthProvider && !config.autoSAMLProvider;
10952
- @if (useCredentials) {
10953
- <!-- authentication using credentials -->
10954
- <div class="grid gap-2">
10955
- <label class="text-sm font-medium" for="username">{{
10956
- "login.username" | transloco
10957
- }}</label>
10958
- <input
10959
- id="username"
10960
- type="text"
10961
- required
10962
- [(ngModel)]="username"
10963
- (keydown.enter)="handleLoginWithCredentials()" />
10964
- </div>
10965
-
10966
- <div class="grid gap-2">
10967
- <label class="text-sm font-medium" for="password">{{
10968
- "login.password" | transloco
10969
- }}</label>
10970
- <input
10971
- id="password"
10972
- type="password"
10973
- required
10974
- [(ngModel)]="password"
10975
- (keydown.enter)="handleLoginWithCredentials()" />
10976
- </div>
10977
-
10978
- <span
10979
- class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
10980
- role="button"
10981
- tabindex="0"
10982
- (click)="forgotPassword.emit()"
10983
- (keydown.enter)="forgotPassword.emit()">
10984
- {{ "login.forgotPassword" | transloco }}
10985
- </span>
10986
- <button variant="primary"
10987
- [disabled]="!isValid()"
10988
- (click)="handleLoginWithCredentials()">
10989
- {{ "login.connect" | transloco }}
10990
- </button>
10991
- }
10992
- @else {
10993
- <!-- authentication using OAuth or SAML provider -->
10994
- <button (click)="handleLogin()">
10995
- {{ "login.SignInWith" | transloco : { provider: config.autoOAuthProvider ? "OAuth" : "SAML" } }}
10996
- </button>
10997
- }
10979
+ <!-- authentication using credentials -->
10980
+ <div class="grid gap-2">
10981
+ <label class="text-sm font-medium" for="username">{{
10982
+ "login.username" | transloco
10983
+ }}</label>
10984
+ <input
10985
+ id="username"
10986
+ type="text"
10987
+ required
10988
+ [(ngModel)]="username"
10989
+ (keydown.enter)="handleLoginWithCredentials()" />
10990
+ </div>
10991
+
10992
+ <div class="grid gap-2">
10993
+ <label class="text-sm font-medium" for="password">{{
10994
+ "login.password" | transloco
10995
+ }}</label>
10996
+ <input
10997
+ id="password"
10998
+ type="password"
10999
+ required
11000
+ [(ngModel)]="password"
11001
+ (keydown.enter)="handleLoginWithCredentials()" />
11002
+ </div>
11003
+
11004
+ <span
11005
+ class="text-muted-foreground cursor-pointer justify-self-start text-xs hover:underline"
11006
+ role="button"
11007
+ tabindex="0"
11008
+ (click)="forgotPassword.emit()"
11009
+ (keydown.enter)="forgotPassword.emit()">
11010
+ {{ "login.forgotPassword" | transloco }}
11011
+ </span>
11012
+ <button variant="primary"
11013
+ [disabled]="!isValid()"
11014
+ (click)="handleLoginWithCredentials()">
11015
+ {{ "login.connect" | transloco }}
11016
+ </button>
10998
11017
  </CardContent>
10999
11018
  </Card>
11000
11019
  } @else {
11001
- <app-wait />
11020
+ <div class="flex h-dvh w-full items-center justify-center">
11021
+ <div class="flex flex-col items-center space-y-4">
11022
+ <span class="loader"></span>
11023
+ </div>
11024
+ </div>
11002
11025
  }
11003
11026
  `, host: {
11004
11027
  "[class]": "cn('grid h-dvh w-full place-content-center', class())"
11005
- }, styles: ["input{background-color:var(--background)}\n"] }]
11028
+ }, styles: ["input{background-color:var(--background)}.loader{--w: 96px;--h: 96px;transform:rotate(45deg);perspective:1000px;border-radius:50%;width:var(--w);height:var(--h);color:#0040bf}.loader:before,.loader:after{content:\"\";display:block;position:absolute;top:0;left:0;width:inherit;height:inherit;border-radius:50%;transform:rotateX(70deg);animation:1s spin linear infinite}.loader:after{color:#ff854a;transform:rotateY(70deg);animation-delay:.4s}@keyframes spin{0%,to{box-shadow:.4em 0 0 0 currentcolor}12%{box-shadow:.4em .4em 0 0 currentcolor}25%{box-shadow:0 .4em 0 0 currentcolor}37%{box-shadow:-.4em .4em 0 0 currentcolor}50%{box-shadow:-.4em 0 0 0 currentcolor}62%{box-shadow:-.4em -.4em 0 0 currentcolor}75%{box-shadow:0 -.4em 0 0 currentcolor}87%{box-shadow:.4em -.4em 0 0 currentcolor}}\n"] }]
11006
11029
  }], ctorParameters: () => [{ type: i0.DestroyRef }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], forgotPassword: [{ type: i0.Output, args: ["forgotPassword"] }], username: [{ type: i0.Input, args: [{ isSignal: true, alias: "username", required: false }] }, { type: i0.Output, args: ["usernameChange"] }], password: [{ type: i0.Input, args: [{ isSignal: true, alias: "password", required: false }] }, { type: i0.Output, args: ["passwordChange"] }] } });
11007
11030
 
11008
11031
  class AuthPageComponent {