diff --git a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts index 148c5791b..f28075ac0 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts @@ -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; } diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html index 849fb8223..3ca868685 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html @@ -8,29 +8,29 @@ -
+ - - +  {{ 'NavigationBar.Logout' | translate }} @@ -82,7 +82,7 @@
diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss index c9daf031c..135220631 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss @@ -19,6 +19,14 @@ display: flex; } +.menu-spacing { + margin-bottom: 5%; +} + +.icon-spacing { + margin-right: 5%; +} + .example-form { min-width: 150px; max-width: 500px; diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.html b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.html index ab47343e4..2a3203f68 100644 --- a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.html +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.html @@ -1,54 +1,135 @@
-

- - -
-
-
-
- {{'UserPreferences.LanguageDescription' | translate}} -
- - - - - {{lang.display}} - - - -
-
- -
+
+
+
-
-
- -
- - +
+

{{username}} ({{user.emailAddress}})

+
+
+ + + + +
+ +
+
+ User Type: +
+
+ {{UserType[user.userType]}} +
+
+ +
+
+
+ {{'UserPreferences.LanguageDescription' | translate}} +
+ + + + + {{lang.display}} + + + +
+
+ +
+
+
+ + +
+
+ {{'UserPreferences.StreamingCountryDescription' | translate}} +
+ + + + + {{value}} + + + +
+
+
+
+
+ + -
-
- {{'UserPreferences.StreamingCountryDescription' | translate}} -
- - - - - {{value}} - - -
-
+ + + +
+

Change Details

+
+
+
+ You need your current password to make any changes here + + Current Password + + +
+
+
+
+ + Email Address + + +
+
+ +
+
+ + New Password + + +
+
+
+
+ + New Password Confirm + + +
+
+ +
-
+ + + + + Coming Soon... + + + +
+
+ +
+ + +
+
+
+ -
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.scss b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.scss index b2bf4612b..faaef4fd5 100644 --- a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.scss +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.scss @@ -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%; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.ts b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.ts index 7969bdd45..50753dfc8 100644 --- a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.ts +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.ts @@ -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'; + } + } } diff --git a/src/Ombi/ClientApp/src/app/user-preferences/user-preferences.module.ts b/src/Ombi/ClientApp/src/app/user-preferences/user-preferences.module.ts index c5a1dec67..de08ed7ec 100644 --- a/src/Ombi/ClientApp/src/app/user-preferences/user-preferences.module.ts +++ b/src/Ombi/ClientApp/src/app/user-preferences/user-preferences.module.ts @@ -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, ], }) diff --git a/src/Ombi/ClientApp/src/app/usermanagement/updatedetails.component.html b/src/Ombi/ClientApp/src/app/usermanagement/updatedetails.component.html deleted file mode 100644 index 97e62b81b..000000000 --- a/src/Ombi/ClientApp/src/app/usermanagement/updatedetails.component.html +++ /dev/null @@ -1,47 +0,0 @@ -
- -

Hello {{form.value.username}}!

-
-
- -
- - -
-
-
-
-
-
Email address format is incorrect
-
The Password is required
-
The Confirm New Password is required
-
Your current password is required
-
-
-
diff --git a/src/Ombi/ClientApp/src/app/usermanagement/updatedetails.component.ts b/src/Ombi/ClientApp/src/app/usermanagement/updatedetails.component.ts deleted file mode 100644 index e44313ad7..000000000 --- a/src/Ombi/ClientApp/src/app/usermanagement/updatedetails.component.ts +++ /dev/null @@ -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); - }); - } - }); - - } - -} diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.module.ts b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.module.ts index 3ac5aed21..885e6e2e4 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.module.ts +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.module.ts @@ -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: [ diff --git a/tests/cypress/integration/page-objects/shared/NavBar.ts b/tests/cypress/integration/page-objects/shared/NavBar.ts index ff164cc58..cdf61eabb 100644 --- a/tests/cypress/integration/page-objects/shared/NavBar.ts +++ b/tests/cypress/integration/page-objects/shared/NavBar.ts @@ -73,7 +73,7 @@ class NavBar { } get userPreferences(): Cypress.Chainable { - return cy.get('#nav-userPreferences'); + return cy.get('#profile-image'); } get logout(): Cypress.Chainable { diff --git a/tests/cypress/integration/page-objects/user-preferences/user-preferences.page.ts b/tests/cypress/integration/page-objects/user-preferences/user-preferences.page.ts index d0b40f822..83f6a8bc5 100644 --- a/tests/cypress/integration/page-objects/user-preferences/user-preferences.page.ts +++ b/tests/cypress/integration/page-objects/user-preferences/user-preferences.page.ts @@ -1,7 +1,6 @@ import { BasePage } from "../base.page"; -class UserPreferencesPage extends BasePage { - +class ProfileTab { get languageSelectBox(): Cypress.Chainable { return cy.get('#langSelect'); } @@ -17,6 +16,9 @@ class UserPreferencesPage extends BasePage { streamingSelectBoxOption(country: string): Cypress.Chainable { return cy.get('#streamingSelect'+country); } +} + +class MobileTab { get qrCode(): Cypress.Chainable { return cy.get('#qrCode'); @@ -25,6 +27,59 @@ class UserPreferencesPage extends BasePage { get noQrCode(): Cypress.Chainable { return cy.get('#noQrCode'); } +} + +class SecurityTab { + get currentPassword(): Cypress.Chainable { + return cy.get('#currentPassword'); + } + + get email(): Cypress.Chainable { + return cy.get('#email'); + } + + get newPassword(): Cypress.Chainable { + return cy.get('#newPassword'); + } + + get confirmPassword(): Cypress.Chainable { + return cy.get('#confirmPassword'); + } + + get submitButton(): Cypress.Chainable { + return cy.get('#submitSecurity'); + } +} + +class UserPreferencesPage extends BasePage { + + + get username(): Cypress.Chainable { + return cy.get('#usernameTitle'); + } + get email(): Cypress.Chainable { + return cy.get('#emailTitle'); + } + + get profileTab(): Cypress.Chainable { + return cy.get('[role="tab"]').eq(0); + } + + get securityTab(): Cypress.Chainable { + return cy.get('[role="tab"]').eq(1); + } + + get preferencesTab(): Cypress.Chainable { + return cy.get('[role="tab"]').eq(2); + } + + get mobileTab(): Cypress.Chainable { + return cy.get('[role="tab"]').eq(3); + } + + profile = new ProfileTab(); + mobile = new MobileTab(); + security = new SecurityTab(); constructor() { super(); diff --git a/tests/cypress/support/commands.ts b/tests/cypress/support/commands.ts index 2a4e47aa9..35d58a63e 100644 --- a/tests/cypress/support/commands.ts +++ b/tests/cypress/support/commands.ts @@ -120,4 +120,6 @@ Cypress.Commands.add("getByData", (selector) => { } } - }); \ No newline at end of file + }); + + \ No newline at end of file diff --git a/tests/cypress/tests/user-preferences/user-preferences.spec.ts b/tests/cypress/tests/user-preferences/user-preferences-profile.spec.ts similarity index 76% rename from tests/cypress/tests/user-preferences/user-preferences.spec.ts rename to tests/cypress/tests/user-preferences/user-preferences-profile.spec.ts index 6e49ead76..19075aba6 100644 --- a/tests/cypress/tests/user-preferences/user-preferences.spec.ts +++ b/tests/cypress/tests/user-preferences/user-preferences-profile.spec.ts @@ -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); diff --git a/tests/cypress/tests/user-preferences/user-preferences-security.spec.ts b/tests/cypress/tests/user-preferences/user-preferences-security.spec.ts new file mode 100644 index 000000000..d0c94a8ac --- /dev/null +++ b/tests/cypress/tests/user-preferences/user-preferences-security.spec.ts @@ -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)') + }); + }); + }); +}); + +