Add functionality to allow a user to change their email address when logged in
parent
3d21653c7c
commit
5d192c1bf8
|
@ -26,13 +26,15 @@ import { Account } from '@account/models/account.model';
|
|||
import { environment } from '../../../environments/environment';
|
||||
import { MembershipType } from '@account/models/membership.model';
|
||||
import { handleError } from '@account/app/app.service';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
|
||||
|
||||
// URLs for the http requests
|
||||
const ACCOUNT_URL = '/api/account';
|
||||
const CHANGE_PASSWORD_URL = '/api/password-change';
|
||||
const CHANGE_EMAIL_ADDRESS_URL = '/api/change-email';
|
||||
const CHANGE_PASSWORD_URL = '/api/change-password';
|
||||
const MEMBERSHIP_URL = '/api/memberships';
|
||||
const VALIDATE_EMAIL_URL = '/api/validate-email';
|
||||
const VERIFY_EMAIL_URL = '/api/verify-email';
|
||||
|
||||
|
||||
export function storeRedirect() {
|
||||
|
@ -87,4 +89,18 @@ export class ProfileService {
|
|||
const codedPassword = btoa(newPassword);
|
||||
return this.http.put(CHANGE_PASSWORD_URL, {password: codedPassword});
|
||||
}
|
||||
|
||||
validateEmailAddress(token: string): Observable<any> {
|
||||
const queryParams = {platform: 'Internal', token: token};
|
||||
return this.http.get(VALIDATE_EMAIL_URL, {params: queryParams});
|
||||
}
|
||||
|
||||
changeEmailAddress(newEmailAddress: string) {
|
||||
const codedEmailAddress = btoa(newEmailAddress);
|
||||
return this.http.put(CHANGE_EMAIL_ADDRESS_URL, {token: codedEmailAddress});
|
||||
}
|
||||
|
||||
verifyEmailAddress(newEmailAddress: string) {
|
||||
return this.http.put<string>(VERIFY_EMAIL_URL, {token: newEmailAddress});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<a mat-button>CHANGE EMAIL ADDRESS</a>
|
||||
<button mat-button (click)="onPasswordChange()">CHANGE PASSWORD</button>
|
||||
<button mat-button (click)="openEmailAddressChangeDialog()">CHANGE EMAIL ADDRESS</button>
|
||||
<button mat-button (click)="openPasswordChangeDialog()">CHANGE PASSWORD</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
|
|
@ -26,6 +26,7 @@ import { Account } from '@account/models/account.model';
|
|||
import { ChangePasswordComponent } from '@account/app/modules/profile/components/views/change-password/change-password.component';
|
||||
import { ProfileService } from '@account/http/profile.service';
|
||||
import { SnackbarComponent } from 'shared';
|
||||
import { ChangeEmailComponent } from '@account/app/modules/profile/components/views/change-email/change-email.component';
|
||||
|
||||
const fiveSeconds = 5000;
|
||||
|
||||
|
@ -40,19 +41,16 @@ export class LoginComponent {
|
|||
|
||||
constructor(
|
||||
private profileService: ProfileService,
|
||||
public passwordDialog: MatDialog,
|
||||
private changeEmailDialog: MatDialog,
|
||||
public changePasswordDialog: MatDialog,
|
||||
private snackbar: MatSnackBar
|
||||
) { }
|
||||
|
||||
onPasswordChange() {
|
||||
this.openPasswordDialog();
|
||||
}
|
||||
|
||||
openPasswordDialog() {
|
||||
openPasswordChangeDialog() {
|
||||
const dialogConfig = new MatDialogConfig();
|
||||
dialogConfig.disableClose = true;
|
||||
dialogConfig.restoreFocus = true;
|
||||
const dialogRef = this.passwordDialog.open(ChangePasswordComponent, dialogConfig);
|
||||
const dialogRef = this.changePasswordDialog.open(ChangePasswordComponent, dialogConfig);
|
||||
dialogRef.afterClosed().subscribe(
|
||||
(newPassword) => {
|
||||
if (newPassword) {
|
||||
|
@ -61,24 +59,37 @@ export class LoginComponent {
|
|||
);
|
||||
}
|
||||
|
||||
updatePassword (newPassword) {
|
||||
updatePassword (newPassword): void {
|
||||
this.profileService.changePassword(newPassword).subscribe({
|
||||
next: () => { this.openSuccessSnackbar(); },
|
||||
error: () => { this.openErrorSnackbar(); }
|
||||
next: () => { this.openSnackbar('success', 'Password successfully changed'); },
|
||||
error: () => { this.openSnackbar('error', 'An error occurred changing the password'); }
|
||||
});
|
||||
}
|
||||
|
||||
openErrorSnackbar() {
|
||||
const config = new MatSnackBarConfig();
|
||||
config.duration = fiveSeconds;
|
||||
config.data = {type: 'error', message: 'An error occurred changing the password'};
|
||||
this.snackbar.openFromComponent(SnackbarComponent, config);
|
||||
openEmailAddressChangeDialog() {
|
||||
const dialogConfig = new MatDialogConfig();
|
||||
dialogConfig.disableClose = true;
|
||||
dialogConfig.restoreFocus = true;
|
||||
const dialogRef = this.changeEmailDialog.open(ChangeEmailComponent, dialogConfig);
|
||||
dialogRef.afterClosed().subscribe(
|
||||
(newEmailAddress) => {
|
||||
if (newEmailAddress) {
|
||||
this.updateEmailAddress(newEmailAddress);
|
||||
}}
|
||||
);
|
||||
}
|
||||
|
||||
openSuccessSnackbar() {
|
||||
updateEmailAddress (newEmailAddress): void {
|
||||
this.profileService.changeEmailAddress(newEmailAddress).subscribe({
|
||||
next: () => { this.openSnackbar('info', 'Check your inbox for a link to verify the new email address'); },
|
||||
error: () => { this.openSnackbar('error', 'An error occurred changing the email address'); }
|
||||
});
|
||||
}
|
||||
|
||||
openSnackbar(type: string, message: string): void {
|
||||
const config = new MatSnackBarConfig();
|
||||
config.duration = fiveSeconds;
|
||||
config.data = {type: 'success', message: 'Password successfully changed'};
|
||||
config.data = {type: type, message: message};
|
||||
this.snackbar.openFromComponent(SnackbarComponent, config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<mat-card class="mat-elevation-z0">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Enter your new email address</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p class="mat-body">
|
||||
Email messages will be sent to both the current and new email addresses.
|
||||
The message sent to the new email address contains a verification link.
|
||||
This change will not be applied to the account until the new email address
|
||||
been verified.
|
||||
</p>
|
||||
<mat-form-field [appearance]="'outline'">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput required [type]="'email'" [formControl]="emailControl">
|
||||
<mat-error *ngIf="emailControl.invalid">
|
||||
{{getEmailError()}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button
|
||||
mat-button
|
||||
id="submit-button"
|
||||
[disabled]="emailControl.invalid"
|
||||
(click)="changeEmail()"
|
||||
>
|
||||
SUBMIT
|
||||
</button>
|
||||
<button mat-button id="cancel-button" (click)="onCancel()">CANCEL</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,52 @@
|
|||
// *****************************************************************************
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//
|
||||
// Copyright (c) Mycroft AI Inc. All rights reserved.
|
||||
// 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
|
||||
//
|
||||
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
||||
// WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
// MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||
//
|
||||
// See the Apache Version 2.0 License for specific language governing permissions
|
||||
// and limitations under the License.
|
||||
// *****************************************************************************
|
||||
|
||||
@use "@angular/material" as mat;
|
||||
@use "components/buttons" as buttons;
|
||||
@use "components/cards" as cards;
|
||||
@use 'mycroft-theme' as theme;
|
||||
|
||||
mat-card {
|
||||
@include cards.selene-card;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
mat-card-content {
|
||||
padding: 16px;
|
||||
|
||||
button {
|
||||
margin-top: -8px;
|
||||
fa-icon {
|
||||
font-size: 20px;
|
||||
color: mat.get-color-from-palette(theme.$mycroft-primary, 500);
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card-actions {
|
||||
#cancel-button {
|
||||
background-color: white;
|
||||
color: mat.get-color-from-palette(theme.$mycroft-accent, 800);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { AbstractControl, AsyncValidatorFn, UntypedFormControl, ValidationErrors, Validators } from '@angular/forms';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
import { ProfileService } from '@account/http/profile.service';
|
||||
|
||||
|
||||
export function uniqueEmailValidator(profileService: ProfileService): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
const token: string = control.value ? btoa(control.value) : '';
|
||||
return profileService.validateEmailAddress(token).pipe(
|
||||
map((response) => response.accountExists ? { duplicateEmail: true } : null),
|
||||
catchError(() => null),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-change-email',
|
||||
templateUrl: './change-email.component.html',
|
||||
styleUrls: ['./change-email.component.scss']
|
||||
})
|
||||
export class ChangeEmailComponent implements OnInit {
|
||||
public emailControl: UntypedFormControl;
|
||||
|
||||
constructor(
|
||||
private profileService: ProfileService,
|
||||
private dialogRef: MatDialogRef<ChangeEmailComponent>,
|
||||
) {
|
||||
this.emailControl = new UntypedFormControl(
|
||||
null,
|
||||
{
|
||||
validators: [Validators.email, Validators.required],
|
||||
asyncValidators: [uniqueEmailValidator(this.profileService)],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
getEmailError(): string {
|
||||
let errorMessage = '';
|
||||
if (this.emailControl.hasError('email')) {
|
||||
errorMessage = 'Must be a valid email address';
|
||||
} else if (this.emailControl.hasError('required')) {
|
||||
errorMessage = 'Email is required';
|
||||
} else if (this.emailControl.hasError('duplicateEmail')) {
|
||||
errorMessage = 'Account already exists for this email';
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
changeEmail() {
|
||||
this.dialogRef.close(this.emailControl.value);
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<mat-card>
|
||||
<mat-card-header>
|
||||
<fa-icon mat-card-avatar [icon]="emailAddressIcon"></fa-icon>
|
||||
<mat-card-title>Email address verification</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p class="mat-body">{{verificationMessage}}</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button
|
||||
*ngIf="!emailVerified"
|
||||
mat-button
|
||||
id="login-button"
|
||||
(click)="loginToAccount()"
|
||||
>
|
||||
LOGIN
|
||||
</button>
|
||||
<button
|
||||
*ngIf="emailVerified"
|
||||
mat-button
|
||||
id="success-button"
|
||||
(click)="navigateToDashboard()"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,7 @@
|
|||
@use 'components/cards' as cards;
|
||||
|
||||
mat-card {
|
||||
@include cards.selene-card;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { faEnvelopeCircleCheck, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||
import { environment } from '@account/environments/environment';
|
||||
import { ProfileService } from '@account/http/profile.service';
|
||||
|
||||
@Component({
|
||||
selector: 'account-verify-email',
|
||||
templateUrl: './verify-email.component.html',
|
||||
styleUrls: ['./verify-email.component.scss']
|
||||
})
|
||||
export class VerifyEmailComponent implements OnInit {
|
||||
public emailAddressIcon: IconDefinition = faEnvelopeCircleCheck;
|
||||
public emailVerified: boolean;
|
||||
public verificationMessage: string;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private profileService: ProfileService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const resetToken = this.route.snapshot.queryParams['token'];
|
||||
this.profileService.verifyEmailAddress(resetToken).subscribe({
|
||||
next: () => {
|
||||
this.verificationMessage = 'Thank you for verifying your new email address. Use it the next time you login.';
|
||||
this.emailVerified = true;
|
||||
},
|
||||
error: (error) => {
|
||||
if (error.status === 401) {
|
||||
this.verificationMessage = 'Your need to be logged in to verify your email address. ' +
|
||||
'The new email address has not yet been applied to your account so login using the email address being replaced.';
|
||||
this.emailVerified = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loginToAccount(): void {
|
||||
window.location.href = environment.mycroftUrls.singleSignOn + '/login?redirect=' + window.location.href;
|
||||
}
|
||||
|
||||
navigateToDashboard(): void {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import { AccountResolverService } from '@account/app/core/guards/account-resolve
|
|||
import { MembershipResolverService } from '@account/app/core/guards/membership-resolver.service';
|
||||
import { EditComponent } from './pages/edit/edit.component';
|
||||
import { NewComponent } from './pages/new/new.component';
|
||||
import { VerifyEmailComponent } from './pages/verify-email/verify-email.component';
|
||||
|
||||
const profileRoutes: Routes = [
|
||||
{
|
||||
|
@ -40,6 +41,10 @@ const profileRoutes: Routes = [
|
|||
membershipTypes: MembershipResolverService
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'verify-email',
|
||||
component: VerifyEmailComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -36,13 +36,16 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
|||
import { NgxStripeModule } from 'ngx-stripe';
|
||||
|
||||
import { AgreementsComponent } from './components/cards/agreements/agreements.component';
|
||||
import { ChangeEmailComponent } from './components/views/change-email/change-email.component';
|
||||
import { ChangePasswordComponent } from './components/views/change-password/change-password.component';
|
||||
import { DeleteComponent } from './components/cards/delete/delete.component';
|
||||
import { DeleteConfirmComponent } from './components/modals/delete-confirm/delete-confirm.component';
|
||||
import { EditComponent } from './pages/edit/edit.component';
|
||||
import { environment} from '../../../environments/environment';
|
||||
import { LoginComponent } from './components/cards/login/login.component';
|
||||
import { MembershipComponent } from './components/cards/membership/membership.component';
|
||||
import { MembershipOptionsComponent } from './components/controls/membership-options/membership-options.component';
|
||||
import { MembershipStepComponent } from './components/views/membership-step/membership-step.component';
|
||||
import { NewComponent } from './pages/new/new.component';
|
||||
import { PaymentComponent } from './components/views/payment/payment.component';
|
||||
import { ProfileService } from '@account/http/profile.service';
|
||||
|
@ -50,15 +53,16 @@ import { ProfileRoutingModule } from './profile-routing.module';
|
|||
import { SharedModule } from 'shared';
|
||||
import { UsernameStepComponent } from './components/views/username-step/username-step.component';
|
||||
import { VerifyCardDialogComponent } from './components/views/payment/verify-card-dialog.component';
|
||||
import { DeleteConfirmComponent } from './components/modals/delete-confirm/delete-confirm.component';
|
||||
import { MembershipStepComponent } from './components/views/membership-step/membership-step.component';
|
||||
import { VerifyEmailComponent } from './pages/verify-email/verify-email.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
// Profile view and edit
|
||||
AgreementsComponent,
|
||||
ChangeEmailComponent,
|
||||
ChangePasswordComponent,
|
||||
DeleteComponent,
|
||||
DeleteConfirmComponent,
|
||||
EditComponent,
|
||||
LoginComponent,
|
||||
MembershipComponent,
|
||||
|
@ -70,7 +74,7 @@ import { MembershipStepComponent } from './components/views/membership-step/memb
|
|||
MembershipOptionsComponent,
|
||||
PaymentComponent,
|
||||
VerifyCardDialogComponent,
|
||||
DeleteConfirmComponent
|
||||
VerifyEmailComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
Loading…
Reference in New Issue