This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 34609af010 feat: Add optional terms acknowledgment (#3754)
34609af010 is described below

commit 34609af0104d3dadaf99837606fea560e72129f5
Author: Dominik Riemer <[email protected]>
AuthorDate: Sat Aug 30 19:11:29 2025 +0200

    feat: Add optional terms acknowledgment (#3754)
    
    * feat: Add configurable screen to manage and acknowledge terms
    
    * fix bug in user form
    
    * Fix rat
---
 .../streampipes/model/client/user/UserAccount.java |   9 +
 .../org/apache/streampipes/model/UserInfo.java     |   9 +
 .../model/configuration/GeneralConfig.java         |   9 +
 .../model/configuration/UserAcknowledgment.java    |  15 +-
 .../streampipes/rest/impl/Authentication.java      |   7 +
 .../apache/streampipes/rest/impl/UserResource.java |  31 +-
 .../user/management/util/UserInfoUtil.java         |   1 +
 ui/deployment/app-routing.module.mst               |   6 +-
 ui/deployment/base-navigation.component.mst        |  11 +-
 .../src/lib/model/config/general-config.model.ts   |   7 +
 .../src/lib/model/gen/streampipes-model-client.ts  |   6 +-
 .../src/lib/model/gen/streampipes-model.ts         |   4 +-
 .../_guards/auth.can-activate-children.guard.ts    |   3 +-
 ...hildren.guard.ts => auth.can-activate.guard.ts} |  23 +-
 .../_guards/base-configured.can-activate.guard.ts  |   3 +-
 .../_guards/terms.can-activate-children.guard.ts   |  62 ++++
 ui/src/app/configuration/configuration.module.ts   |   4 +
 .../general-configuration.component.html           |   4 +-
 .../general-configuration.component.ts             |  85 +++--
 .../link-settings/link-settings.component.html     |   4 +-
 .../user-acknowledgment.component.html             |  42 +++
 .../user-acknowledgment.component.ts}              |  35 ++-
 .../edit-user-dialog.component.html                | 342 +++++++++++----------
 .../edit-user-dialog/edit-user-dialog.component.ts | 145 ++++++---
 .../core/components/iconbar/iconbar.component.ts   |  13 -
 .../core/components/toolbar/toolbar.component.ts   |  24 +-
 .../activate-account/activate-account.component.ts |  14 +-
 .../login/components/base-login-page.directive.ts  |   6 +-
 .../login/components/login/login.component.html    | 196 ++++++------
 .../app/login/components/login/login.component.ts  |  33 +-
 ui/src/app/login/components/login/login.model.ts   |   3 +
 .../components/register/register.component.ts      |  10 +-
 .../restore-password/restore-password.component.ts |  10 +-
 .../set-new-password/set-new-password.component.ts |  16 +-
 .../login/components/terms/terms.component.html    |  50 +++
 .../login.model.ts => terms/terms.component.scss}  |  35 ++-
 .../app/login/components/terms/terms.component.ts  | 102 ++++++
 ui/src/app/login/login.module.ts                   |   2 +
 ui/src/app/login/services/login.service.ts         |  16 +-
 39 files changed, 886 insertions(+), 511 deletions(-)

