This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch add-optional-terms-acknowledgment in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 833b1531d17c3c13e510122253a66ae93a0d44ed Author: Dominik Riemer <[email protected]> AuthorDate: Wed Aug 27 22:45:27 2025 +0200 feat: Add configurable screen to manage and acknowledge terms --- .../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.scss | 0 .../user-acknowledgment.component.ts} | 36 ++-- .../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 +- 38 files changed, 618 insertions(+), 293 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/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.scss b/ui/src/app/configuration/general-configuration/user-acknowledgement/user-acknowledgment.component.scss new file mode 100644 index 0000000000..e69de29bb2 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 53% 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..f2e03d943e 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,26 @@ * */ -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', + styleUrls: ['./user-acknowledgment.component.scss'], + 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/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> {
