Browse Source

UI: expiration message for activation link dialog, expired activation or password reset links redirect to message view about expiration

pull/11578/head
rusikv 2 years ago
parent
commit
620322e3f6
  1. 6
      ui-ngx/src/app/core/http/user.service.ts
  2. 2
      ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html
  3. 18
      ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts
  4. 8
      ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts
  5. 6
      ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts
  6. 20
      ui-ngx/src/app/modules/login/login-routing.module.ts
  7. 4
      ui-ngx/src/app/modules/login/login.module.ts
  8. 40
      ui-ngx/src/app/modules/login/pages/login/link-expired.component.html
  9. 32
      ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss
  10. 47
      ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts
  11. 5
      ui-ngx/src/app/shared/models/user.model.ts
  12. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6
ui-ngx/src/app/core/http/user.service.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { User, UserEmailInfo } from '@shared/models/user.model';
import { ActivationLinkInfo, User, UserEmailInfo } from '@shared/models/user.model';
import { Observable } from 'rxjs';
import { HttpClient, HttpParams } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
@ -77,6 +77,10 @@ export class UserService {
{...{responseType: 'text'}, ...defaultHttpOptionsFromConfig(config)});
}
public getActivationLinkInfo(userId: string, config?: RequestConfig): Observable<ActivationLinkInfo> {
return this.http.get<ActivationLinkInfo>(`/api/user/${userId}/activationLinkInfo`, defaultHttpOptionsFromConfig(config));
}
public sendActivationEmail(email: string, config?: RequestConfig) {
const encodeEmail = encodeURIComponent(email);
return this.http.post(`/api/user/sendActivationMail?email=${encodeEmail}`, null, defaultHttpOptionsFromConfig(config));

2
ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html

@ -30,7 +30,7 @@
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
<div class="mat-content" fxLayout="column">
<span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink}"></span>
<span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink, activationLinkTtl: activationLinkTtl}"></span>
<div fxLayout="row" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre>
<button mat-icon-button

