Started adding the ability for them to self manage their accounts

pull/4113/head v4.0.1257
tidusjar 3 years ago
parent 612fbb213f
commit 27f0d6e225

@ -68,8 +68,11 @@ export interface IIdentityResult {
successful: boolean;
}
export interface IUpdateLocalUser extends IUser {
export interface IUpdateLocalUser {
currentPassword: string;
password: string;
id: string;
emailAddress: string;
confirmNewPassword: string;
}

@ -8,29 +8,29 @@
<mat-nav-list>
<span *ngFor="let nav of navItems">
<div *ngIf="(nav.requiresAdmin && isAdmin || !nav.requiresAdmin) && nav.enabled">
<div class="menu-spacing" *ngIf="(nav.requiresAdmin && isAdmin || !nav.requiresAdmin) && nav.enabled">
<a id="{{nav.id}}" *ngIf="nav.externalLink" mat-list-item [href]="nav.link" target="_blank"
matTooltip="{{nav.toolTipMessage | translate}}" matTooltipPosition="right"
[routerLinkActive]="'active-list-item'">
<i *ngIf="nav.icon" class="fa-lg {{nav.icon}}"
<i *ngIf="nav.icon" class="fa-lg {{nav.icon}} icon-spacing"
style="padding-left: 5px; padding-right: 5px;" aria-hidden="true"></i>
&nbsp;{{nav.name | translate}}
</a>
<a id="{{nav.id}}" *ngIf="!nav.externalLink" mat-list-item [routerLink]="nav.link" [style]="nav.color"
[routerLinkActive]="'active-list-item'">
<i class="fa-lg {{nav.icon}}"></i>
<i class="fa-lg {{nav.icon}} icon-spacing"></i>
&nbsp;{{nav.name | translate}}
</a>
</div>
</span>
<a id="nav-logout" mat-list-item [routerLinkActive]="'active-list-item'"
<a class="menu-spacing" id="nav-logout" mat-list-item [routerLinkActive]="'active-list-item'"
aria-label="Toggle sidenav" (click)="logOut();">
<i class="fa-lg fas fa-sign-out-alt"></i>
<i class="fa-lg fas fa-sign-out-alt icon-spacing"></i>
&nbsp;{{ 'NavigationBar.Logout' | translate }}
</a>
@ -82,7 +82,7 @@
</div>
<div class="col-1">
<a routerLink="/user-preferences">
<img [matTooltip]="username" class="profile-img" [src]="getUserImage()" />
<img [matTooltip]="username" id="profile-image" class="profile-img" [src]="getUserImage()" />
</a>
</div>

@ -19,6 +19,14 @@
display: flex;
}
.menu-spacing {
margin-bottom: 5%;
}
.icon-spacing {
margin-right: 5%;
}
.example-form {
min-width: 150px;
max-width: 500px;

@ -1,54 +1,135 @@
<div class="small-middle-container" *ngIf="username">
<h3 [translate]="'UserPreferences.Welcome'" [translateParams]="{username: username}"></h3>
<hr>
<div class="row top-spacing">
<div class="col-4">
<div>
<small>{{'UserPreferences.LanguageDescription' | translate}}</small>
<br>
<mat-form-field>
<mat-label [translate]="'UserPreferences.OmbiLanguage'"></mat-label>
<mat-select id="langSelect" [(value)]="selectedLang" (selectionChange)="languageSelected();">
<mat-option id="langSelect{{lang.value}}" *ngFor="let lang of availableLanguages" [value]="lang.value">
{{lang.display}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
</div>
<div class="row h-100">
<div class="col-1">
<img class="profile-img" [src]="getProfileImage()">
</div>
<div class="col-1"></div>
<div class="col-7">
<mat-label [translate]="'UserPreferences.MobileQRCode'"></mat-label>
<div id="noQrCode" *ngIf="!qrCodeEnabled" [translate]="'UserPreferences.NoQrCode'"></div>
<qrcode id="qrCode" *ngIf="qrCodeEnabled" [qrdata]="qrCode" [size]="256" [level]="'L'"></qrcode>
<button mat-raised-button (click)="openMobileApp($event)" *ngIf="customizationSettings.applicationUrl"> {{
'UserPreferences.LegacyApp' | translate }}</button>
<div class="col-11 align-middle">
<h2 id="usernameTitle">{{username}} <small id="emailTitle" *ngIf="user.emailAddress">({{user.emailAddress}})</small></h2>
</div>
</div>
<mat-tab-group>
<mat-tab label="Profile">
<div class="tab-content">
<div class="row">
<div class="col-1">
User Type:
</div>
<div class="col-11">
{{UserType[user.userType]}}
</div>
</div>
<div class="row">
<div class="col-4">
<div>
<small>{{'UserPreferences.LanguageDescription' | translate}}</small>
<br>
<mat-form-field>
<mat-label [translate]="'UserPreferences.OmbiLanguage'"></mat-label>
<mat-select id="langSelect" [(value)]="selectedLang" (selectionChange)="languageSelected();">
<mat-option id="langSelect{{lang.value}}" *ngFor="let lang of availableLanguages"
[value]="lang.value">
{{lang.display}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
</div>
</div>
<div class="col-1"></div>
<div class="col-4">
<div>
<small>{{'UserPreferences.StreamingCountryDescription' | translate}}</small>
<br>
<mat-form-field>
<mat-label [translate]="'UserPreferences.StreamingCountry'"></mat-label>
<mat-select id="streamingSelect" [(value)]="selectedCountry" (selectionChange)="countrySelected();">
<mat-option id="streamingSelect{{value}}" *ngFor="let value of countries" [value]="value">
{{value}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
</div>
</div>
</div>
<div class="col-4">
<div>
<small>{{'UserPreferences.StreamingCountryDescription' | translate}}</small>
<br>
<mat-form-field>
<mat-label [translate]="'UserPreferences.StreamingCountry'"></mat-label>
<mat-select id="streamingSelect" [(value)]="selectedCountry" (selectionChange)="countrySelected();">
<mat-option id="streamingSelect{{value}}" *ngFor="let value of countries" [value]="value">
{{value}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
</mat-tab>
<mat-tab *ngIf="user.userType === UserType.LocalUser" label="Security">
<div class="tab-content">
<h2>Change Details</h2>
<form novalidate [formGroup]="passwordForm" (ngSubmit)="updatePassword()">
<div class="row">
<div class="col-md-6 col-12">
<span>You need your current password to make any changes here</span>
<mat-form-field appearance="outline" floatLabel=always>
<mat-label>Current Password</mat-label>
<input id="currentPassword" matInput type="password" formControlName="currentPassword">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6 col-12">
<mat-form-field appearance="outline" floatLabel=always>
<mat-label>Email Address</mat-label>
<input id="email" matInput formControlName="emailAddress">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6 col-12">
<mat-form-field appearance="outline" floatLabel=always>
<mat-label>New Password</mat-label>
<input id="newPassword" matInput type="password" formControlName="password">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-6 col-12">
<mat-form-field appearance="outline" floatLabel=always>
<mat-label>New Password Confirm</mat-label>
<input id="confirmPassword" matInput type="password" formControlName="confirmPassword">
</mat-form-field>
</div>
</div>
<button id="submitSecurity" mat-raised-button color="accent" type="submit">Update</button>
</form>
</div>
</div>
</mat-tab>
<mat-tab label="Preferences"> Coming Soon... </mat-tab>
<mat-tab label="Mobile">
<div class="tab-content">
<div class="col-7">
<mat-label [translate]="'UserPreferences.MobileQRCode'"></mat-label>
<div id="noQrCode" *ngIf="!qrCodeEnabled" [translate]="'UserPreferences.NoQrCode'"></div>
<qrcode id="qrCode" *ngIf="qrCodeEnabled" [qrdata]="qrCode" [size]="256" [level]="'L'"></qrcode>
<button mat-raised-button (click)="openMobileApp($event)" *ngIf="customizationSettings.applicationUrl"> {{
'UserPreferences.LegacyApp' | translate }}</button>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</div>

@ -1,5 +1,23 @@
.small-middle-container{
margin: auto;
margin-top: 3%;
width: 80%;
width: 90%;
}
.profile-img {
border-radius: 100%;
width: 75px;
}
.my-auto {
margin-top: auto;
margin-bottom: auto;
}
::ng-deep .mat-tab-body-content {
height: 100%;
overflow: hidden !important;
}
.tab-content {
margin-top: 1%;
}

@ -2,9 +2,10 @@ import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../../auth/auth.service";
import { TranslateService } from "@ngx-translate/core";
import { AvailableLanguages, ILanguage } from "./user-preference.constants";
import { StorageService } from "../../../shared/storage/storage-service";
import { IdentityService, NotificationService, SettingsService } from "../../../services";
import { ICustomizationSettings, IUser } from "../../../interfaces";
import { IdentityService, NotificationService, SettingsService, ValidationService } from "../../../services";
import { ICustomizationSettings, IUser, UserType } from "../../../interfaces";
import { Md5 } from "ts-md5";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
@Component({
templateUrl: "./user-preference.component.html",
@ -20,6 +21,9 @@ export class UserPreferenceComponent implements OnInit {
public countries: string[];
public selectedCountry: string;
public customizationSettings: ICustomizationSettings;
public UserType = UserType;
public passwordForm: FormGroup;
private user: IUser;
@ -27,7 +31,9 @@ export class UserPreferenceComponent implements OnInit {
private readonly translate: TranslateService,
private readonly notification: NotificationService,
private readonly identityService: IdentityService,
private readonly settingsService: SettingsService) { }
private readonly settingsService: SettingsService,
private readonly fb: FormBuilder,
private readonly validationService: ValidationService) { }
public async ngOnInit() {
const user = this.authService.claims();
@ -52,6 +58,23 @@ export class UserPreferenceComponent implements OnInit {
this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x);
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.passwordForm = this.fb.group({
password: [null],
currentPassword: [null, Validators.required],
confirmPassword: [null],
emailAddress: [this.user.emailAddress, Validators.email]
})
this.passwordForm.controls.password.valueChanges.subscribe(x => {
if (x) {
this.validationService.enableValidation(this.passwordForm, "confirmPassword");
}
});
this.passwordForm.controls.confirmPassword.valueChanges.subscribe(x => {
if (x) {
this.validationService.enableValidation(this.passwordForm, "password");
}
});
}
public languageSelected() {
@ -71,4 +94,55 @@ export class UserPreferenceComponent implements OnInit {
window.location.assign(url);
});
}
public getProfileImage(): string {
let emailHash: string|Int32Array;
if (this.user.emailAddress) {
const md5 = new Md5();
emailHash = md5.appendStr(this.user.emailAddress).end();
}
var fallback = this.customizationSettings.logo ? this.customizationSettings.logo : 'https://raw.githubusercontent.com/Ombi-app/Ombi/gh-pages/img/android-chrome-512x512.png';
return `https://www.gravatar.com/avatar/${emailHash}?d=${fallback}`;
}
public updatePassword() {
if (this.passwordForm.invalid) {
this.passwordForm.markAsDirty();
return;
}
var values = this.passwordForm.value;
this.identityService.updateLocalUser({
password: values.password,
confirmNewPassword: values.confirmPassword,
emailAddress: values.emailAddress,
id: this.user.id,
currentPassword: values.currentPassword
}).subscribe(x => {
if (x.successful) {
this.notification.success("Updated your information");
this.user.emailAddress = values.emailAddress;
} else {
this.notification.error(x.errors[0]);
}
})
}
private welcomeText: string;
private setWelcomeText() {
var d = new Date();
var hour = d.getHours();
if (hour >= 0 && hour < 12) {
this.welcomeText = 'NavigationBar.MorningWelcome';
}
if (hour >= 12 && hour < 18) {
this.welcomeText = 'NavigationBar.AfternoonWelcome';
}
if (hour >= 18 && hour < 24) {
this.welcomeText = 'NavigationBar.EveningWelcome';
}
}
}

@ -7,12 +7,15 @@ import { SharedModule } from "../shared/shared.module";
import { QRCodeModule } from 'angularx-qrcode';
import * as fromComponents from './components';
import { ReactiveFormsModule } from "@angular/forms";
import { ValidationService } from "../services";
@NgModule({
imports: [
RouterModule.forChild(fromComponents.routes),
SharedModule,
ReactiveFormsModule,
MatCheckboxModule,
QRCodeModule,
],
@ -23,6 +26,7 @@ import * as fromComponents from './components';
RouterModule,
],
providers: [
ValidationService,
],
})

@ -1,47 +0,0 @@
<div *ngIf="form">
<h3>Hello {{form.value.username}}!</h3>
<div class="col-md-6">
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<div class="modal-body" style="margin-top:45px;">
<div class="form-group">
<label for="emailAddress" class="control-label">Email Address</label>
<div>
<input type="text" formControlName="emailAddress" class="form-control form-control-custom " id="emailAddress" name="emailAddress">
</div>
</div>
<div class="form-group">
<label for="currentPassword" class="control-label">Current Password</label>
<div>
<input type="password" formControlName="currentPassword" class="form-control form-control-custom " id="currentPassword" name="currentPassword">
</div>
</div>
<div class="form-group">
<label for="password" class="control-label">New Password</label>
<div>
<input type="password" formControlName="password" class="form-control form-control-custom " id="password" name="password">
</div>
</div>
<div class="form-group">
<label for="confirmPassword" class="control-label">Confirm New Password</label>
<div>
<input type="password" formControlName="confirmNewPassword" class="form-control form-control-custom " id="confirmPassword"
name="confirmPassword">
</div>
</div>
</div>
<div>
<button type="submit" data-test="submitbtn" class="btn btn-primary-outline" [disabled]="form.invalid">Save</button>
</div>
</form>
</div>
<div class="col-md-6">
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('emailAddress').hasError('email')">Email address format is incorrect</div>
<div *ngIf="form.get('password').hasError('required')">The Password is required</div>
<div *ngIf="form.get('confirmNewPassword').hasError('required')">The Confirm New Password is required</div>
<div *ngIf="form.get('currentPassword').hasError('required')">Your current password is required</div>
</div>
</div>
</div>

@ -1,59 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { IUpdateLocalUser } from "../interfaces";
import { IdentityService } from "../services";
import { NotificationService } from "../services";
@Component({
templateUrl: "./updatedetails.component.html",
})
export class UpdateDetailsComponent implements OnInit {
public form: FormGroup;
constructor(private identityService: IdentityService,
private notificationService: NotificationService,
private fb: FormBuilder) { }
public ngOnInit() {
this.identityService.getUser().subscribe(x => {
const localUser = x as IUpdateLocalUser;
this.form = this.fb.group({
id: [localUser.id],
username: [localUser.userName],
emailAddress: [localUser.emailAddress, [Validators.email]],
confirmNewPassword: [localUser.confirmNewPassword],
currentPassword: [localUser.currentPassword, [Validators.required]],
password: [localUser.password],
});
});
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
if (form.controls.password.dirty) {
if (form.value.password !== form.value.confirmNewPassword) {
this.notificationService.error("Passwords do not match");
return;
}
}
this.identityService.updateLocalUser(this.form.value).subscribe(x => {
if (x.successful) {
this.notificationService.success(`All of your details have now been updated`);
} else {
x.errors.forEach((val) => {
this.notificationService.error(val);
});
}
});
}
}

@ -7,8 +7,6 @@ import { MultiSelectModule } from "primeng/multiselect";
import { SidebarModule } from "primeng/sidebar";
import { TooltipModule } from "primeng/tooltip";
import { UpdateDetailsComponent } from "./updatedetails.component";
import { UserManagementUserComponent } from "./usermanagement-user.component";
import { UserManagementComponent } from "./usermanagement.component";
@ -25,7 +23,6 @@ const routes: Routes = [
{ path: "", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "user", component: UserManagementUserComponent, canActivate: [AuthGuard] },
{ path: "user/:id", component: UserManagementUserComponent, canActivate: [AuthGuard] },
{ path: "updatedetails", component: UpdateDetailsComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -44,7 +41,6 @@ const routes: Routes = [
],
declarations: [
UserManagementComponent,
UpdateDetailsComponent,
UserManagementUserComponent,
],
exports: [

@ -73,7 +73,7 @@ class NavBar {
}
get userPreferences(): Cypress.Chainable<any> {
return cy.get('#nav-userPreferences');
return cy.get('#profile-image');
}
get logout(): Cypress.Chainable<any> {

@ -1,7 +1,6 @@
import { BasePage } from "../base.page";
class UserPreferencesPage extends BasePage {
class ProfileTab {
get languageSelectBox(): Cypress.Chainable<any> {
return cy.get('#langSelect');
}
@ -17,6 +16,9 @@ class UserPreferencesPage extends BasePage {
streamingSelectBoxOption(country: string): Cypress.Chainable<any> {
return cy.get('#streamingSelect'+country);
}
}
class MobileTab {
get qrCode(): Cypress.Chainable<any> {
return cy.get('#qrCode');
@ -25,6 +27,59 @@ class UserPreferencesPage extends BasePage {
get noQrCode(): Cypress.Chainable<any> {
return cy.get('#noQrCode');
}
}
class SecurityTab {
get currentPassword(): Cypress.Chainable<any> {
return cy.get('#currentPassword');
}
get email(): Cypress.Chainable<any> {
return cy.get('#email');
}
get newPassword(): Cypress.Chainable<any> {
return cy.get('#newPassword');
}
get confirmPassword(): Cypress.Chainable<any> {
return cy.get('#confirmPassword');
}
get submitButton(): Cypress.Chainable<any> {
return cy.get('#submitSecurity');
}
}
class UserPreferencesPage extends BasePage {
get username(): Cypress.Chainable<any> {
return cy.get('#usernameTitle');
}
get email(): Cypress.Chainable<any> {
return cy.get('#emailTitle');
}
get profileTab(): Cypress.Chainable<any> {
return cy.get('[role="tab"]').eq(0);
}
get securityTab(): Cypress.Chainable<any> {
return cy.get('[role="tab"]').eq(1);
}
get preferencesTab(): Cypress.Chainable<any> {
return cy.get('[role="tab"]').eq(2);
}
get mobileTab(): Cypress.Chainable<any> {
return cy.get('[role="tab"]').eq(3);
}
profile = new ProfileTab();
mobile = new MobileTab();
security = new SecurityTab();
constructor() {
super();

@ -120,4 +120,6 @@ Cypress.Commands.add("getByData", (selector) => {
}
}
});
});

@ -1,6 +1,6 @@
import { userPreferencesPage as Page } from "@/integration/page-objects";
describe("User Preferences Tests", () => {
describe("User Preferences Profile Tests", () => {
beforeEach(() => {
cy.login();
});
@ -16,8 +16,8 @@ langs.forEach((l) => {
cy.intercept('POST','/language').as('langSave');
Page.visit();
Page.languageSelectBox.click();
Page.languageSelectBoxOption(l.code).click();
Page.profile.languageSelectBox.click();
Page.profile.languageSelectBoxOption(l.code).click();
Page.navbar.discover.contains(l.discover);
@ -42,10 +42,10 @@ streamingCountries.forEach((country) => {
Page.visit();
cy.wait('@countryApi');
Page.streamingSelectBox.click();
Page.streamingSelectBoxOption(country).click();
Page.profile.streamingSelectBox.click();
Page.profile.streamingSelectBoxOption(country).click();
Page.streamingSelectBox.should('have.attr','ng-reflect-value', country);
Page.profile.streamingSelectBox.should('have.attr','ng-reflect-value', country);
cy.wait('@countryApiSave').then((intercept) => {
expect(intercept.request.body.code).equal(country);

@ -0,0 +1,69 @@
import { userPreferencesPage as Page } from "@/integration/page-objects";
describe("User Preferences Security Tests", () => {
beforeEach(() => {
cy.login();
Page.visit();
Page.securityTab.click();
});
it(`Change Email Address Requires Current Password`, () => {
Page.security.email.clear();
Page.security.email.type('test@test.com');
Page.security.submitButton.click();
Page.security.currentPassword.should('have.class', 'ng-invalid');
});
it(`Change Password Requires Current Password`, () => {
Page.security.newPassword.type('test@test.com');
Page.security.confirmPassword.type('test@test.com');
Page.security.submitButton.click();
Page.security.currentPassword.should('have.class', 'ng-invalid');
});
it(`Change Email incorrect password`, () => {
Page.security.currentPassword.type('incorrect');
Page.security.email.clear();
Page.security.email.type('test@test.com');
Page.security.submitButton.click();
cy.verifyNotification('password is incorrect');
});
it(`Change password, existing password incorrect`, () => {
Page.security.currentPassword.type('incorrect');
Page.security.newPassword.type('test@test.com');
Page.security.confirmPassword.type('test@test.com');
Page.security.submitButton.click();
cy.verifyNotification('password is incorrect');
});
it("Change password of user", () => {
cy.generateUniqueId().then((id) => {
const roles = [];
roles.push({ value: "RequestMovie", enabled: true });
cy.createUser(id, "a", roles).then(() => {
cy.removeLogin();
cy.loginWithCreds(id, "a");
Page.visit();
Page.securityTab.click();
Page.security.currentPassword.type('a');
Page.security.email.clear();
Page.security.email.type('test@test.com');
Page.security.newPassword.type('b');
Page.security.confirmPassword.type('b');
Page.security.submitButton.click();
cy.verifyNotification('Updated your information');
Page.email.should('have.text','(test@test.com)')
});
});
});
});
Loading…
Cancel
Save