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> 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> 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(/ |\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> {