diff options
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form')
4 files changed, 291 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html new file mode 100755 index 000000000..2dc30df52 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html @@ -0,0 +1,95 @@ +<div> + <h2 i18n>Please set a new password.</h2> + <h4 i18n>You will be redirected to the login page afterwards.</h4> + <form #frm="ngForm" + [formGroup]="userForm" + novalidate> + + <!-- Old password --> + <div class="form-group has-feedback"> + <div class="input-group"> + <input class="form-control" + type="password" + placeholder="Old password..." + id="oldpassword" + formControlName="oldpassword" + autocomplete="new-password" + autofocus> + <span class="input-group-append"> + <button class="btn btn-outline-light btn-password" + cdPasswordButton="oldpassword"> + </button> + </span> + </div> + <span class="invalid-feedback" + *ngIf="userForm.showError('oldpassword', frm, 'required')" + i18n>This field is required.</span> + <span class="invalid-feedback" + *ngIf="userForm.showError('oldpassword', frm, 'notmatch')" + i18n>The old and new passwords must be different.</span> + </div> + + <!-- New password --> + <div class="form-group has-feedback"> + <div class="input-group"> + <input class="form-control" + type="password" + placeholder="New password..." + id="newpassword" + autocomplete="new-password" + formControlName="newpassword"> + <span class="input-group-append"> + <button type="button" + class="btn btn-outline-light btn-password" + cdPasswordButton="newpassword"> + </button> + </span> + </div> + <div class="password-strength-level"> + <div class="{{ passwordStrengthLevelClass }}" + data-toggle="tooltip" + title="{{ passwordValuation }}"> + </div> + </div> + <span class="invalid-feedback" + *ngIf="userForm.showError('newpassword', frm, 'required')" + i18n>This field is required.</span> + <span class="invalid-feedback" + *ngIf="userForm.showError('newpassword', frm, 'notmatch')" + i18n>The old and new passwords must be different.</span> + <span class="invalid-feedback" + *ngIf="userForm.showError('newpassword', frm, 'passwordPolicy')"> + {{ passwordValuation }} + </span> + </div> + + <!-- Confirm new password --> + <div class="form-group has-feedback"> + <div class="input-group"> + <input class="form-control" + type="password" + autocomplete="new-password" + placeholder="Confirm new password..." + id="confirmnewpassword" + formControlName="confirmnewpassword"> + <span class="input-group-append"> + <button class="btn btn-outline-light btn-password" + cdPasswordButton="confirmnewpassword"> + </button> + </span> + </div> + <span class="invalid-feedback" + *ngIf="userForm.showError('confirmnewpassword', frm, 'required')" + i18n>This field is required.</span> + <span class="invalid-feedback" + *ngIf="userForm.showError('confirmnewpassword', frm, 'match')" + i18n>Password confirmation doesn't match the new password.</span> + </div> + <cd-form-button-panel (submitActionEvent)="onSubmit()" + (backActionEvent)="onCancel()" + [form]="userForm" + [disabled]="userForm.invalid" + [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)" + wrappingClass="text-right"></cd-form-button-panel> + </form> +</div> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss new file mode 100755 index 000000000..15addd1e8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss @@ -0,0 +1,68 @@ +@use 'sass:map'; +@use './src/styles/vendor/variables' as vv; + +$dark-secondary: darken(vv.$secondary, 4%); + +::ng-deep cd-login-password-form { + h4 { + margin: 0 0 30px; + } + + .form-group { + background-color: $dark-secondary; + border-left: 4px solid vv.$white; + + &:focus-within { + border-left: 4px solid map.get(vv.$theme-colors, 'accent'); + } + } + + .btn-password, + .btn-password:focus, + .form-control, + .form-control:focus { + background-color: $dark-secondary; + border: 0; + box-shadow: none; + color: vv.$body-color-bright; + filter: none; + outline: none; + } + + .form-control::placeholder { + color: vv.$gray-600; + } + + .btn-password:focus { + outline-color: vv.$primary; + } + + button.btn:not(:first-child) { + margin-left: 5px; + } +} + +// This will override the colors applied by chrome +@keyframes autofill { + to { + background-color: $dark-secondary; + color: vv.$body-color-bright; + } +} + +input:-webkit-autofill { + animation-fill-mode: both; + animation-name: autofill; + border-radius: 0; + box-shadow: 0 0 0 1000px $dark-secondary inset; + -webkit-text-fill-color: vv.$body-color-bright; + transition-property: none; +} + +.invalid-feedback { + padding-left: 9px; +} + +.is-invalid.cd-form-control { + border-color: transparent; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts new file mode 100755 index 000000000..062d076e4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts @@ -0,0 +1,77 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastrModule } from 'ngx-toastr'; + +import { AuthService } from '~/app/shared/api/auth.service'; +import { ComponentsModule } from '~/app/shared/components/components.module'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { SharedModule } from '~/app/shared/shared.module'; +import { configureTestBed, FormHelper } from '~/testing/unit-test-helper'; +import { LoginPasswordFormComponent } from './login-password-form.component'; + +describe('LoginPasswordFormComponent', () => { + let component: LoginPasswordFormComponent; + let fixture: ComponentFixture<LoginPasswordFormComponent>; + let form: CdFormGroup; + let formHelper: FormHelper; + let httpTesting: HttpTestingController; + let router: Router; + let authStorageService: AuthStorageService; + let authService: AuthService; + + configureTestBed({ + imports: [ + HttpClientTestingModule, + RouterTestingModule, + ReactiveFormsModule, + ComponentsModule, + ToastrModule.forRoot(), + SharedModule + ], + declarations: [LoginPasswordFormComponent] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginPasswordFormComponent); + component = fixture.componentInstance; + httpTesting = TestBed.inject(HttpTestingController); + router = TestBed.inject(Router); + authStorageService = TestBed.inject(AuthStorageService); + authService = TestBed.inject(AuthService); + spyOn(router, 'navigate'); + fixture.detectChanges(); + form = component.userForm; + formHelper = new FormHelper(form); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should submit', () => { + spyOn(component, 'onPasswordChange').and.callThrough(); + spyOn(authService, 'logout'); + spyOn(authStorageService, 'getUsername').and.returnValue('test1'); + formHelper.setMultipleValues({ + oldpassword: 'foo', + newpassword: 'bar' + }); + formHelper.setValue('confirmnewpassword', 'bar', true); + component.onSubmit(); + const request = httpTesting.expectOne('api/user/test1/change_password'); + request.flush({}); + expect(component.onPasswordChange).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should cancel', () => { + spyOn(authService, 'logout'); + component.onCancel(); + expect(authService.logout).toHaveBeenCalled(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts new file mode 100755 index 000000000..0e72cca35 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthService } from '~/app/shared/api/auth.service'; +import { UserService } from '~/app/shared/api/user.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { PasswordPolicyService } from '~/app/shared/services/password-policy.service'; +import { UserPasswordFormComponent } from '../user-password-form/user-password-form.component'; + +@Component({ + selector: 'cd-login-password-form', + templateUrl: './login-password-form.component.html', + styleUrls: ['./login-password-form.component.scss'] +}) +export class LoginPasswordFormComponent extends UserPasswordFormComponent { + constructor( + public actionLabels: ActionLabelsI18n, + public notificationService: NotificationService, + public userService: UserService, + public authStorageService: AuthStorageService, + public formBuilder: CdFormBuilder, + public router: Router, + public passwordPolicyService: PasswordPolicyService, + public authService: AuthService + ) { + super( + actionLabels, + notificationService, + userService, + authStorageService, + formBuilder, + router, + passwordPolicyService + ); + } + + onPasswordChange() { + // Logout here because changing the password will change the + // session token which will finally lead to a 401 when calling + // the REST API the next time. The API HTTP interceptor will + // then also redirect to the login page immediately. + this.authService.logout(); + } + + onCancel() { + this.authService.logout(); + } +} |