Rework the reset password component (fixes #4079)

This commit is contained in:
FinnStutzenstein 2019-02-08 18:26:37 +01:00 committed by Emanuel Schütze
parent 76210e807f
commit 6a33b68a41
6 changed files with 123 additions and 181 deletions

View File

@ -132,11 +132,15 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
* *
* @param user The user to update * @param user The user to update
* @param password The password to set * @param password The password to set
* @param updateDefaultPassword Control, if the default password should be updated.
*/ */
public async resetPassword(user: ViewUser, password: string): Promise<void> { public async resetPassword(
user: ViewUser,
password: string,
updateDefaultPassword: boolean = false
): Promise<void> {
const path = `/rest/users/user/${user.id}/reset_password/`; const path = `/rest/users/user/${user.id}/reset_password/`;
await this.httpService.post(path, { password: password }); await this.httpService.post(path, { password: password, update_default_password: updateDefaultPassword });
await this.update({ default_password: password }, user);
} }
/** /**

View File

@ -11,24 +11,37 @@
<!-- can Manage, but not own Page (a.k.a. Admin) --> <!-- can Manage, but not own Page (a.k.a. Admin) -->
<div *ngIf="user"> <div *ngIf="user">
<h1><span translate>Change password for</span> {{ user.full_name }}</h1> <h1><span translate>Change password for</span> {{ user.full_name }}</h1>
<mat-icon>warning</mat-icon> <span translate>You override the personally set password!</span> <mat-icon>warning</mat-icon>
&nbsp;<span translate>You override the personally set password!</span>
</div> </div>
<br> <br>
<form [formGroup]="adminPasswordForm" (keydown)="onKeyDown($event)"> <form [formGroup]="adminPasswordForm" (keydown)="onKeyDown($event)">
<mat-form-field> <mat-form-field>
<input <input
[type]="hide_admin_newPassword ? 'password' : 'text'" [type]="hidePassword ? 'password' : 'text'"
matInput matInput
formControlName="admin_newPassword" formControlName="newPassword"
placeholder="{{ 'New password' | translate }}" placeholder="{{ 'New password' | translate }}"
required
/> />
<mat-icon <mat-icon
class="pointer" class="pointer"
matSuffix matSuffix
mat-icon-button mat-icon-button
(click)="admin_generatePassword()"> matTooltip="{{ hidePassword ? ('Show password' | translate) : ('Hide password' | translate) }}"
sync_problem (click)="hidePassword = !hidePassword">
{{ hidePassword ? 'visibility' : 'visibility_off' }}
</mat-icon> </mat-icon>
<mat-icon
class="pointer spacer-left-10"
matSuffix
mat-icon-button
matTooltip="{{ 'Generate password' | translate }}"
(click)="generatePassword()">
settings
</mat-icon>
</mat-form-field> </mat-form-field>
</form> </form>
<br> <br>
@ -36,62 +49,45 @@
<span translate>Initial password</span>: {{ user.default_password }}<br> <span translate>Initial password</span>: {{ user.default_password }}<br>
<span translate>Username</span>: {{ user.username }} <span translate>Username</span>: {{ user.username }}
</div> </div>
<br>
<button
mat-raised-button
color="primary"
(click)="hide_admin_newPassword = !hide_admin_newPassword">
<mat-icon
matSuffix
mat-icon-button>
{{hide_admin_newPassword ? 'visibility_off' : 'visibility'}}
</mat-icon>
&nbsp;<span translate>Show password</span>
</button>
</div> </div>
<div *ngIf="this.ownPage"> <div *ngIf="this.ownPage">
<!-- can not Manage, but own Page (a.k.a. User) --> <!-- can not Manage, but own Page (a.k.a. User) -->
<form [formGroup]="userPasswordForm" (keydown)="onKeyDown($event)"> <form [formGroup]="userPasswordForm" (keydown)="onKeyDown($event)">
<mat-form-field> <mat-form-field>
<input <input
[type]="hide_user_password ? 'password' : 'text'"
matInput matInput
formControlName="user_oldPassword" formControlName="oldPassword"
placeholder="{{ 'Old password' | translate }}" placeholder="{{ 'Old password' | translate }}"
required
/> />
</mat-form-field><br> </mat-form-field><br>
<mat-form-field> <mat-form-field>
<input <input
[type]="hide_user_password ? 'password' : 'text'" [type]="hidePassword ? 'password' : 'text'"
matInput matInput
formControlName="user_newPassword1" formControlName="newPassword1"
placeholder="{{ 'New password' | translate }}" placeholder="{{ 'New password' | translate }}"
required
/> />
<mat-icon <mat-icon
mat-button matSuffix mat-icon-button mat-button matSuffix mat-icon-button
(click)="user_generatePassword()"> (click)="hidePassword = !hidePassword">
sync_problem {{ hidePassword ? 'visibility' : 'visibility_off' }}
</mat-icon> </mat-icon>
</mat-form-field><br> </mat-form-field><br>
<mat-form-field> <mat-form-field>
<input <input
[type]="hide_user_password ? 'password' : 'text'" [type]="hidePassword ? 'password' : 'text'"
matInput matInput
formControlName="user_newPassword2" formControlName="newPassword2"
placeholder="{{ 'Confirm new password' | translate }}" placeholder="{{ 'Confirm new password' | translate }}"
required
/> />
</mat-form-field> </mat-form-field>
</form> </form>
<button
mat-raised-button
color="primary"
(click)="hide_user_password = !hide_user_password">
<mat-icon
matSuffix
mat-icon-button>
{{ hide_user_password ? 'visibility_off' : 'visibility' }}
</mat-icon>
&nbsp;<span translate>Show password</span>
</button>
</div> </div>
</mat-card> </mat-card>

View File

@ -1,10 +1,11 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ViewUser } from '../../models/view-user'; import { ViewUser } from '../../models/view-user';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
@ -29,11 +30,6 @@ export class PasswordComponent extends BaseViewComponent implements OnInit {
*/ */
public ownPage: boolean; public ownPage: boolean;
/**
* user id from url parameter
*/
public userId: number;
/** /**
* if current user has the "can_manage" permission * if current user has the "can_manage" permission
*/ */
@ -50,14 +46,11 @@ export class PasswordComponent extends BaseViewComponent implements OnInit {
public userPasswordForm: FormGroup; public userPasswordForm: FormGroup;
/** /**
* if the new password in userform is hidden * if all password inputs is hidden
*/ */
public hide_user_password = true; public hidePassword = true;
/** private urlUserId: number | null;
* If the new Password in the adminform is hidden
*/
public hide_admin_newPassword = true;
/** /**
* Constructor * Constructor
@ -82,39 +75,48 @@ export class PasswordComponent extends BaseViewComponent implements OnInit {
private formBuilder: FormBuilder private formBuilder: FormBuilder
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.route.params.subscribe(params => {
if (params.id) {
this.userId = params.id;
}
});
if (this.userId === undefined) {
this.operator.getUserObservable().subscribe(user => {
if (user) {
this.userId = user.id;
this.router.navigate([`./users/password/${this.userId}`]);
}
});
}
} }
/** /**
* Initializes the forms and some of the frontend options * Initializes the forms and some of the frontend options
*/ */
public ngOnInit(): void { public ngOnInit(): void {
this.setViewUser(this.userId); this.route.params.subscribe(params => {
this.setOpOwnsPage(this.userId); if (params.id) {
this.urlUserId = +params.id;
this.repo.getViewModelObservable(this.urlUserId).subscribe(() => {
this.updateUser();
});
}
this.updateUser();
});
this.operator.getUserObservable().subscribe(() => {
this.updateUser();
});
this.adminPasswordForm = this.formBuilder.group({ this.adminPasswordForm = this.formBuilder.group({
admin_newPassword: ['', Validators.required] newPassword: ['', Validators.required]
}); });
this.userPasswordForm = this.formBuilder.group({ this.userPasswordForm = this.formBuilder.group({
user_newPassword1: ['', Validators.required], newPassword1: ['', Validators.required],
user_newPassword2: ['', Validators.required], newPassword2: ['', Validators.required],
user_oldPassword: ['', Validators.required] oldPassword: ['', Validators.required]
}); });
} }
private updateUser(): void {
const operator = this.operator.user;
this.ownPage = this.urlUserId ? operator.id === this.urlUserId : true;
if (this.ownPage) {
this.user = this.operator.viewUser;
} else {
this.user = this.repo.getViewModel(this.urlUserId);
}
this.canManage = this.operator.hasPerms('users.can_manage');
}
/** /**
* Triggered by the "x" Button of the Form * Triggered by the "x" Button of the Form
*/ */
@ -126,34 +128,6 @@ export class PasswordComponent extends BaseViewComponent implements OnInit {
} }
} }
/**
* sets the current user that should be worked on
*
* @param userId user id from the route
*/
private setViewUser(userId: number): void {
this.repo.getViewModelObservable(userId).subscribe(newViewUser => {
if (newViewUser) {
this.user = newViewUser;
}
});
}
/**
* sets the parameters if the pw-page is our own and if the current
* user has the can_manage permission
*
* @param userId user id from the route
*/
private setOpOwnsPage(userId: number): void {
this.operator.getUserObservable().subscribe(user => {
if (user) {
this.ownPage = +userId === +user.id;
this.canManage = this.operator.hasPerms('users.can_manage');
}
});
}
/** /**
* Handles the whole save routine for every possible event * Handles the whole save routine for every possible event
*/ */
@ -161,46 +135,32 @@ export class PasswordComponent extends BaseViewComponent implements OnInit {
// can Manage, but not own Page (a.k.a. Admin) // can Manage, but not own Page (a.k.a. Admin)
try { try {
if (this.canManage && !this.ownPage) { if (this.canManage && !this.ownPage) {
const pw = this.adminPasswordForm.get('admin_newPassword').value; if (!this.adminPasswordForm.valid) {
this.adminNewPassword(pw); return;
}
const password = this.adminPasswordForm.value.newPassword;
await this.repo.resetPassword(this.user, password);
this.router.navigate([`./users/${this.user.id}`]); this.router.navigate([`./users/${this.user.id}`]);
} } else if (this.ownPage) {
// can not Manage, but own Page (a.k.a. User) if (!this.userPasswordForm.valid) {
if (this.ownPage) { return;
const oldPw = this.userPasswordForm.get('user_oldPassword').value; }
const newPw1 = this.userPasswordForm.get('user_newPassword1').value; const oldPassword = this.userPasswordForm.value.oldPassword;
const newPw2 = this.userPasswordForm.get('user_newPassword2').value; const newPassword1 = this.userPasswordForm.value.newPassword1;
await this.userNewPassword(newPw1, newPw2, oldPw); const newPassword2 = this.userPasswordForm.value.newPassword2;
this.router.navigate(['./']);
if (newPassword1 !== newPassword2) {
this.raiseError(this.translate.instant('New passwords do not match'));
} else {
await this.repo.setNewPassword(oldPassword, newPassword1);
this.router.navigate(['./']);
}
} }
} catch (e) { } catch (e) {
this.raiseError(e); this.raiseError(e);
} }
} }
/**
* Sends new Password entered in the new password field to server
*
* @param password the password that should be set
*/
private adminNewPassword(password: string): void {
this.repo.resetPassword(this.user, password).catch(this.raiseError);
}
/**
* sets the new password for a user and sends it to the server
*
* @param newPassword1 the new password
* @param newPassword2 confirmation of the new password
* @param oldPassword the old password
*/
private userNewPassword(newPassword1: string, newPassword2: string, oldPassword: string): void {
if (newPassword1 !== newPassword2) {
this.raiseError(this.translate.instant('Passwords do not match'));
}
this.repo.setNewPassword(oldPassword, newPassword1).catch(this.raiseError);
}
/** /**
* clicking Shift and Enter will save automatically * clicking Shift and Enter will save automatically
* *
@ -216,49 +176,15 @@ export class PasswordComponent extends BaseViewComponent implements OnInit {
* Takes generated password and puts it into admin PW field * Takes generated password and puts it into admin PW field
* and displays it * and displays it
*/ */
public admin_generatePassword(): void { public generatePassword(): void {
const randomPassword = this.repo.getRandomPassword();
this.adminPasswordForm.patchValue({ this.adminPasswordForm.patchValue({
admin_newPassword: this.repo.getRandomPassword() newPassword: randomPassword
}); });
this.admin_hidePassword(false);
}
/**
* Takes generated password and puts it into user PW fields
* and displays them
*/
public user_generatePassword(): void {
const newPW = this.repo.getRandomPassword();
this.userPasswordForm.patchValue({ this.userPasswordForm.patchValue({
user_newPassword1: newPW, newPassword1: randomPassword,
user_newPassword2: newPW newPassword2: randomPassword
}); });
this.user_hidePassword(false); this.hidePassword = false;
}
/**
* Helper function to hide or display the pw in cleartext for admin form
*
* @param hide optional - states if it should be shown or not
*/
public admin_hidePassword(hide?: boolean): void {
if (hide !== null) {
this.hide_admin_newPassword = hide;
} else {
this.hide_admin_newPassword = !this.hide_admin_newPassword;
}
}
/**
* Helper function to hide or display new pw in clearext for user form
*
* @param hide optional - states if it should be shown or not
*/
public user_hidePassword(hide?: boolean): void {
if (hide !== null) {
this.hide_user_password = hide;
} else {
this.hide_user_password = !this.hide_user_password;
}
} }
} }

