diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 3ab3739b0..2a307ca82 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -1,10 +1,12 @@ import { NgModule } from '@angular/core'; 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 { 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 { 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 @@ -12,9 +14,11 @@ import { LoginPrivacyPolicyComponent } from './site/login/components/login-priva const routes: Routes = [ { path: 'login', - component: LoginComponent, + component: LoginWrapperComponent, children: [ { path: '', component: LoginMaskComponent }, + { path: 'reset-password', component: ResetPasswordComponent }, + { path: 'reset-password-confirm', component: ResetPasswordConfirmComponent }, { path: 'legalnotice', component: LoginLegalNoticeComponent }, { path: 'privacypolicy', component: LoginPrivacyPolicyComponent } ] diff --git a/client/src/app/site/login/assets/reset-password-pages.scss b/client/src/app/site/login/assets/reset-password-pages.scss new file mode 100644 index 000000000..0b154707d --- /dev/null +++ b/client/src/app/site/login/assets/reset-password-pages.scss @@ -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; +} diff --git a/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.scss b/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.ts b/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.ts index 44d9032b0..52bf24616 100644 --- a/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.ts +++ b/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.ts @@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'os-login-legal-notice', 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 { /** diff --git a/client/src/app/site/login/components/login-mask/login-mask.component.scss b/client/src/app/site/login/components/login-mask/login-mask.component.scss index 8aa5bee02..9a65e2996 100644 --- a/client/src/app/site/login/components/login-mask/login-mask.component.scss +++ b/client/src/app/site/login/components/login-mask/login-mask.component.scss @@ -28,7 +28,7 @@ mat-form-field { } .login-form { - padding-top: 60px; + padding-top: 50px; margin: 0 auto; max-width: 400px; } diff --git a/client/src/app/site/login/components/login-mask/login-mask.component.ts b/client/src/app/site/login/components/login-mask/login-mask.component.ts index d545c63d2..ca64667f0 100644 --- a/client/src/app/site/login/components/login-mask/login-mask.component.ts +++ b/client/src/app/site/login/components/login-mask/login-mask.component.ts @@ -1,5 +1,5 @@ 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 { AuthService } from 'app/core/services/auth.service'; @@ -72,6 +72,7 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr private authService: AuthService, private operator: OperatorService, private router: Router, + private route: ActivatedRoute, private formBuilder: FormBuilder, private http: HttpService, 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 { - console.log('TODO'); + this.router.navigate(['./reset-password'], { relativeTo: this.route }); } /** diff --git a/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.scss b/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.ts b/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.ts index 213c70243..e3b24213f 100644 --- a/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.ts +++ b/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.ts @@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'os-login-privacy-policy', 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 { /** diff --git a/client/src/app/site/login/components/login-wrapper/login.component.html b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html similarity index 100% rename from client/src/app/site/login/components/login-wrapper/login.component.html rename to client/src/app/site/login/components/login-wrapper/login-wrapper.component.html diff --git a/client/src/app/site/login/components/login-wrapper/login.component.scss b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.scss similarity index 100% rename from client/src/app/site/login/components/login-wrapper/login.component.scss rename to client/src/app/site/login/components/login-wrapper/login-wrapper.component.scss diff --git a/client/src/app/site/login/components/login-wrapper/login.component.spec.ts b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.spec.ts similarity index 61% rename from client/src/app/site/login/components/login-wrapper/login.component.spec.ts rename to client/src/app/site/login/components/login-wrapper/login-wrapper.component.spec.ts index bf0455456..a647e56f3 100644 --- a/client/src/app/site/login/components/login-wrapper/login.component.spec.ts +++ b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.spec.ts @@ -1,11 +1,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { LoginComponent } from './login.component'; -import { E2EImportsModule } from '../../../../../e2e-imports.module'; +import { LoginWrapperComponent } from './login-wrapper.component'; +import { E2EImportsModule } from 'e2e-imports.module'; describe('LoginComponent', () => { - let component: LoginComponent; - let fixture: ComponentFixture; + let component: LoginWrapperComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -14,7 +14,7 @@ describe('LoginComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(LoginComponent); + fixture = TestBed.createComponent(LoginWrapperComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/client/src/app/site/login/components/login-wrapper/login.component.ts b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.ts similarity index 70% rename from client/src/app/site/login/components/login-wrapper/login.component.ts rename to client/src/app/site/login/components/login-wrapper/login-wrapper.component.ts index 254a4e60f..8108a3c20 100644 --- a/client/src/app/site/login/components/login-wrapper/login.component.ts +++ b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.ts @@ -7,14 +7,14 @@ import { BaseComponent } from '../../../../base.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({ - selector: 'os-login', - templateUrl: './login.component.html', - styleUrls: ['./login.component.scss'] + selector: 'os-login-wrapper', + templateUrl: './login-wrapper.component.html', + 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 * diff --git a/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.html b/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.html new file mode 100644 index 000000000..b496b6f3b --- /dev/null +++ b/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.html @@ -0,0 +1,15 @@ +
+
+

Please enter your new password

+ + + + A password is required + + +
+ + +
+
diff --git a/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.spec.ts b/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.spec.ts new file mode 100644 index 000000000..ca00e6c07 --- /dev/null +++ b/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.spec.ts @@ -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; + +describe('ResetPasswordConfirmComponent', () => { + let component: ResetPasswordConfirmComponent; + let fixture: ComponentFixture; + + 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'); + })); +}); diff --git a/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.ts b/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.ts new file mode 100644 index 000000000..784f3d046 --- /dev/null +++ b/client/src/app/site/login/components/reset-password-confirm/reset-password-confirm.component.ts @@ -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 { + if (this.newPasswordForm.invalid) { + return; + } + + try { + await this.http + .post(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); + } + } +} diff --git a/client/src/app/site/login/components/reset-password/reset-password.component.html b/client/src/app/site/login/components/reset-password/reset-password.component.html new file mode 100644 index 000000000..a6015db63 --- /dev/null +++ b/client/src/app/site/login/components/reset-password/reset-password.component.html @@ -0,0 +1,15 @@ +
+
+

Enter your email to send the password reset link

+ + + + Please enter a valid email address + + +
+ + +
+
diff --git a/client/src/app/site/login/components/reset-password/reset-password.component.spec.ts b/client/src/app/site/login/components/reset-password/reset-password.component.spec.ts new file mode 100644 index 000000000..3ac3da139 --- /dev/null +++ b/client/src/app/site/login/components/reset-password/reset-password.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ResetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/login/components/reset-password/reset-password.component.ts b/client/src/app/site/login/components/reset-password/reset-password.component.ts new file mode 100644 index 000000000..1fb54cdef --- /dev/null +++ b/client/src/app/site/login/components/reset-password/reset-password.component.ts @@ -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 { + if (this.resetPasswordForm.invalid) { + return; + } + + try { + await this.http + .post(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); + } + } +} diff --git a/client/src/app/site/login/login.module.ts b/client/src/app/site/login/login.module.ts index 499c95e1e..a3b11eb3a 100644 --- a/client/src/app/site/login/login.module.ts +++ b/client/src/app/site/login/login.module.ts @@ -2,14 +2,23 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; 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 { LoginMaskComponent } from './components/login-mask/login-mask.component'; import { LoginLegalNoticeComponent } from './components/login-legal-notice/login-legal-notice.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({ imports: [CommonModule, RouterModule, SharedModule], - declarations: [LoginComponent, LoginMaskComponent, LoginLegalNoticeComponent, LoginPrivacyPolicyComponent] + declarations: [ + LoginWrapperComponent, + ResetPasswordComponent, + ResetPasswordConfirmComponent, + LoginMaskComponent, + LoginLegalNoticeComponent, + LoginPrivacyPolicyComponent + ] }) export class LoginModule {} diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.html b/client/src/app/site/users/components/user-detail/user-detail.component.html index 26497c4fe..ff783be2c 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.html +++ b/client/src/app/site/users/components/user-detail/user-detail.component.html @@ -64,7 +64,7 @@
- + diff --git a/openslides/users/views.py b/openslides/users/views.py index c984255e0..e41c915b8 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -19,7 +19,6 @@ from django.db import transaction from django.utils.encoding import force_bytes, force_text from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.translation import ugettext as _ -from django.template import loader from ..core.config import config from ..core.signals import permission_change @@ -540,7 +539,7 @@ class PasswordResetView(APIView): this address) with a one-use only link. """ 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): """ @@ -555,7 +554,7 @@ class PasswordResetView(APIView): 'site_name': site_name, 'protocol': 'https' if self.use_https else 'http', 'domain': current_site.domain, - 'path': '/reset-password-confirm/', + 'path': '/login/reset-password-confirm/', 'user_id': urlsafe_base64_encode(force_bytes(user.pk)).decode(), 'token': default_token_generator.make_token(user), 'username': user.get_username(),