Client: password reset view and reset password confirm view

This commit is contained in:
FinnStutzenstein 2018-10-16 08:08:59 +02:00 committed by Norman Jäckel
parent e03d715602
commit 621b0471f2
21 changed files with 355 additions and 24 deletions

View File

@ -1,10 +1,12 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './site/login/components/login-wrapper/login.component'; import { LoginWrapperComponent } from './site/login/components/login-wrapper/login-wrapper.component';
import { LoginMaskComponent } from './site/login/components/login-mask/login-mask.component'; import { LoginMaskComponent } from './site/login/components/login-mask/login-mask.component';
import { LoginLegalNoticeComponent } from './site/login/components/login-legal-notice/login-legal-notice.component'; import { LoginLegalNoticeComponent } from './site/login/components/login-legal-notice/login-legal-notice.component';
import { LoginPrivacyPolicyComponent } from './site/login/components/login-privacy-policy/login-privacy-policy.component'; import { LoginPrivacyPolicyComponent } from './site/login/components/login-privacy-policy/login-privacy-policy.component';
import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component';
import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component';
/** /**
* Global app routing * Global app routing
@ -12,9 +14,11 @@ import { LoginPrivacyPolicyComponent } from './site/login/components/login-priva
const routes: Routes = [ const routes: Routes = [
{ {
path: 'login', path: 'login',
component: LoginComponent, component: LoginWrapperComponent,
children: [ children: [
{ path: '', component: LoginMaskComponent }, { path: '', component: LoginMaskComponent },
{ path: 'reset-password', component: ResetPasswordComponent },
{ path: 'reset-password-confirm', component: ResetPasswordConfirmComponent },
{ path: 'legalnotice', component: LoginLegalNoticeComponent }, { path: 'legalnotice', component: LoginLegalNoticeComponent },
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent } { path: 'privacypolicy', component: LoginPrivacyPolicyComponent }
] ]

View File

@ -0,0 +1,30 @@
h3 {
margin-top: 0;
font-weight: normal;
}
mat-form-field {
width: 100%;
}
.submit-button {
margin-top: 30px;
width: 60%;
margin-left: auto;
margin-right: auto;
}
.back-button {
width: 40%;
}
.form-wrapper {
padding-left: 30px;
padding-right: 30px;
}
form {
padding-top: 50px;
margin: 0 auto;
max-width: 400px;
}

View File

@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'os-login-legal-notice', selector: 'os-login-legal-notice',
templateUrl: './login-legal-notice.component.html', templateUrl: './login-legal-notice.component.html',
styleUrls: ['./login-legal-notice.component.scss', '../../assets/login-info-pages.scss'] styleUrls: ['../../assets/login-info-pages.scss']
}) })
export class LoginLegalNoticeComponent implements OnInit { export class LoginLegalNoticeComponent implements OnInit {
/** /**

View File

@ -28,7 +28,7 @@ mat-form-field {
} }
.login-form { .login-form {
padding-top: 60px; padding-top: 50px;
margin: 0 auto; margin: 0 auto;
max-width: 400px; max-width: 400px;
} }

View File

@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { BaseComponent } from 'app/base.component'; import { BaseComponent } from 'app/base.component';
import { AuthService } from 'app/core/services/auth.service'; import { AuthService } from 'app/core/services/auth.service';
@ -72,6 +72,7 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
private authService: AuthService, private authService: AuthService,
private operator: OperatorService, private operator: OperatorService,
private router: Router, private router: Router,
private route: ActivatedRoute,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private http: HttpService, private http: HttpService,
private matSnackBar: MatSnackBar, private matSnackBar: MatSnackBar,
@ -150,10 +151,10 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
} }
/** /**
* TODO, should open an edit view for the users password. * Go to the reset password view
*/ */
public resetPassword(): void { public resetPassword(): void {
console.log('TODO'); this.router.navigate(['./reset-password'], { relativeTo: this.route });
} }
/** /**

View File

@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'os-login-privacy-policy', selector: 'os-login-privacy-policy',
templateUrl: './login-privacy-policy.component.html', templateUrl: './login-privacy-policy.component.html',
styleUrls: ['./login-privacy-policy.component.scss', '../../assets/login-info-pages.scss'] styleUrls: ['../../assets/login-info-pages.scss']
}) })
export class LoginPrivacyPolicyComponent implements OnInit { export class LoginPrivacyPolicyComponent implements OnInit {
/** /**

View File

@ -1,11 +1,11 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component'; import { LoginWrapperComponent } from './login-wrapper.component';
import { E2EImportsModule } from '../../../../../e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
describe('LoginComponent', () => { describe('LoginComponent', () => {
let component: LoginComponent; let component: LoginWrapperComponent;
let fixture: ComponentFixture<LoginComponent>; let fixture: ComponentFixture<LoginWrapperComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -14,7 +14,7 @@ describe('LoginComponent', () => {
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent); fixture = TestBed.createComponent(LoginWrapperComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -7,14 +7,14 @@ import { BaseComponent } from '../../../../base.component';
/** /**
* Login component. * Login component.
* *
* Serves as container for the login mask, legal notice and privacy policy * Serves as container for the login mask, reset password (confirm) form, legal notice and privacy policy
*/ */
@Component({ @Component({
selector: 'os-login', selector: 'os-login-wrapper',
templateUrl: './login.component.html', templateUrl: './login-wrapper.component.html',
styleUrls: ['./login.component.scss'] styleUrls: ['./login-wrapper.component.scss']
}) })
export class LoginComponent extends BaseComponent implements OnInit { export class LoginWrapperComponent extends BaseComponent implements OnInit {
/** /**
* Imports the title service and the translate service * Imports the title service and the translate service
* *

View File

@ -0,0 +1,15 @@
<div class="form-wrapper">
<form [formGroup]="newPasswordForm" (ngSubmit)="submitNewPassword()">
<h3 translate>Please enter your new password</h3>
<mat-form-field>
<input matInput required placeholder="{{ 'New password' | translate }}" formControlName="password" type="password">
<mat-error *ngIf="newPasswordForm.get('password').hasError('required')" translate>
A password is required
</mat-error>
</mat-form-field>
<br>
<button mat-raised-button color="primary" class='submit-button' [disabled]="newPasswordForm.invalid"
type="submit" translate>Reset password</button>
<button mat-button class="back-button" routerLink="/login" translate>Back to login</button>
</form>
</div>

View File

@ -0,0 +1,46 @@
import { async, ComponentFixture, TestBed, tick, fakeAsync, flush, flushMicrotasks } from '@angular/core/testing';
import { ResetPasswordConfirmComponent } from './reset-password-confirm.component';
import { E2EImportsModule } from 'e2e-imports.module';
import { MatSnackBar } from '@angular/material';
let matSnackBarSpy: jasmine.SpyObj<MatSnackBar>;
describe('ResetPasswordConfirmComponent', () => {
let component: ResetPasswordConfirmComponent;
let fixture: ComponentFixture<ResetPasswordConfirmComponent>;
beforeEach(async(() => {
const spy = jasmine.createSpyObj('MatSnackBar', ['open']);
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [
{provide: MatSnackBar, useValue: spy}
]
}).compileComponents();
matSnackBarSpy = TestBed.get(MatSnackBar)
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResetPasswordConfirmComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
xit('should open a snackbar error', fakeAsync(() => {
// WTF? I do not kno what to do more, but the expect should run after the set timeout...
fixture.detectChanges();
tick();
fixture.detectChanges();
flush();
fixture.detectChanges();
flushMicrotasks();
fixture.detectChanges();
expect(matSnackBarSpy.open.calls.count()).toBe(1, 'mat snack bar was opened');
}));
});

View File

@ -0,0 +1,109 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { environment } from 'environments/environment';
import { ActivatedRoute, Router } from '@angular/router';
import { MatSnackBar } from '@angular/material';
/**
* Reset password component.
*
*/
@Component({
selector: 'os-reset-password-confirm',
templateUrl: './reset-password-confirm.component.html',
styleUrls: ['../../assets/reset-password-pages.scss']
})
export class ResetPasswordConfirmComponent extends BaseComponent implements OnInit {
/**
* THis form holds one control for the new password.
*/
public newPasswordForm: FormGroup;
/**
* The user_id that should be provided in the queryparams.
*/
private user_id: string;
/**
* The token that should be provided in the queryparams.
*/
private token: string;
/**
* Constructur for the reset password confirm view. Initializes the form for the new password.
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
private http: HttpClient,
formBuilder: FormBuilder,
private activatedRoute: ActivatedRoute,
private router: Router,
private matSnackBar: MatSnackBar
) {
super(titleService, translate);
this.newPasswordForm = formBuilder.group({
password: ['', [Validators.required]]
});
}
/**
* Sets the title of the page and gets the queryparams.
*/
public ngOnInit(): void {
super.setTitle('Reset password');
this.activatedRoute.queryParams.subscribe(params => {
if (!params.user_id || !params.token) {
setTimeout(() => {
this.matSnackBar.open('');
this.matSnackBar.open(
this.translate.instant('The link is broken. Please contact your system administrator.'),
this.translate.instant('OK'),
{
duration: 0
}
);
this.router.navigate(['/']);
});
} else {
this.user_id = params.user_id;
this.token = params.token;
}
});
}
/**
* Submit the new password.
*/
public async submitNewPassword(): Promise<void> {
if (this.newPasswordForm.invalid) {
return;
}
try {
await this.http
.post<void>(environment.urlPrefix + '/users/reset-password-confirm/', {
user_id: this.user_id,
token: this.token,
password: this.newPasswordForm.get('password').value
})
.toPromise();
// TODO: Does we get a response for displaying?
this.matSnackBar.open(
this.translate.instant('Your password was resetted successfully!'),
this.translate.instant('OK'),
{
duration: 0
}
);
this.router.navigate(['/login']);
} catch (e) {
console.log('error', e);
}
}
}

View File

@ -0,0 +1,15 @@
<div class="form-wrapper">
<form [formGroup]="resetPasswordForm" (ngSubmit)="resetPassword()">
<h3 translate>Enter your email to send the password reset link</h3>
<mat-form-field>
<input matInput required placeholder="Email" formControlName="email" type="email">
<mat-error *ngIf="resetPasswordForm.get('email').hasError('email')" translate>
Please enter a valid email address
</mat-error>
</mat-form-field>
<br>
<button mat-raised-button color="primary" class='submit-button' [disabled]="resetPasswordForm.invalid"
type="submit" translate>Reset password</button>
<button mat-button class="back-button" routerLink="/login" translate>Back to login</button>
</form>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ResetPasswordComponent } from './reset-password.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('ResetPasswordComponent', () => {
let component: ResetPasswordComponent;
let fixture: ComponentFixture<ResetPasswordComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResetPasswordComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,78 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { environment } from 'environments/environment';
import { MatSnackBar } from '@angular/material';
import { Router } from '@angular/router';
/**
* Reset password component.
*
*/
@Component({
selector: 'os-reset-password',
templateUrl: './reset-password.component.html',
styleUrls: ['../../assets/reset-password-pages.scss']
})
export class ResetPasswordComponent extends BaseComponent implements OnInit {
/**
* THis form holds one control for the email.
*/
public resetPasswordForm: FormGroup;
/**
* Constructur for the reset password view. Initializes the form for the email.
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
private http: HttpClient,
formBuilder: FormBuilder,
private matSnackBar: MatSnackBar,
private router: Router
) {
super(titleService, translate);
this.resetPasswordForm = formBuilder.group({
email: ['', [Validators.required, Validators.email]]
});
}
/**
* sets the title of the page
*/
public ngOnInit(): void {
super.setTitle('Reset password');
}
/**
* Do the password reset.
*/
public async resetPassword(): Promise<void> {
if (this.resetPasswordForm.invalid) {
return;
}
try {
await this.http
.post<void>(environment.urlPrefix + '/users/reset-password/', {
email: this.resetPasswordForm.get('email').value
})
.toPromise();
// TODO: Does we get a response for displaying?
this.matSnackBar.open(
this.translate.instant('An email with a password reset link was send!'),
this.translate.instant('OK'),
{
duration: 0
}
);
this.router.navigate(['/login']);
} catch (e) {
console.log('error', e);
}
}
}

View File

@ -2,14 +2,23 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { LoginComponent } from './components/login-wrapper/login.component'; import { LoginWrapperComponent } from './components/login-wrapper/login-wrapper.component';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { LoginMaskComponent } from './components/login-mask/login-mask.component'; import { LoginMaskComponent } from './components/login-mask/login-mask.component';
import { LoginLegalNoticeComponent } from './components/login-legal-notice/login-legal-notice.component'; import { LoginLegalNoticeComponent } from './components/login-legal-notice/login-legal-notice.component';
import { LoginPrivacyPolicyComponent } from './components/login-privacy-policy/login-privacy-policy.component'; import { LoginPrivacyPolicyComponent } from './components/login-privacy-policy/login-privacy-policy.component';
import { ResetPasswordComponent } from './components/reset-password/reset-password.component';
import { ResetPasswordConfirmComponent } from './components/reset-password-confirm/reset-password-confirm.component';
@NgModule({ @NgModule({
imports: [CommonModule, RouterModule, SharedModule], imports: [CommonModule, RouterModule, SharedModule],
declarations: [LoginComponent, LoginMaskComponent, LoginLegalNoticeComponent, LoginPrivacyPolicyComponent] declarations: [
LoginWrapperComponent,
ResetPasswordComponent,
ResetPasswordConfirmComponent,
LoginMaskComponent,
LoginLegalNoticeComponent,
LoginPrivacyPolicyComponent
]
}) })
export class LoginModule {} export class LoginModule {}

View File

@ -64,7 +64,7 @@
</div> </div>
<div> <div>
<!-- Strcuture Level --> <!-- Strucuture Level -->
<mat-form-field class='form70 distance' *ngIf='user.structure_level || editUser && isAllowed("manage")'> <mat-form-field class='form70 distance' *ngIf='user.structure_level || editUser && isAllowed("manage")'>
<input type='text' matInput placeholder='{{"Structure Level" | translate}}' formControlName='structure_level' <input type='text' matInput placeholder='{{"Structure Level" | translate}}' formControlName='structure_level'
[value]='user.structure_level'> [value]='user.structure_level'>

View File

@ -19,7 +19,6 @@ from django.db import transaction
from django.utils.encoding import force_bytes, force_text from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.template import loader
from ..core.config import config from ..core.config import config
from ..core.signals import permission_change from ..core.signals import permission_change
@ -540,7 +539,7 @@ class PasswordResetView(APIView):
this address) with a one-use only link. this address) with a one-use only link.
""" """
http_method_names = ['post'] http_method_names = ['post']
use_https = False #TODO: Do we use https? use_https = False # TODO: Do we use https?
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """
@ -555,7 +554,7 @@ class PasswordResetView(APIView):
'site_name': site_name, 'site_name': site_name,
'protocol': 'https' if self.use_https else 'http', 'protocol': 'https' if self.use_https else 'http',
'domain': current_site.domain, 'domain': current_site.domain,
'path': '/reset-password-confirm/', 'path': '/login/reset-password-confirm/',
'user_id': urlsafe_base64_encode(force_bytes(user.pk)).decode(), 'user_id': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
'token': default_token_generator.make_token(user), 'token': default_token_generator.make_token(user),
'username': user.get_username(), 'username': user.get_username(),