View File

@ -273,7 +273,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
public async resetPasswordsSelected(): Promise<void> { public async resetPasswordsSelected(): Promise<void> {
for (const user of this.selectedRows) { for (const user of this.selectedRows) {
const password = this.repo.getRandomPassword(); const password = this.repo.getRandomPassword();
this.repo.resetPassword(user, password); this.repo.resetPassword(user, password, true);
} }
} }

View File

@ -398,6 +398,11 @@ button.mat-menu-item.selected {
.spacer-bottom-20 { .spacer-bottom-20 {
margin-bottom: 20px !important; margin-bottom: 20px !important;
} }
.spacer-left-10 {
margin-left: 10px;
}
.button24 { .button24 {
background-color: white; background-color: white;
width: 24px !important; width: 24px !important;

View File

@ -141,18 +141,29 @@ class UserViewSet(ModelViewSet):
def reset_password(self, request, pk=None): def reset_password(self, request, pk=None):
""" """
View to reset the password using the requested password. View to reset the password using the requested password.
If update_defualt_password=True is given, the new password will also be set
as the default_password.
""" """
user = self.get_object() user = self.get_object()
if isinstance(request.data.get("password"), str): password = request.data.get("password")
try: if not isinstance(password, str):
validate_password(request.data.get("password"), user=request.user) raise ValidationError({"detail": "Password has to be a string."})
except DjangoValidationError as errors:
raise ValidationError({"detail": " ".join(errors)})
user.set_password(request.data.get("password"))
user.save()
return Response({"detail": "Password successfully reset."})
raise ValidationError({"detail": "Password has to be a string."}) update_default_password = request.data.get("update_default_password", False)
if not isinstance(update_default_password, bool):
raise ValidationError(
{"detail": "update_default_password has to be a boolean."}
)
try:
validate_password(password, user=request.user)
except DjangoValidationError as errors:
raise ValidationError({"detail": " ".join(errors)})
user.set_password(password)
if update_default_password:
user.default_password = password
user.save()
return Response({"detail": "Password successfully reset."})
@list_route(methods=["post"]) @list_route(methods=["post"])
@transaction.atomic @transaction.atomic