diff --git 
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
 
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
index 6df8e62555..8fc20b0f0f 100644
--- 
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
+++ 
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
@@ -40,6 +40,7 @@ public class UserAccount extends Principal {
 
   protected boolean hideTutorial;
   protected boolean darkMode = false;
+  protected boolean hasAcknowledged = false;
 
   /**
    * The authentication provider (LOCAL or one of the configured OAuth 
providers
@@ -181,4 +182,12 @@ public class UserAccount extends Principal {
   public void setExternallyManagedRoles(boolean externallyManagedRoles) {
     this.externallyManagedRoles = externallyManagedRoles;
   }
+
+  public boolean isHasAcknowledged() {
+    return hasAcknowledged;
+  }
+
+  public void setHasAcknowledged(boolean hasAcknowledged) {
+    this.hasAcknowledged = hasAcknowledged;
+  }
 }
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/UserInfo.java 
b/streampipes-model/src/main/java/org/apache/streampipes/model/UserInfo.java
index 9a8b200ae5..b2298160cc 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/UserInfo.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/UserInfo.java
@@ -30,6 +30,7 @@ public class UserInfo {
   private Set<String> roles;
   private boolean showTutorial;
   private boolean darkMode;
+  private boolean hasAcknowledged;
 
   public UserInfo() {
   }
@@ -73,4 +74,12 @@ public class UserInfo {
   public void setDarkMode(boolean darkMode) {
     this.darkMode = darkMode;
   }
+
+  public boolean isHasAcknowledged() {
+    return hasAcknowledged;
+  }
+
+  public void setHasAcknowledged(boolean hasAcknowledged) {
+    this.hasAcknowledged = hasAcknowledged;
+  }
 }
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/GeneralConfig.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/GeneralConfig.java
index 188590a196..3e37848f90 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/GeneralConfig.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/GeneralConfig.java
@@ -32,6 +32,7 @@ public class GeneralConfig {
 
   private List<String> defaultUserRoles;
   private LinkSettings linkSettings;
+  private UserAcknowledgment userAcknowledgment;
 
   public GeneralConfig() {
   }
@@ -113,4 +114,12 @@ public class GeneralConfig {
   public void setLinkSettings(LinkSettings linkSettings) {
     this.linkSettings = linkSettings;
   }
+
+  public UserAcknowledgment getUserAcknowledgment() {
+    return userAcknowledgment;
+  }
+
+  public void setUserAcknowledgment(UserAcknowledgment userAcknowledgment) {
+    this.userAcknowledgment = userAcknowledgment;
+  }
 }
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/config/general-config.model.ts
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/UserAcknowledgment.java
similarity index 69%
copy from 
ui/projects/streampipes/platform-services/src/lib/model/config/general-config.model.ts
copy to 
streampipes-model/src/main/java/org/apache/streampipes/model/configuration/UserAcknowledgment.java
index fbc6db7798..a13aa00a72 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/config/general-config.model.ts
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/configuration/UserAcknowledgment.java
@@ -16,16 +16,9 @@
  *
  */
 
-import { LinkSettings } from '../gen/streampipes-model';
+package org.apache.streampipes.model.configuration;
 
-export interface GeneralConfigModel {
-    hostname: string;
-    port: number;
-    protocol: 'http' | 'https';
-    configured: boolean;
-    allowPasswordRecovery: boolean;
-    allowSelfRegistration: boolean;
-    defaultUserRoles: string[];
-    appName: string;
-    linkSettings: LinkSettings;
+public record UserAcknowledgment(boolean required,
+                                 String title,
+                                 String text) {
 }
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
index 214651e97f..dc2d6d252a 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
@@ -137,11 +137,18 @@ public class Authentication extends AbstractRestResource {
       produces = org.springframework.http.MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<Map<String, Object>> getAuthSettings() {
     GeneralConfig config = 
getSpCoreConfigurationStorage().get().getGeneralConfig();
+    var termsAcknowledgmentRequired = config.getUserAcknowledgment() != null
+        && config.getUserAcknowledgment().required();
     Map<String, Object> response = new HashMap<>();
     response.put("allowSelfRegistration", config.isAllowSelfRegistration());
     response.put("allowPasswordRecovery", config.isAllowPasswordRecovery());
     response.put("linkSettings", config.getLinkSettings());
     response.put("oAuthSettings", makeOAuthSettings());
+    response.put("termsAcknowledgmentRequired", termsAcknowledgmentRequired);
+    if (termsAcknowledgmentRequired) {
+      response.put("termsAcknowledgmentTitle", 
config.getUserAcknowledgment().title());
+      response.put("termsAcknowledgmentText", 
config.getUserAcknowledgment().text());
+    }
 
     return ok(response);
   }
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
index 7a70277d50..462153118d 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
@@ -72,25 +72,34 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
 
   @GetMapping(path = "{principalId}", produces = 
MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<?> getUserDetails(@PathVariable("principalId") String 
principalId) {
-    Principal principal = getPrincipalById(principalId);
-    Utils.removeCredentials(principal);
+    if (principalId.equals(getAuthenticatedUserSid()) || isAdmin()) {
+      Principal principal = getPrincipalById(principalId);
+      Utils.removeCredentials(principal);
 
-    if (principal != null) {
-      return ok(principal);
+      if (principal != null) {
+        return ok(principal);
+      } else {
+        return statusMessage(Notifications.error("User not found"));
+      }
     } else {
-      return statusMessage(Notifications.error("User not found"));
+      return badRequest();
     }
   }
 
   @GetMapping(path = "username/{username}", produces = 
MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<?> getUserDetailsByName(@PathVariable("username") 
String username) {
-    Principal principal = getPrincipal(username);
-    Utils.removeCredentials(principal);
+    var authenticatedPrincipal = getPrincipal();
+    if (username.equals(authenticatedPrincipal.getUsername()) || isAdmin()) {
+      Principal principal = getPrincipalByUsername(username);
+      Utils.removeCredentials(principal);
 
-    if (principal != null) {
-      return ok(principal);
+      if (principal != null) {
+        return ok(principal);
+      } else {
+        return statusMessage(Notifications.error("User not found"));
+      }
     } else {
-      return statusMessage(Notifications.error("User not found"));
+      return badRequest();
     }
   }
 
@@ -356,7 +365,7 @@ public class UserResource extends 
AbstractAuthGuardedRestResource {
     return getUserStorage().getUserAccount(username);
   }
 
-  private Principal getPrincipal(String username) {
+  private Principal getPrincipalByUsername(String username) {
     return getUserStorage().getUser(username);
   }
 
diff --git 
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/UserInfoUtil.java
 
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/UserInfoUtil.java
index 4849bf3e59..b06bae827f 100644
--- 
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/UserInfoUtil.java
+++ 
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/UserInfoUtil.java
@@ -37,6 +37,7 @@ public class UserInfoUtil {
                                      Set<String> roles) {
     UserInfo userInfo = prepareUserInfo(userAccount, roles);
     userInfo.setShowTutorial(!userAccount.isHideTutorial());
+    userInfo.setHasAcknowledged(userAccount.isHasAcknowledged());
     return userInfo;
   }
 
diff --git a/ui/deployment/app-routing.module.mst 
b/ui/deployment/app-routing.module.mst
index e3e44722fa..262a344edb 100644
--- a/ui/deployment/app-routing.module.mst
+++ b/ui/deployment/app-routing.module.mst
@@ -39,6 +39,9 @@ import { RestorePasswordAllowedCanActivateGuard } from 
'./_guards/restore-passwo
 import { SetNewPasswordComponent } from 
'./login/components/set-new-password/set-new-password.component';
 import { ActivateAccountComponent } from 
'./login/components/activate-account/activate-account.component';
 import { UserPrivilege } from './_enums/user-privilege.enum';
+import { TermsComponent } from './login/components/terms/terms.component';
+import { AuthCanActivateGuard } from './_guards/auth.can-activate.guard';
+import { TermsCanActivateChildrenGuard } from 
'./_guards/terms.can-activate-children.guard';
 
 {{#modulesActive}}
 {{#componentImport}}
@@ -52,6 +55,7 @@ const routes: Routes = [
   data: {animation: 'LoginPage'}},
   { path: 'dashboard-kiosk', loadChildren: () => 
import('./dashboard-kiosk/dashboard-kiosk.module').then(m => 
m.DashboardKioskModule), canActivate: [ConfiguredCanActivateGuard]},
   { path: 'register', component: RegisterComponent, canActivate: 
[RegistrationAllowedCanActivateGuard] },
+  { path: 'terms', component: TermsComponent, canActivate: 
[AuthCanActivateGuard] },
   { path: 'activate-account', component: ActivateAccountComponent, 
canActivate: [RegistrationAllowedCanActivateGuard] },
   { path: 'restore-password', component: RestorePasswordComponent, 
canActivate: [RestorePasswordAllowedCanActivateGuard] },
   { path: 'set-new-password', component: SetNewPasswordComponent, canActivate: 
[RestorePasswordAllowedCanActivateGuard] },
@@ -69,7 +73,7 @@ const routes: Routes = [
       { path: 'notifications', component: NotificationsComponent },
       { path: 'info', component: InfoComponent },
       { path: 'profile', component: ProfileComponent},
-    ], canActivateChild: [AuthCanActivateChildrenGuard, PageAuthGuard] }
+    ], canActivateChild: [AuthCanActivateChildrenGuard, PageAuthGuard, 
TermsCanActivateChildrenGuard] }
 ];
 
 @NgModule({
diff --git a/ui/deployment/base-navigation.component.mst 
b/ui/deployment/base-navigation.component.mst
index 1d4593444a..cf4a84b36d 100644
--- a/ui/deployment/base-navigation.component.mst
+++ b/ui/deployment/base-navigation.component.mst
@@ -23,6 +23,7 @@ import { AuthService } from '../../services/auth.service';
 import { CurrentUserService } from '@streampipes/shared-ui';
 import { AppConstants } from '../../services/app.constants';
 import { UserPrivilege } from '../../_enums/user-privilege.enum';
+import { inject } from '@angular/core';
 
 export abstract class BaseNavigationComponent {
 
@@ -47,12 +48,10 @@ export abstract class BaseNavigationComponent {
   notificationsVisible = false;
 
 
-  constructor(protected authService: AuthService,
-              protected currentUserService: CurrentUserService,
-              protected router: Router,
-              private appConstants: AppConstants) {
-
-    }
+   protected authService = inject(AuthService);
+   protected currentUserService = inject(CurrentUserService);
+   protected router = inject(Router);
+   protected appConstants = inject(AppConstants);
 
     onInit() {
       this.currentUserService.user$.subscribe(user => {
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/config/general-config.model.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/config/general-config.model.ts
index fbc6db7798..512b2d0913 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/config/general-config.model.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/config/general-config.model.ts
@@ -18,6 +18,12 @@
 
 import { LinkSettings } from '../gen/streampipes-model';
 
+export interface UserAcknowledgment {
+    required: boolean;
+    title: string;
+    text: string;
+}
+
 export interface GeneralConfigModel {
     hostname: string;
     port: number;
@@ -28,4 +34,5 @@ export interface GeneralConfigModel {
     defaultUserRoles: string[];
     appName: string;
     linkSettings: LinkSettings;
+    userAcknowledgment: UserAcknowledgment;
 }
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
index bb58115952..2d5224365b 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
@@ -20,7 +20,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2025-08-21 
14:22:05.
+// Generated using typescript-generator version 3.2.1263 on 2025-08-27 
16:31:50.
 
 import { Storable } from './streampipes-model';
 
@@ -244,12 +244,14 @@ export class UserAccount extends Principal {
     darkMode: boolean;
     externallyManagedRoles: boolean;
     fullName: string;
+    hasAcknowledged: boolean;
     hideTutorial: boolean;
     password: string;
     preferredDataProcessors: string[];
     preferredDataSinks: string[];
     preferredDataStreams: string[];
     provider: string;
+    shouldAcknowledge: boolean;
     userApiTokens: UserApiToken[];
 
     static fromData(data: UserAccount, target?: UserAccount): UserAccount {
@@ -261,6 +263,7 @@ export class UserAccount extends Principal {
         instance.darkMode = data.darkMode;
         instance.externallyManagedRoles = data.externallyManagedRoles;
         instance.fullName = data.fullName;
+        instance.hasAcknowledged = data.hasAcknowledged;
         instance.hideTutorial = data.hideTutorial;
         instance.password = data.password;
         instance.preferredDataProcessors = __getCopyArrayFn(
@@ -273,6 +276,7 @@ export class UserAccount extends Principal {
             data.preferredDataStreams,
         );
         instance.provider = data.provider;
+        instance.shouldAcknowledge = data.shouldAcknowledge;
         instance.userApiTokens = __getCopyArrayFn(UserApiToken.fromData)(
             data.userApiTokens,
         );
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index acc60b0040..7c6b3efd8f 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -20,7 +20,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2025-08-20 
10:54:16.
+// Generated using typescript-generator version 3.2.1263 on 2025-08-27 
21:55:36.
 
 export class NamedStreamPipesEntity implements Storable {
     '@class':
@@ -4094,6 +4094,7 @@ export class UserDefinedOutputStrategy extends 
OutputStrategy {
 export class UserInfo {
     darkMode: boolean;
     displayName: string;
+    hasAcknowledged: boolean;
     roles: string[];
     showTutorial: boolean;
     username: string;
@@ -4105,6 +4106,7 @@ export class UserInfo {
         const instance = target || new UserInfo();
         instance.darkMode = data.darkMode;
         instance.displayName = data.displayName;
+        instance.hasAcknowledged = data.hasAcknowledged;
         instance.roles = __getCopyArrayFn(__identity<string>())(data.roles);
         instance.showTutorial = data.showTutorial;
         instance.username = data.username;
diff --git a/ui/src/app/_guards/auth.can-activate-children.guard.ts 
b/ui/src/app/_guards/auth.can-activate-children.guard.ts
index dfab23656e..ea301bc404 100644
--- a/ui/src/app/_guards/auth.can-activate-children.guard.ts
+++ b/ui/src/app/_guards/auth.can-activate-children.guard.ts
@@ -19,13 +19,14 @@
 import { Injectable } from '@angular/core';
 import {
     ActivatedRouteSnapshot,
+    CanActivateChild,
     Router,
     RouterStateSnapshot,
 } from '@angular/router';
 import { AuthService } from '../services/auth.service';
 
 @Injectable()
-export class AuthCanActivateChildrenGuard {
+export class AuthCanActivateChildrenGuard implements CanActivateChild {
     constructor(
         private authService: AuthService,
         private router: Router,
diff --git a/ui/src/app/_guards/auth.can-activate-children.guard.ts 
b/ui/src/app/_guards/auth.can-activate.guard.ts
similarity index 77%
copy from ui/src/app/_guards/auth.can-activate-children.guard.ts
copy to ui/src/app/_guards/auth.can-activate.guard.ts
index dfab23656e..9af91a2acd 100644
--- a/ui/src/app/_guards/auth.can-activate-children.guard.ts
+++ b/ui/src/app/_guards/auth.can-activate.guard.ts
@@ -16,25 +16,26 @@
  *
  */
 
-import { Injectable } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
+import { AuthService } from '../services/auth.service';
 import {
     ActivatedRouteSnapshot,
+    CanActivate,
+    GuardResult,
+    MaybeAsync,
     Router,
     RouterStateSnapshot,
 } from '@angular/router';
-import { AuthService } from '../services/auth.service';
 
-@Injectable()
-export class AuthCanActivateChildrenGuard {
-    constructor(
-        private authService: AuthService,
-        private router: Router,
-    ) {}
+@Injectable({ providedIn: 'root' })
+export class AuthCanActivateGuard implements CanActivate {
+    private authService = inject(AuthService);
+    private router = inject(Router);
 
-    canActivateChild(
-        childRoute: ActivatedRouteSnapshot,
+    canActivate(
+        route: ActivatedRouteSnapshot,
         state: RouterStateSnapshot,
-    ): boolean {
+    ): MaybeAsync<GuardResult> {
         if (this.authService.authenticated()) {
             return true;
         }
diff --git a/ui/src/app/_guards/base-configured.can-activate.guard.ts 
b/ui/src/app/_guards/base-configured.can-activate.guard.ts
index 2843d39c5e..79576b3187 100644
--- a/ui/src/app/_guards/base-configured.can-activate.guard.ts
+++ b/ui/src/app/_guards/base-configured.can-activate.guard.ts
@@ -18,6 +18,7 @@
 
 import {
     ActivatedRouteSnapshot,
+    CanActivate,
     Router,
     RouterStateSnapshot,
     UrlTree,
@@ -25,7 +26,7 @@ import {
 import { Observable } from 'rxjs';
 import { AuthService } from '../services/auth.service';
 
-export abstract class BaseConfiguredCanActivateGuard {
+export abstract class BaseConfiguredCanActivateGuard implements CanActivate {
     constructor(
         protected router: Router,
         protected authService: AuthService,
diff --git a/ui/src/app/_guards/terms.can-activate-children.guard.ts 
b/ui/src/app/_guards/terms.can-activate-children.guard.ts
new file mode 100644
index 0000000000..efbdd17799
--- /dev/null
+++ b/ui/src/app/_guards/terms.can-activate-children.guard.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { inject, Injectable } from '@angular/core';
+import {
+    ActivatedRouteSnapshot,
+    CanActivateChild,
+    GuardResult,
+    MaybeAsync,
+    Router,
+    RouterStateSnapshot,
+} from '@angular/router';
+import { CurrentUserService } from '@streampipes/shared-ui';
+import { LoginService } from '../login/services/login.service';
+import { of, take } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+
+@Injectable({ providedIn: 'root' })
+export class TermsCanActivateChildrenGuard implements CanActivateChild {
+    canActivateChild(
+        childRoute: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot,
+    ): MaybeAsync<GuardResult> {
+        const currentUser = this.currentUserService.getCurrentUser();
+        return this.loginService.fetchLoginSettings().pipe(
+            take(1),
+            map(settings => {
+                const needsAck =
+                    settings.termsAcknowledgmentRequired &&
+                    !currentUser?.hasAcknowledged;
+
+                if (needsAck) {
+                    return this.router.createUrlTree(['/terms'], {
+                        queryParams: { returnUrl: state.url },
+                    });
+                }
+                return true;
+            }),
+            catchError(() => of(true)),
+        );
+    }
+
+    private currentUserService = inject(CurrentUserService);
+    private loginService = inject(LoginService);
+
+    private router = inject(Router);
+}
diff --git a/ui/src/app/configuration/configuration.module.ts 
b/ui/src/app/configuration/configuration.module.ts
index 14ad6a1fac..9b1bfd1cff 100644
--- a/ui/src/app/configuration/configuration.module.ts
+++ b/ui/src/app/configuration/configuration.module.ts
@@ -103,6 +103,8 @@ import { TranslateModule } from '@ngx-translate/core';
 import { CertificateConfigurationComponent } from 
'./extensions-service-management/certificate-configuration/certificate-configuration.component';
 import { CertificateDetailsDialogComponent } from 
'./dialog/certificate-details/certificate-details-dialog.component';
 import { AlternateIdConfigurationComponent } from 
'./security-configuration/alternate-id-configuration/alternate-id-configuration.component';
+import { UserAcknowledgmentComponent } from 
'./general-configuration/user-acknowledgement/user-acknowledgment.component';
+import { QuillEditorComponent } from 'ngx-quill';
 
 @NgModule({
     imports: [
@@ -203,6 +205,7 @@ import { AlternateIdConfigurationComponent } from 
'./security-configuration/alte
         MatListModule,
         MatDialogModule,
         TranslateModule.forChild({}),
+        QuillEditorComponent,
     ],
     declarations: [
         ServiceConfigsComponent,
@@ -261,6 +264,7 @@ import { AlternateIdConfigurationComponent } from 
'./security-configuration/alte
         CertificateConfigurationComponent,
         CertificateDetailsDialogComponent,
         AlternateIdConfigurationComponent,
+        UserAcknowledgmentComponent,
     ],
     providers: [
         OrderByPipe,
diff --git 
a/ui/src/app/configuration/general-configuration/general-configuration.component.html
 
b/ui/src/app/configuration/general-configuration/general-configuration.component.html
index 945171769a..3a957a8217 100644
--- 
a/ui/src/app/configuration/general-configuration/general-configuration.component.html
+++ 
b/ui/src/app/configuration/general-configuration/general-configuration.component.html
@@ -134,6 +134,8 @@
                 </sp-split-section>
                 <sp-configuration-link-settings [parentForm]="parentForm">
                 </sp-configuration-link-settings>
+                <sp-user-acknowledgment [parentForm]="parentForm">
+                </sp-user-acknowledgment>
                 <sp-split-section>
                     <div class="mt-10">
                         <button
@@ -142,7 +144,7 @@
                             color="accent"
                             (click)="updateConfig()"
                             style="margin-right: 10px"
-                            [disabled]="!parentForm.valid"
+                            [disabled]="parentForm.invalid"
                             data-cy="sp-element-general-config-save"
                         >
                             <i class="material-icons">save</i
diff --git 
a/ui/src/app/configuration/general-configuration/general-configuration.component.ts
 
b/ui/src/app/configuration/general-configuration/general-configuration.component.ts
index 2be55f19fe..64378f127c 100644
--- 
a/ui/src/app/configuration/general-configuration/general-configuration.component.ts
+++ 
b/ui/src/app/configuration/general-configuration/general-configuration.component.ts
@@ -97,6 +97,11 @@ export class GeneralConfigurationComponent implements OnInit 
{
                     defaultUserRoles: [UserRole.ROLE_PIPELINE_USER],
                     appName: this.appConstants.APP_NAME,
                     linkSettings: configs[0].linkSettings,
+                    userAcknowledgment: {
+                        required: false,
+                        title: '',
+                        text: '',
+                    },
                 };
             }
             this.mailConfig = configs[1];
@@ -186,30 +191,26 @@ export class GeneralConfigurationComponent implements 
OnInit {
                 ),
             );
 
-            this.parentForm.valueChanges.subscribe(v => {
-                this.generalConfig.appName = v.appName;
-                this.generalConfig.protocol = v.protocol;
-                this.generalConfig.port = v.port;
-                this.generalConfig.hostname = v.hostname;
-                this.generalConfig.allowPasswordRecovery =
-                    v.allowPasswordRecovery;
-                this.generalConfig.allowSelfRegistration =
-                    v.allowSelfRegistration;
-                this.generalConfig.defaultUserRoles = v.defaultUserRoles.map(
-                    r => UserRole[r],
-                );
-                this.generalConfig.linkSettings.documentationUrl =
-                    v.documentationUrl;
-                this.generalConfig.linkSettings.supportUrl = v.supportUrl;
-                
this.generalConfig.linkSettings.showApiDocumentationLinkOnStartScreen =
-                    v.showApiDocumentationLinkOnStartScreen;
-                this.generalConfig.linkSettings.showSupportUrlOnStartScreen =
-                    v.showSupportUrlOnStartScreen;
-                
this.generalConfig.linkSettings.showDocumentationLinkInProfileMenu =
-                    v.showDocumentationLinkInProfileMenu;
-                
this.generalConfig.linkSettings.showDocumentationLinkOnStartScreen =
-                    v.showDocumentationLinkOnStartScreen;
-            });
+            this.parentForm.addControl(
+                'requireTermsAcknowledgment',
+                new UntypedFormControl(
+                    this.generalConfig.userAcknowledgment?.required || false,
+                ),
+            );
+
+            this.parentForm.addControl(
+                'termsAcknowledgmentTitle',
+                new UntypedFormControl(
+                    this.generalConfig.userAcknowledgment?.title || '',
+                ),
+            );
+
+            this.parentForm.addControl(
+                'termsAcknowledgmentText',
+                new UntypedFormControl(
+                    this.generalConfig.userAcknowledgment?.text || '',
+                ),
+            );
 
             this.formReady = true;
         });
@@ -222,6 +223,42 @@ export class GeneralConfigurationComponent implements 
OnInit {
     }
 
     updateConfig() {
+        const formValue = this.parentForm.getRawValue();
+        const toUserRole = (r: string | number) =>
+            typeof r === 'number'
+                ? r
+                : UserRole[r as keyof typeof UserRole] ?? r;
+
+        this.generalConfig = {
+            ...this.generalConfig,
+            appName: formValue.appName,
+            protocol: formValue.protocol,
+            port: formValue.port,
+            hostname: formValue.hostname,
+            allowPasswordRecovery: formValue.allowPasswordRecovery,
+            allowSelfRegistration: formValue.allowSelfRegistration,
+            defaultUserRoles: (formValue.defaultUserRoles || []).map(
+                toUserRole,
+            ),
+            linkSettings: {
+                documentationUrl: formValue.documentationUrl,
+                supportUrl: formValue.supportUrl,
+                showApiDocumentationLinkOnStartScreen:
+                    formValue.showApiDocumentationLinkOnStartScreen,
+                showSupportUrlOnStartScreen:
+                    formValue.showSupportUrlOnStartScreen,
+                showDocumentationLinkInProfileMenu:
+                    formValue.showDocumentationLinkInProfileMenu,
+                showDocumentationLinkOnStartScreen:
+                    formValue.showDocumentationLinkOnStartScreen,
+            },
+            userAcknowledgment: {
+                required: formValue.requireTermsAcknowledgment,
+                title: formValue.termsAcknowledgmentTitle,
+                text: formValue.termsAcknowledgmentText,
+            },
+        };
+
         this.generalConfigService
             .updateGeneralConfig(this.generalConfig)
             .subscribe(result => {
diff --git 
a/ui/src/app/configuration/general-configuration/link-settings/link-settings.component.html
 
b/ui/src/app/configuration/general-configuration/link-settings/link-settings.component.html
index 673315bf9b..1ef3b1d032 100644
--- 
a/ui/src/app/configuration/general-configuration/link-settings/link-settings.component.html
+++ 
b/ui/src/app/configuration/general-configuration/link-settings/link-settings.component.html
@@ -22,7 +22,7 @@
     [formGroup]="parentForm"
 >
     <div class="subsection-title">Documentation Link</div>
-    <mat-form-field color="accent" class="ml-10">
+    <mat-form-field color="accent">
         <mat-label>Documentation URL</mat-label>
         <input formControlName="documentationUrl" fxFlex matInput />
     </mat-form-field>
@@ -39,7 +39,7 @@
     </mat-checkbox>
 
     <div class="subsection-title mt-10">Support Link</div>
-    <mat-form-field color="accent" class="ml-10">
+    <mat-form-field color="accent">
         <mat-label>Support URL</mat-label>
         <input formControlName="supportUrl" fxFlex matInput />
     </mat-form-field>
diff --git 
a/ui/src/app/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.html
 
b/ui/src/app/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.html
new file mode 100644
index 0000000000..df1921e752
--- /dev/null
+++ 
b/ui/src/app/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.html
@@ -0,0 +1,42 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+<sp-split-section
+    title="Terms"
+    subtitle="Terms acknowledgment after login"
+    [formGroup]="parentForm"
+>
+    <mat-checkbox formControlName="requireTermsAcknowledgment"
+        >Require users to accept terms after login
+    </mat-checkbox>
+
+    @if (parentForm.get('requireTermsAcknowledgment').getRawValue()) {
+        <mat-form-field color="accent" class="mt-10">
+            <mat-label>Dialog Title</mat-label>
+            <input formControlName="termsAcknowledgmentTitle" fxFlex matInput 
/>
+        </mat-form-field>
+
+        <h5>Terms</h5>
+        <quill-editor
+            fxFlex="100"
+            #textEditor
+            formControlName="termsAcknowledgmentText"
+            [modules]="quillConfig"
+        ></quill-editor>
+    }
+</sp-split-section>
diff --git a/ui/src/app/login/components/login/login.model.ts 
b/ui/src/app/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.ts
similarity index 56%
copy from ui/src/app/login/components/login/login.model.ts
copy to 
ui/src/app/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.ts
index a56f76c941..b41f085afe 100644
--- a/ui/src/app/login/components/login/login.model.ts
+++ 
b/ui/src/app/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.ts
@@ -16,22 +16,25 @@
  *
  */
 
-import { LinkSettings } from '@streampipes/platform-services';
+import { Component, Input } from '@angular/core';
+import { FormGroup } from '@angular/forms';
 
-export interface OAuthProvider {
-    name: string;
-    registrationId: string;
-}
-
-export interface OAuthSettings {
-    enabled: boolean;
-    redirectUri: string;
-    supportedProviders: OAuthProvider[];
-}
+@Component({
+    selector: 'sp-user-acknowledgment',
+    templateUrl: './user-acknowledgment.component.html',
+    standalone: false,
+})
+export class UserAcknowledgmentComponent {
+    @Input()
+    parentForm: FormGroup;
 
-export interface LoginModel {
-    allowSelfRegistration: boolean;
-    allowPasswordRecovery: boolean;
-    linkSettings: LinkSettings;
-    oAuthSettings: OAuthSettings;
+    quillConfig: any = {
+        toolbar: [
+            ['bold', 'italic', 'underline', 'strike'],
+            [{ header: 1 }, { header: 2 }],
+            [{ size: ['small', false, 'large', 'huge'] }],
+            [{ header: [1, 2, 3, 4, 5, 6, false] }],
+            [{ color: [] }, { background: [] }],
+        ],
+    };
 }
diff --git 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
index d46527b426..9b057f8c88 100644
--- 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
+++ 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
@@ -16,185 +16,193 @@
   ~
   -->
 
-<div class="sp-dialog-container">
-    <div class="sp-dialog-content">
-        <div fxFlex="100" fxLayout="column" class="p-15">
-            <sp-warning-box *ngIf="isUserAccount && isExternalProvider">
-                Settings of externally-managed users cannot be changed.
-            </sp-warning-box>
-            <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
-                <div class="general-options-panel" fxLayout="column">
-                    <span class="general-options-header">Basics</span>
-                    <mat-error *ngIf="registrationError">{{
-                        registrationError
-                    }}</mat-error>
-                    <mat-form-field color="accent" *ngIf="!isUserAccount">
-                        <mat-label>Username</mat-label>
-                        <input
-                            formControlName="username"
-                            fxFlex
-                            matInput
-                            required
-                        />
-                    </mat-form-field>
-                    <mat-form-field color="accent" *ngIf="isUserAccount">
-                        <mat-label>Email</mat-label>
-                        <input
-                            formControlName="username"
-                            fxFlex
-                            type="email"
-                            matInput
-                            data-cy="new-user-email"
-                        />
-                        <mat-error>Must be a valid email address.</mat-error>
-                    </mat-form-field>
-                    <div class="email-changed" *ngIf="emailChanged">
-                        Changing the current user's email will require a
-                        re-login.
+@if (formAvailable) {
+    <div class="sp-dialog-container">
+        <div class="sp-dialog-content">
+            <div fxFlex="100" fxLayout="column" class="p-15">
+                <sp-warning-box *ngIf="isUserAccount && isExternalProvider">
+                    Settings of externally-managed users cannot be changed.
+                </sp-warning-box>
+                <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
+                    <div class="general-options-panel" fxLayout="column">
+                        <span class="general-options-header">Basics</span>
+                        <mat-error *ngIf="registrationError">{{
+                            registrationError
+                        }}</mat-error>
+                        <mat-form-field color="accent" *ngIf="!isUserAccount">
+                            <mat-label>Username</mat-label>
+                            <input
+                                formControlName="username"
+                                fxFlex
+                                matInput
+                                required
+                            />
+                        </mat-form-field>
+                        <mat-form-field color="accent" *ngIf="isUserAccount">
+                            <mat-label>Email</mat-label>
+                            <input
+                                formControlName="username"
+                                fxFlex
+                                type="email"
+                                matInput
+                                data-cy="new-user-email"
+                            />
+                            <mat-error
+                                >Must be a valid email address.</mat-error
+                            >
+                        </mat-form-field>
+                        <div class="email-changed" *ngIf="emailChanged">
+                            Changing the current user's email will require a
+                            re-login.
+                        </div>
+                        <mat-form-field color="accent" *ngIf="isUserAccount">
+                            <mat-label>Full Name</mat-label>
+                            <input
+                                formControlName="fullName"
+                                fxFlex
+                                matInput
+                                data-cy="new-user-full-name"
+                            />
+                        </mat-form-field>
                     </div>
-                    <mat-form-field color="accent" *ngIf="isUserAccount">
-                        <mat-label>Full Name</mat-label>
-                        <input
-                            formControlName="fullName"
-                            fxFlex
-                            matInput
-                            data-cy="new-user-full-name"
-                        />
-                    </mat-form-field>
-                </div>
-                <div
-                    fxLayout="column"
-                    class="general-options-panel"
-                    *ngIf="!editMode && isUserAccount"
-                >
-                    <span class="general-options-header">Password</span>
-                    <mat-checkbox
-                        formControlName="sendPasswordToUser"
-                        *ngIf="emailConfigured"
-                        >Auto-create password and send to user</mat-checkbox
-                    >
-                    <mat-form-field color="accent" *ngIf="!sendPasswordToUser">
-                        <mat-label>Initial password</mat-label>
-                        <input
-                            formControlName="password"
-                            fxFlex
-                            type="password"
-                            matInput
-                            required
-                            data-cy="new-user-password"
-                        />
-                    </mat-form-field>
-                    <mat-form-field color="accent" *ngIf="!sendPasswordToUser">
-                        <mat-label>Repeat password</mat-label>
-                        <input
-                            formControlName="repeatPassword"
-                            fxFlex
-                            type="password"
-                            matInput
-                            required
-                            data-cy="new-user-password-repeat"
-                        />
-                    </mat-form-field>
-                    <mat-error *ngIf="parentForm.hasError('notMatching')"
-                        >Passwords do not match.</mat-error
-                    >
-                </div>
-                <div
-                    fxLayout="column"
-                    class="general-options-panel"
-                    *ngIf="!isUserAccount"
-                >
-                    <span class="general-options-header">Authentication</span>
-                    <mat-form-field color="accent">
-                        <mat-label>Client Secret</mat-label>
-                        <input
-                            formControlName="clientSecret"
-                            fxFlex
-                            type="password"
-                            matInput
-                            required
-                        />
-                    </mat-form-field>
-                    <mat-error *ngIf="parentForm.controls.clientSecret.errors"
-                        >Minimum length 35 characters.</mat-error
-                    >
-                </div>
-                <div fxLayout="column" class="general-options-panel">
-                    <span class="general-options-header">Groups</span>
-                    <mat-checkbox
-                        *ngFor="let group of availableGroups"
-                        [disabled]="
-                            isExternalProvider && isExternallyManagedRoles
-                        "
-                        [value]="group.groupId"
-                        [checked]="user.groups.indexOf(group.groupId) > -1"
-                        (change)="changeGroupAssignment($event)"
+                    <div
+                        fxLayout="column"
+                        class="general-options-panel"
+                        *ngIf="!editMode && isUserAccount"
                     >
-                        {{ group.groupName }}
-                    </mat-checkbox>
-                </div>
-                <div fxLayout="column" class="general-options-panel">
-                    <span class="general-options-header">Roles</span>
-                    <mat-checkbox
-                        *ngFor="let role of availableRoles$ | async"
-                        [value]="role.elementId"
-                        [disabled]="
-                            isExternalProvider && isExternallyManagedRoles
-                        "
-                        [checked]="user.roles.indexOf(role.elementId) > -1"
-                        (change)="changeRoleAssignment($event)"
-                        [attr.data-cy]="'role-' + role.elementId"
+                        <span class="general-options-header">Password</span>
+                        <mat-checkbox
+                            formControlName="sendPasswordToUser"
+                            *ngIf="emailConfigured"
+                            >Auto-create password and send to 
user</mat-checkbox
+                        >
+                        <mat-form-field color="accent">
+                            <mat-label>Initial password</mat-label>
+                            <input
+                                formControlName="password"
+                                fxFlex
+                                type="password"
+                                matInput
+                                required
+                                data-cy="new-user-password"
+                            />
+                        </mat-form-field>
+                        <mat-form-field color="accent">
+                            <mat-label>Repeat password</mat-label>
+                            <input
+                                formControlName="repeatPassword"
+                                fxFlex
+                                type="password"
+                                matInput
+                                required
+                                data-cy="new-user-password-repeat"
+                            />
+                        </mat-form-field>
+                        <mat-error *ngIf="parentForm.hasError('notMatching')"
+                            >Passwords do not match.</mat-error
+                        >
+                    </div>
+                    <div
+                        fxLayout="column"
+                        class="general-options-panel"
+                        *ngIf="!isUserAccount"
                     >
-                        {{ role.label }}
-                    </mat-checkbox>
-                </div>
-                @if (!isExternalProvider) {
+                        <span class="general-options-header"
+                            >Authentication</span
+                        >
+                        <mat-form-field color="accent">
+                            <mat-label>Client Secret</mat-label>
+                            <input
+                                formControlName="clientSecret"
+                                fxFlex
+                                type="password"
+                                matInput
+                                required
+                            />
+                        </mat-form-field>
+                        <mat-error
+                            *ngIf="parentForm.controls.clientSecret.errors"
+                            >Minimum length 35 characters.</mat-error
+                        >
+                    </div>
                     <div fxLayout="column" class="general-options-panel">
-                        <span class="general-options-header">Account</span>
+                        <span class="general-options-header">Groups</span>
                         <mat-checkbox
-                            formControlName="accountEnabled"
-                            data-cy="new-user-enabled"
+                            *ngFor="let group of availableGroups"
+                            [disabled]="
+                                isExternalProvider && isExternallyManagedRoles
+                            "
+                            [value]="group.groupId"
+                            [checked]="user.groups.indexOf(group.groupId) > -1"
+                            (change)="changeGroupAssignment($event)"
                         >
-                            Enabled
+                            {{ group.groupName }}
                         </mat-checkbox>
+                    </div>
+                    <div fxLayout="column" class="general-options-panel">
+                        <span class="general-options-header">Roles</span>
                         <mat-checkbox
-                            formControlName="accountLocked"
-                            data-cy="new-user-locked"
+                            *ngFor="let role of availableRoles$ | async"
+                            [value]="role.elementId"
+                            [disabled]="
+                                isExternalProvider && isExternallyManagedRoles
+                            "
+                            [checked]="user.roles.indexOf(role.elementId) > -1"
+                            (change)="changeRoleAssignment($event)"
+                            [attr.data-cy]="'role-' + role.elementId"
                         >
-                            Locked
+                            {{ role.label }}
                         </mat-checkbox>
                     </div>
-                }
-            </form>
+                    @if (!isExternalProvider) {
+                        <div fxLayout="column" class="general-options-panel">
+                            <span class="general-options-header">Account</span>
+                            <mat-checkbox
+                                formControlName="accountEnabled"
+                                data-cy="new-user-enabled"
+                            >
+                                Enabled
+                            </mat-checkbox>
+                            <mat-checkbox
+                                formControlName="accountLocked"
+                                data-cy="new-user-locked"
+                            >
+                                Locked
+                            </mat-checkbox>
+                        </div>
+                    }
+                </form>
+            </div>
         </div>
-    </div>
-    <mat-divider></mat-divider>
-    <div class="sp-dialog-actions">
-        <div fxLayout="column">
-            <div fxLayout="row">
-                <button
-                    mat-button
-                    mat-raised-button
-                    color="accent"
-                    (click)="save()"
-                    style="margin-right: 10px"
-                    [disabled]="
-                        parentForm.invalid ||
-                        (isExternalProvider && isExternallyManagedRoles)
-                    "
-                    data-cy="sp-element-edit-user-save"
-                >
-                    <i class="material-icons">save</i><span>&nbsp;Save</span>
-                </button>
-                <button
-                    mat-button
-                    mat-raised-button
-                    class="mat-basic"
-                    (click)="close(false)"
-                >
-                    Cancel
-                </button>
+        <mat-divider></mat-divider>
+        <div class="sp-dialog-actions">
+            <div fxLayout="column">
+                <div fxLayout="row">
+                    <button
+                        mat-button
+                        mat-raised-button
+                        color="accent"
+                        (click)="save()"
+                        style="margin-right: 10px"
+                        [disabled]="
+                            parentForm.invalid ||
+                            (isExternalProvider && isExternallyManagedRoles)
+                        "
+                        data-cy="sp-element-edit-user-save"
+                    >
+                        <i class="material-icons">save</i
+                        ><span>&nbsp;Save</span>
+                    </button>
+                    <button
+                        mat-button
+                        mat-raised-button
+                        class="mat-basic"
+                        (click)="close(false)"
+                    >
+                        Cancel
+                    </button>
+                </div>
             </div>
         </div>
     </div>
-</div>
+}
diff --git 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
index a6ff998ed0..b00a75a65c 100644
--- 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
+++ 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
@@ -29,6 +29,7 @@ import {
 } from '@streampipes/platform-services';
 import {
     AbstractControl,
+    FormControl,
     UntypedFormBuilder,
     UntypedFormControl,
     UntypedFormGroup,
@@ -72,6 +73,7 @@ export class EditUserDialogComponent implements OnInit {
     sendPasswordToUser = false;
     emailChanged = false;
     emailConfigured = false;
+    formAvailable = false;
 
     constructor(
         private dialogRef: DialogRef<EditUserDialogComponent>,
@@ -89,8 +91,6 @@ export class EditUserDialogComponent implements OnInit {
         this.initRoleFilter();
         this.loadInitialData();
         this.cloneUser();
-        this.initForm();
-        this.handleFormChanges();
     }
 
     save() {
@@ -103,6 +103,48 @@ export class EditUserDialogComponent implements OnInit {
                 : 'Unknown error';
         };
 
+        if (!this.isUserAccount || !this.isExternalProvider) {
+            this.clonedUser.username = this.parentForm.get('username').value;
+        }
+
+        if (!this.isExternalProvider) {
+            this.clonedUser.accountLocked =
+                this.parentForm.get('accountLocked').value;
+            this.clonedUser.accountEnabled =
+                this.parentForm.get('accountEnabled').value;
+        }
+
+        if (this.clonedUser instanceof UserAccount) {
+            this.emailChanged =
+                this.clonedUser.username !== this.user.username &&
+                this.user.username ===
+                    this.currentUserService.getCurrentUser().username &&
+                this.editMode;
+
+            if (!this.isExternalProvider) {
+                this.clonedUser.fullName =
+                    this.parentForm.get('fullName').value;
+            }
+            if (!this.editMode) {
+                if (
+                    this.emailConfigured &&
+                    this.parentForm.get('sendPasswordToUser').value
+                ) {
+                    this.sendPasswordToUser =
+                        this.parentForm.get('sendPasswordToUser').value;
+                } else {
+                    this.clonedUser.password =
+                        this.parentForm.get('password').value;
+                }
+            }
+        } else {
+            const clientSecret = this.parentForm.get('clientSecret').value;
+            if (this.user.clientSecret !== clientSecret) {
+                this.clonedUser.clientSecret = clientSecret;
+                this.clonedUser.secretEncrypted = false;
+            }
+        }
+
         if (this.editMode) {
             const update$ = this.isUserAccount
                 ? this.userService.updateUser(this.clonedUser as UserAccount)
@@ -150,6 +192,9 @@ export class EditUserDialogComponent implements OnInit {
     private loadInitialData(): void {
         this.mailConfigService.getMailConfig().subscribe(config => {
             this.emailConfigured = config.emailConfigured;
+            this.initForm();
+            this.handleFormChanges();
+            this.formAvailable = true;
         });
 
         this.userGroupService.getAllUserGroups().subscribe(groups => {
@@ -193,12 +238,6 @@ export class EditUserDialogComponent implements OnInit {
             ];
         }
 
-        if (!this.editMode && this.clonedUser instanceof UserAccount) {
-            form['password'] = [this.clonedUser.password, Validators.required];
-            form['repeatPassword'] = [''];
-            form['sendPasswordToUser'] = [this.sendPasswordToUser];
-        }
-
         this.parentForm = this.fb.group(form, {
             validators:
                 this.editMode || !this.isUserAccount
@@ -206,6 +245,20 @@ export class EditUserDialogComponent implements OnInit {
                     : this.checkPasswords,
         });
 
+        if (!this.editMode && this.clonedUser instanceof UserAccount) {
+            if (this.emailConfigured) {
+                this.parentForm.addControl(
+                    'sendPasswordToUser',
+                    new FormControl(this.sendPasswordToUser),
+                );
+            }
+            this.parentForm.addControl(
+                'password',
+                new FormControl(null, [Validators.required]),
+            );
+            this.parentForm.addControl('repeatPassword', new 
FormControl(null));
+        }
+
         if (this.isExternalProvider) {
             this.parentForm.get('username')?.disable();
             this.parentForm.get('fullName')?.disable();
@@ -214,67 +267,57 @@ export class EditUserDialogComponent implements OnInit {
 
     private handleFormChanges(): void {
         this.parentForm.valueChanges.subscribe(v => {
-            const raw = this.parentForm.getRawValue();
-            if (!this.isUserAccount || !this.isExternalProvider) {
-                this.clonedUser.username = v.username;
-            }
-
-            if (!this.isExternalProvider) {
-                this.clonedUser.accountLocked = raw.accountLocked;
-                this.clonedUser.accountEnabled = raw.accountEnabled;
-            }
-
-            if (this.clonedUser instanceof UserAccount) {
-                this.emailChanged =
-                    this.clonedUser.username !== this.user.username &&
-                    this.user.username ===
-                        this.currentUserService.getCurrentUser().username &&
-                    this.editMode;
-
-                if (!this.isExternalProvider) {
-                    this.clonedUser.fullName = v.fullName;
-                }
-
-                if (!this.editMode) {
+            if (this.clonedUser instanceof UserAccount && !this.editMode) {
+                if (this.sendPasswordToUser !== v.sendPasswordToUser) {
                     this.sendPasswordToUser = v.sendPasswordToUser;
-
                     if (this.sendPasswordToUser) {
                         this.removePasswordControls();
                     } else {
                         this.addPasswordControlsIfMissing();
-                        this.clonedUser.password = v.password;
                     }
                 }
-            } else {
-                if (this.user.clientSecret !== v.clientSecret) {
-                    this.clonedUser.clientSecret = v.clientSecret;
-                    this.clonedUser.secretEncrypted = false;
-                }
             }
         });
     }
 
     private removePasswordControls(): void {
-        this.parentForm.removeControl('password');
-        this.parentForm.removeControl('repeatPassword');
-        this.parentForm.clearValidators();
+        const pw = this.parentForm.get('password');
+        const rp = this.parentForm.get('repeatPassword');
+        pw.setValue(null);
+        rp.setValue(null);
+
+        pw?.clearValidators();
+        rp?.clearValidators();
+
+        pw?.disable({ emitEvent: false });
+        rp?.disable({ emitEvent: false });
+
+        pw?.updateValueAndValidity({ emitEvent: false });
+        rp?.updateValueAndValidity({ emitEvent: false });
+
+        this.parentForm.setValidators(null);
+        this.parentForm.updateValueAndValidity({ emitEvent: false });
+
         if (this.clonedUser instanceof UserAccount) {
             this.clonedUser.password = undefined;
         }
     }
 
     private addPasswordControlsIfMissing(): void {
-        if (!this.parentForm.get('password')) {
-            this.parentForm.addControl(
-                'password',
-                new UntypedFormControl('', Validators.required),
-            );
-            this.parentForm.addControl(
-                'repeatPassword',
-                new UntypedFormControl(),
-            );
-            this.parentForm.setValidators(this.checkPasswords);
-        }
+        const pw = this.parentForm.get('password');
+        const rp = this.parentForm.get('repeatPassword');
+
+        pw?.enable({ emitEvent: false });
+        rp?.enable({ emitEvent: false });
+
+        pw?.setValidators([Validators.required]);
+        rp?.setValidators([Validators.required]);
+
+        this.parentForm.addValidators(this.checkPasswords);
+
+        pw?.updateValueAndValidity({ emitEvent: false });
+        rp?.updateValueAndValidity({ emitEvent: false });
+        this.parentForm.updateValueAndValidity({ emitEvent: false });
     }
 
     private getUsernameValidators(): ValidatorFn[] {
diff --git a/ui/src/app/core/components/iconbar/iconbar.component.ts 
b/ui/src/app/core/components/iconbar/iconbar.component.ts
index b2f573bd40..c02d09ea20 100644
--- a/ui/src/app/core/components/iconbar/iconbar.component.ts
+++ b/ui/src/app/core/components/iconbar/iconbar.component.ts
@@ -18,10 +18,6 @@
 
 import { Component, OnInit } from '@angular/core';
 import { BaseNavigationComponent } from '../base-navigation.component';
-import { Router } from '@angular/router';
-import { AuthService } from '../../../services/auth.service';
-import { AppConstants } from '../../../services/app.constants';
-import { CurrentUserService } from '@streampipes/shared-ui';
 
 @Component({
     selector: 'sp-iconbar',
@@ -33,15 +29,6 @@ export class IconbarComponent
     extends BaseNavigationComponent
     implements OnInit
 {
-    constructor(
-        router: Router,
-        authService: AuthService,
-        currentUserService: CurrentUserService,
-        appConstants: AppConstants,
-    ) {
-        super(authService, currentUserService, router, appConstants);
-    }
-
     ngOnInit(): void {
         super.onInit();
     }
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.ts 
b/ui/src/app/core/components/toolbar/toolbar.component.ts
index bd95e9cafd..249fc23871 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.ts
+++ b/ui/src/app/core/components/toolbar/toolbar.component.ts
@@ -16,20 +16,16 @@
  *
  */
 
-import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { Component, inject, OnDestroy, OnInit, ViewChild } from 
'@angular/core';
 import { BaseNavigationComponent } from '../base-navigation.component';
-import { Router } from '@angular/router';
 import { RestApi } from '../../../services/rest-api.service';
 import { MatMenuTrigger } from '@angular/material/menu';
 import { UntypedFormControl } from '@angular/forms';
 import { OverlayContainer } from '@angular/cdk/overlay';
 import { ProfileService } from '../../../profile/profile.service';
-import { AuthService } from '../../../services/auth.service';
-import { AppConstants } from '../../../services/app.constants';
 import { Subscription, timer } from 'rxjs';
 import { exhaustMap } from 'rxjs/operators';
 import { NotificationCountService } from 
'../../../services/notification-count-service';
-import { CurrentUserService } from '@streampipes/shared-ui';
 import { LoginService } from '../../../login/services/login.service';
 
 @Component({
@@ -56,19 +52,11 @@ export class ToolbarComponent
     documentationLinkActive = false;
     documentationLink = '';
 
-    constructor(
-        router: Router,
-        authService: AuthService,
-        private loginService: LoginService,
-        private profileService: ProfileService,
-        private restApi: RestApi,
-        private overlay: OverlayContainer,
-        currentUserService: CurrentUserService,
-        appConstants: AppConstants,
-        public notificationCountService: NotificationCountService,
-    ) {
-        super(authService, currentUserService, router, appConstants);
-    }
+    private loginService = inject(LoginService);
+    private profileService = inject(ProfileService);
+    private restApi = inject(RestApi);
+    private overlay = inject(OverlayContainer);
+    public notificationCountService = inject(NotificationCountService);
 
     ngOnInit(): void {
         this.unreadNotificationsSubscription = timer(0, 10000)
diff --git 
a/ui/src/app/login/components/activate-account/activate-account.component.ts 
b/ui/src/app/login/components/activate-account/activate-account.component.ts
index dc38e3eb33..a2a412085e 100644
--- a/ui/src/app/login/components/activate-account/activate-account.component.ts
+++ b/ui/src/app/login/components/activate-account/activate-account.component.ts
@@ -16,11 +16,10 @@
  *
  */
 
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
 import { AccountActivationService } from 
'../../services/account-activation.service';
 import { ActivatedRoute, Router } from '@angular/router';
 import { BaseLoginPageDirective } from '../base-login-page.directive';
-import { LoginService } from '../../services/login.service';
 
 @Component({
     selector: 'sp-activate-account',
@@ -33,14 +32,9 @@ export class ActivateAccountComponent extends 
BaseLoginPageDirective {
     activationSuccess: boolean;
     activationPerformed = false;
 
-    constructor(
-        private accountActivationService: AccountActivationService,
-        private route: ActivatedRoute,
-        private router: Router,
-        protected loginService: LoginService,
-    ) {
-        super(loginService);
-    }
+    private accountActivationService = inject(AccountActivationService);
+    private route = inject(ActivatedRoute);
+    private router = inject(Router);
 
     navigateToLoginPage() {
         this.router.navigate(['/login']);
diff --git a/ui/src/app/login/components/base-login-page.directive.ts 
b/ui/src/app/login/components/base-login-page.directive.ts
index c572a18e5e..ef15cb926f 100644
--- a/ui/src/app/login/components/base-login-page.directive.ts
+++ b/ui/src/app/login/components/base-login-page.directive.ts
@@ -16,16 +16,16 @@
  *
  */
 
-import { Directive, OnInit } from '@angular/core';
+import { Directive, inject, OnInit } from '@angular/core';
 import { LoginService } from '../services/login.service';
 import { LoginModel } from './login/login.model';
 
 @Directive()
 export abstract class BaseLoginPageDirective implements OnInit {
-    protected loginSettings: LoginModel;
+    public loginSettings: LoginModel;
     protected configReady = false;
 
-    protected constructor(protected loginService: LoginService) {}
+    protected loginService = inject(LoginService);
 
     ngOnInit(): void {
         this.loginService.fetchLoginSettings().subscribe(result => {
diff --git a/ui/src/app/login/components/login/login.component.html 
b/ui/src/app/login/components/login/login.component.html
index a9a1976609..7d78ca2d9a 100644
--- a/ui/src/app/login/components/login/login.component.html
+++ b/ui/src/app/login/components/login/login.component.html
@@ -16,111 +16,115 @@
   ~
   -->
 
-<sp-auth-box [linkSettings]="loginSettings.linkSettings" *ngIf="configReady">
-    <div fxFlex="100" fxLayout="column" fxLayoutAlign="center start">
-        <h1>Login</h1>
-    </div>
-    <div fxFlex="100" fxLayout="column" class="mt-10">
-        <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
-            <div fxFlex="100" fxLayout="column">
-                <mat-form-field fxFlex color="accent">
-                    <mat-label>Email</mat-label>
-                    <input
-                        formControlName="username"
-                        matInput
-                        name="username"
-                        class="sp"
-                        required
-                        data-cy="login-email"
-                    />
-                </mat-form-field>
-                <mat-form-field fxFlex color="accent">
-                    <mat-label>Password</mat-label>
-                    <input
-                        formControlName="password"
-                        matInput
-                        name="password"
-                        type="password"
-                        class="sp"
-                        required
-                        data-cy="login-password"
-                    />
-                </mat-form-field>
-            </div>
-            <div class="form-actions">
-                <button
-                    mat-button
-                    mat-raised-button
-                    color="accent"
-                    data-cy="login-button"
-                    (click)="doLogin()"
-                    [disabled]="!parentForm.valid || loading"
-                >
-                    <span *ngIf="loading">Logging in...</span>
-                    <span *ngIf="!loading">Login</span>
-                </button>
-                <mat-spinner
-                    [mode]="'indeterminate'"
-                    color="accent"
-                    [diameter]="20"
-                    *ngIf="loading"
-                    style="margin-top: 10px"
-                ></mat-spinner>
-                <div class="md-warn" *ngIf="authenticationFailed">
-                    <h5 class="login-error">
-                        User not found or incorrect password provided.<br 
/>Please
-                        try again.
-                    </h5>
+@if (configReady) {
+    <sp-auth-box [linkSettings]="loginSettings.linkSettings">
+        <div fxFlex="100" fxLayout="column" fxLayoutAlign="center start">
+            <h1>Login</h1>
+        </div>
+        <div fxFlex="100" fxLayout="column" class="mt-10">
+            <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
+                <div fxFlex="100" fxLayout="column">
+                    <mat-form-field fxFlex color="accent">
+                        <mat-label>Email</mat-label>
+                        <input
+                            formControlName="username"
+                            matInput
+                            name="username"
+                            class="sp"
+                            required
+                            data-cy="login-email"
+                        />
+                    </mat-form-field>
+                    <mat-form-field fxFlex color="accent">
+                        <mat-label>Password</mat-label>
+                        <input
+                            formControlName="password"
+                            matInput
+                            name="password"
+                            type="password"
+                            class="sp"
+                            required
+                            data-cy="login-password"
+                        />
+                    </mat-form-field>
                 </div>
-                <div fxLayout="row" class="mt-10">
-                    <div *ngIf="loginSettings.allowPasswordRecovery">
-                        <a [routerLink]="['/restore-password']"
-                            >Forgot password?</a
-                        >
-                    </div>
-                    <span
-                        style="margin-left: 5px; margin-right: 5px"
-                        *ngIf="
-                            loginSettings.allowSelfRegistration &&
-                            loginSettings.allowPasswordRecovery
-                        "
+                <div class="form-actions">
+                    <button
+                        mat-button
+                        mat-raised-button
+                        color="accent"
+                        data-cy="login-button"
+                        (click)="doLogin()"
+                        [disabled]="!parentForm.valid || loading"
                     >
-                        |
-                    </span>
-                    <div *ngIf="loginSettings.allowSelfRegistration">
-                        <a [routerLink]="['/register']">Create new account</a>
+                        <span *ngIf="loading">Logging in...</span>
+                        <span *ngIf="!loading">Login</span>
+                    </button>
+                    <mat-spinner
+                        [mode]="'indeterminate'"
+                        color="accent"
+                        [diameter]="20"
+                        *ngIf="loading"
+                        style="margin-top: 10px"
+                    ></mat-spinner>
+                    <div class="md-warn" *ngIf="authenticationFailed">
+                        <h5 class="login-error">
+                            User not found or incorrect password provided.<br 
/>Please
+                            try again.
+                        </h5>
                     </div>
-                </div>
-                <div
-                    fxLayout="column"
-                    class="mt-10"
-                    *ngIf="loginSettings.oAuthSettings?.enabled"
-                >
-                    <div class="separator">
-                        <span>or</span>
+                    <div fxLayout="row" class="mt-10">
+                        <div *ngIf="loginSettings.allowPasswordRecovery">
+                            <a [routerLink]="['/restore-password']"
+                                >Forgot password?</a
+                            >
+                        </div>
+                        <span
+                            style="margin-left: 5px; margin-right: 5px"
+                            *ngIf="
+                                loginSettings.allowSelfRegistration &&
+                                loginSettings.allowPasswordRecovery
+                            "
+                        >
+                            |
+                        </span>
+                        <div *ngIf="loginSettings.allowSelfRegistration">
+                            <a [routerLink]="['/register']"
+                                >Create new account</a
+                            >
+                        </div>
                     </div>
                     <div
                         fxLayout="column"
-                        *ngFor="
-                            let provider of loginSettings.oAuthSettings
-                                .supportedProviders
-                        "
                         class="mt-10"
+                        *ngIf="loginSettings.oAuthSettings?.enabled"
                     >
-                        <button
-                            mat-button
-                            mat-raised-button
-                            color="accent"
-                            data-cy="login-button"
-                            (click)="doOAuthLogin(provider.registrationId)"
+                        <div class="separator">
+                            <span>or</span>
+                        </div>
+                        <div
+                            fxLayout="column"
+                            *ngFor="
+                                let provider of loginSettings.oAuthSettings
+                                    .supportedProviders
+                            "
+                            class="mt-10"
                         >
-                            <span *ngIf="!loading"
-                                >Login with {{ provider.name }}</span
+                            <button
+                                mat-button
+                                mat-raised-button
+                                color="accent"
+                                data-cy="login-button"
+                                (click)="doOAuthLogin(provider.registrationId)"
                             >
-                        </button>
+                                <span *ngIf="!loading"
+                                    >Login with {{ provider.name }}</span
+                                >
+                            </button>
+                        </div>
                     </div>
                 </div>
-            </div>
-        </form>
-    </div>
-</sp-auth-box>
+            </form>
+        </div>
+    </sp-auth-box>
+}
diff --git a/ui/src/app/login/components/login/login.component.ts 
b/ui/src/app/login/components/login/login.component.ts
index ee8d6b3202..50ec16415a 100644
--- a/ui/src/app/login/components/login/login.component.ts
+++ b/ui/src/app/login/components/login/login.component.ts
@@ -16,8 +16,7 @@
  *
  */
 
-import { Component } from '@angular/core';
-import { LoginService } from '../../services/login.service';
+import { Component, inject } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { AuthService } from '../../../services/auth.service';
 import {
@@ -36,24 +35,16 @@ import { BaseLoginPageDirective } from 
'../base-login-page.directive';
 })
 export class LoginComponent extends BaseLoginPageDirective {
     parentForm: UntypedFormGroup;
-    loading: boolean;
-    authenticationFailed: boolean;
-    credentials: any;
+    loading = false;
+    authenticationFailed = false;
+    credentials: any = {};
 
     returnUrl: string;
 
-    constructor(
-        loginService: LoginService,
-        private router: Router,
-        private route: ActivatedRoute,
-        private authService: AuthService,
-        private fb: UntypedFormBuilder,
-    ) {
-        super(loginService);
-        this.loading = false;
-        this.authenticationFailed = false;
-        this.credentials = {};
-    }
+    private router = inject(Router);
+    private route = inject(ActivatedRoute);
+    private authService = inject(AuthService);
+    private fb = inject(UntypedFormBuilder);
 
     doLogin() {
         this.authenticationFailed = false;
@@ -63,7 +54,9 @@ export class LoginComponent extends BaseLoginPageDirective {
                 // success
                 this.authService.login(response);
                 this.loading = false;
-                this.router.navigateByUrl(this.returnUrl);
+                this.router.navigate(['terms'], {
+                    queryParams: { returnUrl: this.returnUrl },
+                });
             },
             response => {
                 // error
@@ -78,7 +71,9 @@ export class LoginComponent extends BaseLoginPageDirective {
         if (token) {
             this.authService.oauthLogin(token);
             this.loading = false;
-            this.router.navigate(['']);
+            this.router.navigate(['terms'], {
+                queryParams: { returnUrl: this.returnUrl },
+            });
         }
         this.parentForm = this.fb.group({});
         this.parentForm.addControl(
diff --git a/ui/src/app/login/components/login/login.model.ts 
b/ui/src/app/login/components/login/login.model.ts
index a56f76c941..95f5793d70 100644
--- a/ui/src/app/login/components/login/login.model.ts
+++ b/ui/src/app/login/components/login/login.model.ts
@@ -34,4 +34,7 @@ export interface LoginModel {
     allowPasswordRecovery: boolean;
     linkSettings: LinkSettings;
     oAuthSettings: OAuthSettings;
+    termsAcknowledgmentRequired: boolean;
+    termsAcknowledgmentTitle?: string;
+    termsAcknowledgmentText?: string;
 }
diff --git a/ui/src/app/login/components/register/register.component.ts 
b/ui/src/app/login/components/register/register.component.ts
index b622f29bed..e620434e66 100644
--- a/ui/src/app/login/components/register/register.component.ts
+++ b/ui/src/app/login/components/register/register.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
 import {
     UntypedFormBuilder,
     UntypedFormControl,
@@ -24,7 +24,6 @@ import {
     Validators,
 } from '@angular/forms';
 import { RegistrationModel } from './registration.model';
-import { LoginService } from '../../services/login.service';
 import { checkPasswords } from '../../utils/check-password';
 import { BaseLoginPageDirective } from '../base-login-page.directive';
 
@@ -43,12 +42,7 @@ export class RegisterComponent extends 
BaseLoginPageDirective {
     registrationSuccess = false;
     registrationError: string;
 
-    constructor(
-        private fb: UntypedFormBuilder,
-        loginService: LoginService,
-    ) {
-        super(loginService);
-    }
+    private fb = inject(UntypedFormBuilder);
 
     registerUser() {
         this.registrationError = undefined;
diff --git 
a/ui/src/app/login/components/restore-password/restore-password.component.ts 
b/ui/src/app/login/components/restore-password/restore-password.component.ts
index c7f2569640..7423fab75b 100644
--- a/ui/src/app/login/components/restore-password/restore-password.component.ts
+++ b/ui/src/app/login/components/restore-password/restore-password.component.ts
@@ -16,14 +16,13 @@
  *
  */
 
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
 import {
     UntypedFormBuilder,
     UntypedFormControl,
     UntypedFormGroup,
     Validators,
 } from '@angular/forms';
-import { LoginService } from '../../services/login.service';
 import { BaseLoginPageDirective } from '../base-login-page.directive';
 
 @Component({
@@ -39,12 +38,7 @@ export class RestorePasswordComponent extends 
BaseLoginPageDirective {
 
     username: string;
 
-    constructor(
-        private fb: UntypedFormBuilder,
-        protected loginService: LoginService,
-    ) {
-        super(loginService);
-    }
+    private fb = inject(UntypedFormBuilder);
 
     sendRestorePasswordLink() {
         this.restoreCompleted = false;
diff --git 
a/ui/src/app/login/components/set-new-password/set-new-password.component.ts 
b/ui/src/app/login/components/set-new-password/set-new-password.component.ts
index a986f818c4..59cdac519e 100644
--- a/ui/src/app/login/components/set-new-password/set-new-password.component.ts
+++ b/ui/src/app/login/components/set-new-password/set-new-password.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
 import { RestorePasswordService } from 
'../../services/restore-password.service';
 import {
     UntypedFormBuilder,
@@ -28,7 +28,6 @@ import { checkPasswords } from '../../utils/check-password';
 import { RegistrationModel } from '../register/registration.model';
 import { ActivatedRoute, Router } from '@angular/router';
 import { BaseLoginPageDirective } from '../base-login-page.directive';
-import { LoginService } from '../../services/login.service';
 
 @Component({
     selector: 'sp-set-new-password',
@@ -45,15 +44,10 @@ export class SetNewPasswordComponent extends 
BaseLoginPageDirective {
     resetInProgress = false;
     resetSuccess = false;
 
-    constructor(
-        private fb: UntypedFormBuilder,
-        private restorePasswordService: RestorePasswordService,
-        private route: ActivatedRoute,
-        private router: Router,
-        protected loginService: LoginService,
-    ) {
-        super(loginService);
-    }
+    private fb = inject(UntypedFormBuilder);
+    private restorePasswordService = inject(RestorePasswordService);
+    private route = inject(ActivatedRoute);
+    private router = inject(Router);
 
     onSettingsAvailable(): void {
         this.route.queryParams.subscribe(params => {
diff --git a/ui/src/app/login/components/terms/terms.component.html 
b/ui/src/app/login/components/terms/terms.component.html
new file mode 100644
index 0000000000..3d96dcad89
--- /dev/null
+++ b/ui/src/app/login/components/terms/terms.component.html
@@ -0,0 +1,50 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+@if (configReady && showAcknowledgment) {
+    <sp-auth-box [linkSettings]="loginSettings.linkSettings">
+        <div fxLayout="column" class="page">
+            <div fxLayout="column" fxLayoutAlign="center start" fxFlex="none">
+                <h1>{{ loginSettings.termsAcknowledgmentTitle }}</h1>
+            </div>
+
+            <div
+                fxFlex="none"
+                class="terms-box"
+                [innerHTML]="sanitizedText"
+            ></div>
+            <mat-divider></mat-divider>
+            <div fxLayout="row" fxLayoutGap="10px" fxFlex="none" class="mt-10">
+                <button
+                    mat-raised-button
+                    color="accent"
+                    (click)="onTermsAcknowledged()"
+                >
+                    Accept
+                </button>
+                <button
+                    mat-raised-button
+                    class="mat-basic"
+                    (click)="onTermsRejected()"
+                >
+                    Reject
+                </button>
+            </div>
+        </div>
+    </sp-auth-box>
+}
diff --git a/ui/src/app/login/components/login/login.model.ts 
b/ui/src/app/login/components/terms/terms.component.scss
similarity index 63%
copy from ui/src/app/login/components/login/login.model.ts
copy to ui/src/app/login/components/terms/terms.component.scss
index a56f76c941..5fdd7ac769 100644
--- a/ui/src/app/login/components/login/login.model.ts
+++ b/ui/src/app/login/components/terms/terms.component.scss
@@ -16,22 +16,27 @@
  *
  */
 
-import { LinkSettings } from '@streampipes/platform-services';
-
-export interface OAuthProvider {
-    name: string;
-    registrationId: string;
+.page {
+    min-width: 0;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
 }
 
-export interface OAuthSettings {
-    enabled: boolean;
-    redirectUri: string;
-    supportedProviders: OAuthProvider[];
-}
+.terms-box {
+    width: 100%;
+    max-width: 100%;
+    max-height: 400px;
+
+    overflow-y: auto;
+    overflow-x: hidden;
+
+    box-sizing: border-box;
+    padding-right: 4px;
+
+    white-space: normal; /* normal wrapping */
+    overflow-wrap: break-word; /* break only very long words */
+    word-break: normal; /* don’t break between characters */
 
-export interface LoginModel {
-    allowSelfRegistration: boolean;
-    allowPasswordRecovery: boolean;
-    linkSettings: LinkSettings;
-    oAuthSettings: OAuthSettings;
+    min-width: 0;
 }
diff --git a/ui/src/app/login/components/terms/terms.component.ts 
b/ui/src/app/login/components/terms/terms.component.ts
new file mode 100644
index 0000000000..d9c9a9bd12
--- /dev/null
+++ b/ui/src/app/login/components/terms/terms.component.ts
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Component, inject } from '@angular/core';
+import { BaseLoginPageDirective } from '../base-login-page.directive';
+import { ActivatedRoute, Router } from '@angular/router';
+import { CurrentUserService } from '@streampipes/shared-ui';
+import { ProfileService } from '../../../profile/profile.service';
+import { AuthService } from '../../../services/auth.service';
+import { UserAccount } from '@streampipes/platform-services';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+
+@Component({
+    selector: 'sp-terms',
+    templateUrl: './terms.component.html',
+    styleUrls: ['./terms.component.scss'],
+    standalone: false,
+})
+export class TermsComponent extends BaseLoginPageDirective {
+    returnUrl = '';
+    showAcknowledgment = false;
+    profile: UserAccount;
+    sanitizedText: SafeHtml | undefined;
+
+    private router = inject(Router);
+    private route = inject(ActivatedRoute);
+    private profileService = inject(ProfileService);
+    private authService = inject(AuthService);
+    private currentUserService = inject(CurrentUserService);
+    private sanitizer = inject(DomSanitizer);
+
+    onSettingsAvailable(): void {
+        this.returnUrl = this.route.snapshot.queryParams.returnUrl || '';
+        if (!this.authService.authenticated()) {
+            this.router.navigate(['login']);
+        } else {
+            if (this.loginSettings.termsAcknowledgmentRequired) {
+                this.profileService
+                    .getUserProfile(
+                        this.currentUserService.getCurrentUser().username,
+                    )
+                    .subscribe(profile => {
+                        if (!profile.hasAcknowledged) {
+                            const normalizedText = this.normalizeNbsp(
+                                this.loginSettings.termsAcknowledgmentText,
+                            );
+                            this.sanitizedText =
+                                this.sanitizer.bypassSecurityTrustHtml(
+                                    normalizedText,
+                                );
+                            this.profile = profile;
+                            this.showAcknowledgment = true;
+                        } else {
+                            this.proceedWithLogin();
+                        }
+                    });
+            } else {
+                this.proceedWithLogin();
+            }
+        }
+    }
+
+    onTermsAcknowledged(): void {
+        const userInfo = this.currentUserService.getCurrentUser();
+        userInfo.hasAcknowledged = true;
+        this.currentUserService.user$.next(userInfo);
+        this.profileService
+            .updateUserProfile({
+                ...this.profile,
+                hasAcknowledged: true,
+            })
+            .subscribe(() => this.proceedWithLogin());
+    }
+
+    onTermsRejected(): void {
+        this.authService.logout();
+        this.router.navigate(['login']);
+    }
+
+    proceedWithLogin(): void {
+        this.router.navigateByUrl(this.returnUrl);
+    }
+
+    private normalizeNbsp(html: string): string {
+        return html.replace(/&nbsp;|\u00A0/g, ' ');
+    }
+}
diff --git a/ui/src/app/login/login.module.ts b/ui/src/app/login/login.module.ts
index 657234688b..194c0f1151 100644
--- a/ui/src/app/login/login.module.ts
+++ b/ui/src/app/login/login.module.ts
@@ -40,6 +40,7 @@ import { RegisterComponent } from 
'./components/register/register.component';
 import { SetNewPasswordComponent } from 
'./components/set-new-password/set-new-password.component';
 import { ActivateAccountComponent } from 
'./components/activate-account/activate-account.component';
 import { PlatformServicesModule } from '@streampipes/platform-services';
+import { TermsComponent } from './components/terms/terms.component';
 
 @NgModule({
     imports: [
@@ -69,6 +70,7 @@ import { PlatformServicesModule } from 
'@streampipes/platform-services';
         SetNewPasswordComponent,
         SetupComponent,
         StartupComponent,
+        TermsComponent,
     ],
     providers: [],
 })
diff --git a/ui/src/app/login/services/login.service.ts 
b/ui/src/app/login/services/login.service.ts
index 7c2e12ba10..f0ab3144ea 100644
--- a/ui/src/app/login/services/login.service.ts
+++ b/ui/src/app/login/services/login.service.ts
@@ -19,9 +19,8 @@
 import { Injectable } from '@angular/core';
 import { HttpClient, HttpContext } from '@angular/common/http';
 import { PlatformServicesCommons } from '@streampipes/platform-services';
-import { Observable } from 'rxjs';
+import { Observable, shareReplay } from 'rxjs';
 import { LoginModel } from '../components/login/login.model';
-import { map } from 'rxjs/operators';
 import { RegistrationModel } from '../components/register/registration.model';
 import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client';
 
@@ -32,10 +31,17 @@ export class LoginService {
         private platformServicesCommons: PlatformServicesCommons,
     ) {}
 
+    private settings$?: Observable<LoginModel>;
+
     fetchLoginSettings(): Observable<LoginModel> {
-        return this.http
-            .get(`${this.platformServicesCommons.apiBasePath}/auth/settings`)
-            .pipe(map(res => res as LoginModel));
+        if (!this.settings$) {
+            this.settings$ = this.http
+                .get<LoginModel>(
+                    
`${this.platformServicesCommons.apiBasePath}/auth/settings`,
+                )
+                .pipe(shareReplay({ bufferSize: 1, refCount: true }));
+        }
+        return this.settings$;
     }
 
     login(credentials): Observable<any> {


Reply via email to