18
ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -22,29 +22,31 @@ import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ActivationLinkInfo } from '@shared/models/user.model';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
export interface ActivationLinkDialogData {
activationLink: string;
activationLinkInfo: ActivationLinkInfo;
}
@Component({
selector: 'tb-activation-link-dialog',
templateUrl: './activation-link-dialog.component.html'
})
export class ActivationLinkDialogComponent extends DialogComponent<ActivationLinkDialogComponent, void> implements OnInit {
export class ActivationLinkDialogComponent extends DialogComponent<ActivationLinkDialogComponent, void> {
activationLink: string;
activationLinkTtl: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData,
public dialogRef: MatDialogRef<ActivationLinkDialogComponent, void>,
private translate: TranslateService) {
private translate: TranslateService,
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe) {
super(store, router, dialogRef);
this.activationLink = this.data.activationLink;
}
ngOnInit(): void {
this.activationLink = this.data.activationLinkInfo.value;
this.activationLinkTtl = this.millisecondsToTimeStringPipe.transform(this.data.activationLinkInfo.ttlMs);
}
close(): void {

8
ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts

@ -21,7 +21,7 @@ import { AppState } from '@core/core.state';
import { UntypedFormGroup } from '@angular/forms';
import { UserComponent } from '@modules/home/pages/user/user.component';
import { Authority } from '@shared/models/authority.enum';
import { ActivationMethod, activationMethodTranslations, User } from '@shared/models/user.model';
import { ActivationLinkInfo, ActivationMethod, activationMethodTranslations, User } from '@shared/models/user.model';
import { CustomerId } from '@shared/models/id/customer-id';
import { UserService } from '@core/http/user.service';
import { Observable } from 'rxjs';
@ -88,7 +88,7 @@ export class AddUserDialogComponent extends DialogComponent<AddUserDialogCompone
this.userService.saveUser(this.user, sendActivationEmail).subscribe(
(user) => {
if (this.activationMethod === ActivationMethod.DISPLAY_ACTIVATION_LINK) {
this.userService.getActivationLink(user.id.id).subscribe(
this.userService.getActivationLinkInfo(user.id.id).subscribe(
(activationLink) => {
this.displayActivationLink(activationLink).subscribe(
() => {
@ -105,13 +105,13 @@ export class AddUserDialogComponent extends DialogComponent<AddUserDialogCompone
}
}
displayActivationLink(activationLink: string): Observable<void> {
displayActivationLink(activationLinkInfo: ActivationLinkInfo): Observable<void> {
return this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData,
void>(ActivationLinkDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
activationLink
activationLinkInfo
}
}).afterClosed();
}

6
ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts

@ -193,14 +193,14 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>
if ($event) {
$event.stopPropagation();
}
this.userService.getActivationLink(user.id.id).subscribe(
(activationLink) => {
this.userService.getActivationLinkInfo(user.id.id).subscribe(
(activationLinkInfo) => {
this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData,
void>(ActivationLinkDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
activationLink
activationLinkInfo
}
});
}

20
ui-ngx/src/app/modules/login/login-routing.module.ts

@ -24,6 +24,7 @@ import { ResetPasswordComponent } from '@modules/login/pages/login/reset-passwor
import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component';
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
import { Authority } from '@shared/models/authority.enum';
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
const routes: Routes = [
{
@ -81,6 +82,25 @@ const routes: Routes = [
module: 'public'
},
canActivate: [AuthGuard]
},
{
path: 'activationLinkExpired',
component: LinkExpiredComponent,
data: {
title: 'login.activation-link-expired',
module: 'public'
},
canActivate: [AuthGuard]
},
{
path: 'passwordResetLinkExpired',
component: LinkExpiredComponent,
data: {
title: 'login.reset-password-link-expired',
module: 'public',
passwordLinkExpired: true
},
canActivate: [AuthGuard]
}
];

4
ui-ngx/src/app/modules/login/login.module.ts

@ -24,6 +24,7 @@ import { ResetPasswordRequestComponent } from '@modules/login/pages/login/reset-
import { ResetPasswordComponent } from '@modules/login/pages/login/reset-password.component';
import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component';
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
@NgModule({
declarations: [
@ -31,7 +32,8 @@ import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-fact
ResetPasswordRequestComponent,
ResetPasswordComponent,
CreatePasswordComponent,
TwoFactorAuthLoginComponent
TwoFactorAuthLoginComponent,
LinkExpiredComponent
],
imports: [
CommonModule,

40
ui-ngx/src/app/modules/login/pages/login/link-expired.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed 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.
-->
<div class="tb-expired-link-content mat-app-background tb-dark tb-flex row center align-center"
style="width: 100%;">
<mat-card appearance="raised" class="tb-expired-link-card">
<mat-card-header style="justify-content: center">
<mat-card-title>
<span class="mat-headline-5">{{ title | translate }}</span>
</mat-card-title>
</mat-card-header>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<span style="height: 4px;" *ngIf="!(isLoading$ | async)"></span>
<mat-card-content style="padding: 24px 16px">
<div>{{ message | translate }}</div>
</mat-card-content>
<mat-card-actions class="tb-flex row center">
<button mat-raised-button color="accent"
[disabled]="(isLoading$ | async)"
(click)="navigateToLoginPage()">
{{ 'login.login' | translate }}
</button>
</mat-card-actions>
</mat-card>
</div>

32
ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed 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 '../../../../../scss/constants';
:host {
display: flex;
flex: 1 1 0;
.tb-expired-link-content {
background-color: #eee;
.tb-expired-link-card {
letter-spacing: 0.15px;
line-height: 24px;
padding: 24px;
@media #{$mat-gt-xs} {
width: 486px !important;
}
}
}
}

47
ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts

@ -0,0 +1,47 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed 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 } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'tb-link-expired',
templateUrl: './link-expired.component.html',
styleUrls: ['./link-expired.component.scss']
})
export class LinkExpiredComponent extends PageComponent {
isPasswordLinkExpired: boolean;
title: string;
message: string;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private router: Router) {
super(store);
this.isPasswordLinkExpired = this.route.snapshot.data.passwordLinkExpired;
this.title = this.isPasswordLinkExpired ? 'login.reset-password-link-expired' : 'login.activation-link-expired';
this.message = this.isPasswordLinkExpired ? 'login.reset-password-link-expired-message' :
'login.activation-link-expired-message';
}
navigateToLoginPage() {
this.router.navigateByUrl('login');
}
}

5
ui-ngx/src/app/shared/models/user.model.ts

@ -44,6 +44,11 @@ export const activationMethodTranslations = new Map<ActivationMethod, string>(
]
);
export interface ActivationLinkInfo {
value: string;
ttlMs: number;
}
export interface AuthUser {
sub: string;
scopes: string[];

8
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4009,7 +4009,11 @@
"email-auth-description": "A security code has been sent to your email address at {{contact}}.",
"email-auth-placeholder": "Email code",
"backup-code-auth-description": "Please enter one of your backup codes.",
"backup-code-auth-placeholder": "Backup code"
"backup-code-auth-placeholder": "Backup code",
"activation-link-expired": "Activation link has expired",
"activation-link-expired-message": "The link to activate your profile has expired. You can return to the login page to receive a new email.",
"reset-password-link-expired": "Password reset link has expired",
"reset-password-link-expired-message": "The link to reset your password has expired. You can return to the login page to receive a new email."
},
"markdown": {
"edit": "Edit",
@ -5629,7 +5633,7 @@
"display-activation-link": "Display activation link",
"send-activation-mail": "Send activation mail",
"activation-link": "User activation link",
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> (expires in {{activationLinkTtl}}) :",
"copy-activation-link": "Copy activation link",
"activation-link-copied-message": "User activation link has been copied to clipboard",
"details": "Details",

Loading…
Cancel
Save