-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DON-752: Create login page #1401
Changes from 12 commits
64b3d13
4aa0282
f8b7f20
d529653
789e62f
f038383
6774ecf
c6dc3d0
d9c0d66
b761e76
2cbbd19
dfa0e46
1f01376
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { LoginComponent } from './login.component' | ||
|
||
describe('LoginComponent', () => { | ||
it('should mount', () => { | ||
cy.mount(LoginComponent) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<main> | ||
<div class="my-account"> | ||
<biggive-page-section> | ||
|
||
<re-captcha | ||
#captcha | ||
size="invisible" | ||
errorMode="handled" | ||
(resolved)="captchaReturn($event)" | ||
(errored)="captchaError()" | ||
siteKey="{{ recaptchaIdSiteKey }}" | ||
></re-captcha> | ||
|
||
<div *ngIf="forgotPassword else logIn"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. may or may not want to move forgotPassword out to a separate page. |
||
|
||
<h2 mat-dialog-title>Reset Password</h2> | ||
|
||
<div *ngIf="!userAskedForResetLink"> | ||
<mat-dialog-content class="mat-typography"> | ||
<form [formGroup]="resetPasswordForm"> | ||
<mat-form-field class="b-w-100" color="primary"> | ||
<mat-label for="loginEmailAddress">Email address</mat-label> | ||
<input matInput type="email" id="loginEmailAddress" formControlName="emailAddress" autocapitalize="off"> | ||
</mat-form-field> | ||
<p class="b-rt-0">Please enter your email address.</p> | ||
</form> | ||
</mat-dialog-content> | ||
|
||
<mat-dialog-actions align="end"> | ||
<button | ||
id="reset-password-modal-submit" | ||
mat-raised-button | ||
[disabled]="!resetPasswordForm.valid || userAskedForResetLink" | ||
(click)="resetPasswordClicked()" | ||
color="primary" | ||
>Reset Password</button> | ||
</mat-dialog-actions> | ||
</div> | ||
|
||
<div *ngIf="userAskedForResetLink"> | ||
<mat-spinner *ngIf="resetPasswordSuccess === undefined" [diameter]="40" aria-label="Loading your details"></mat-spinner> | ||
<p *ngIf="resetPasswordSuccess">Please check your email and click on the password reset link received.</p> | ||
<p *ngIf="resetPasswordSuccess === false" class="error">Sorry, we encountered a problem. Please <a href="https://community.biggive.org/s/contact-us" target="_blank">contact us</a> for help.</p> | ||
</div> | ||
</div> | ||
|
||
<ng-template #logIn> | ||
<h2 mat-dialog-title>Donor Log in</h2> | ||
<mat-dialog-content class="mat-typography"> | ||
<form [formGroup]="loginForm"> | ||
<mat-form-field class="b-w-100" color="primary"> | ||
<mat-label for="loginEmailAddress">Email address</mat-label> | ||
<input matInput type="email" id="loginEmailAddress" formControlName="emailAddress" autocapitalize="off"> | ||
</mat-form-field> | ||
|
||
<mat-form-field class="b-w-100" color="primary"> | ||
<mat-label for="loginPassword">Password</mat-label> | ||
<input matInput type="password" id="loginPassword" formControlName="password"> | ||
</mat-form-field> | ||
|
||
<p *ngIf="loginError" class="error" aria-live="assertive"> | ||
Login error: {{ loginError }} | ||
</p> | ||
</form> | ||
</mat-dialog-content> | ||
|
||
<mat-dialog-actions style="justify-content: space-between;"> | ||
<a (click)="forgotPasswordClicked()">Forgot Password?</a> | ||
<div> | ||
<button | ||
id="login-modal-submit" | ||
mat-raised-button | ||
[disabled]="!loginForm.valid || loggingIn" | ||
(click)="login()" | ||
color="primary" | ||
>Log in</button> | ||
</div> | ||
</mat-dialog-actions> | ||
</ng-template> | ||
|
||
</biggive-page-section> | ||
</div> | ||
</main> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import {Component, OnInit, ViewChild} from '@angular/core'; | ||
import { CommonModule } from '@angular/common'; | ||
import {ComponentsModule} from "@biggive/components-angular"; | ||
import {MatButtonModule} from "@angular/material/button"; | ||
import {MatDialogModule} from "@angular/material/dialog"; | ||
import {MatFormFieldModule} from "@angular/material/form-field"; | ||
import {MatInputModule} from "@angular/material/input"; | ||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; | ||
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; | ||
import {RecaptchaComponent, RecaptchaModule} from "ng-recaptcha"; | ||
import {Credentials} from "../credentials.model"; | ||
import {IdentityService} from "../identity.service"; | ||
import {environment} from "../../environments/environment"; | ||
import {EMAIL_REGEXP} from "../validators/patterns"; | ||
import {ActivatedRoute, Router} from "@angular/router"; | ||
import {MatSnackBar, MatSnackBarModule} from "@angular/material/snack-bar"; | ||
|
||
@Component({ | ||
selector: 'app-login', | ||
standalone: true, | ||
imports: [CommonModule, ComponentsModule, MatButtonModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatProgressSpinnerModule, ReactiveFormsModule, RecaptchaModule, MatSnackBarModule], | ||
templateUrl: './login.component.html', | ||
styleUrls: ['./login.component.css'] | ||
}) | ||
export class LoginComponent implements OnInit{ | ||
@ViewChild('captcha') captcha: RecaptchaComponent; | ||
protected forgotPassword = false; | ||
protected loggingIn = false; | ||
protected loginError?: string; | ||
loginForm: FormGroup; | ||
protected userAskedForResetLink = false; | ||
protected resetPasswordForm: FormGroup; | ||
protected resetPasswordSuccess: boolean|undefined = undefined; | ||
protected recaptchaIdSiteKey = environment.recaptchaIdentitySiteKey; | ||
private targetUrl: URL = new URL(environment.donateGlobalUriPrefix); | ||
|
||
constructor( | ||
private formBuilder: FormBuilder, | ||
private identityService: IdentityService, | ||
private activatedRoute: ActivatedRoute, | ||
) { | ||
} | ||
|
||
ngOnInit() { | ||
this.loginForm = this.formBuilder.group({ | ||
emailAddress: [null, [ | ||
Validators.required, | ||
Validators.pattern(EMAIL_REGEXP), | ||
]], | ||
password: [null, [ | ||
Validators.required, | ||
Validators.minLength(10), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure it's worth having a minLength validation on the password field here. It's here because I copied this from the login modal. Min length is good when setting a new password, but I think it might be more confusing than helpful when trying to authenticate with an existing password. The only password validation rule as far as the user is concerned here is that the password has to match what's set on the account. Yes we know that that implies it will be at least 10 characters, but that feels a bit arbitrary to enforce here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you're right, I don't remember being notified about password length on any login page, but maybe it's good to check length at the form level before checking against db, as we know it can't be right and won't make unnecessary db calls? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we do that for performance reasons then we don't need to tell the user about it, as it just makes things more complicated for them - and if we do it it makes more sense to do it in the Identity server instead of in frontend. I'll leave it here for now but likely change in a later PR. |
||
]], | ||
}); | ||
|
||
this.resetPasswordForm = this.formBuilder.group({ | ||
emailAddress: [null, [ | ||
Validators.required, | ||
Validators.pattern(EMAIL_REGEXP), | ||
]], | ||
}); | ||
|
||
const redirectParam = this.activatedRoute.snapshot.queryParams.r as string|undefined; | ||
|
||
// allowed chars in URL to redirect to: a-z, A-Z, 0-9, - _ / | ||
|
||
if (redirectParam && ! redirectParam.match(/[^a-zA-Z0-9\-_\/]/)) { | ||
this.targetUrl = new URL(environment.donateGlobalUriPrefix + '/' + redirectParam); | ||
} | ||
} | ||
|
||
login(): void { | ||
this.loggingIn = true; // todo - add visible indication of this to page, e.g. spinner and removal of button | ||
this.captcha.reset(); | ||
this.captcha.execute(); | ||
} | ||
|
||
forgotPasswordClicked(): void { | ||
this.forgotPassword = true; | ||
} | ||
|
||
resetPasswordClicked(): void { | ||
this.userAskedForResetLink = true; | ||
this.captcha.reset(); | ||
this.captcha.execute(); | ||
} | ||
|
||
captchaError() { | ||
this.loginError = 'Captcha error – please try again'; | ||
this.loggingIn = false; | ||
} | ||
|
||
captchaReturn(captchaResponse: string): void { | ||
if (captchaResponse === null) { | ||
// We had a code but now don't, e.g. after expiry at 1 minute. In this case | ||
// the trigger wasn't a login click so do nothing. A repeat login attempt will | ||
// re-execute the captcha in `login()`. | ||
return; | ||
} | ||
|
||
if (this.loggingIn) { | ||
const credentials: Credentials = { | ||
captcha_code: captchaResponse, | ||
email_address: this.loginForm.value.emailAddress, | ||
raw_password: this.loginForm.value.password, | ||
}; | ||
|
||
this.identityService.login(credentials).subscribe({ | ||
next: (response: { id: string, jwt: string }) => { | ||
// todo - see if we can make login do `saveJWT` internally and delete it here? | ||
this.identityService.saveJWT(response.id, response.jwt); | ||
// assign window.location rather than the more angular-proper way of | ||
// this.router.navigateByUrl('/') because we need to force the main menu to be updated | ||
// to show that we're now logged in. | ||
|
||
window.location.href = this.targetUrl.href; | ||
}, | ||
error: (error) => { | ||
this.captcha.reset(); | ||
const errorDescription = error.error.error.description; | ||
this.loginError = errorDescription || error.message || 'Unknown error'; | ||
this.loggingIn = false; | ||
}}); | ||
} | ||
|
||
else if (this.userAskedForResetLink) { | ||
this.identityService.getResetPasswordToken(this.resetPasswordForm.value.emailAddress, captchaResponse).subscribe({ | ||
next: _ => this.resetPasswordSuccess = true, | ||
error: _ => this.resetPasswordSuccess = false, | ||
}); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo - inject the Activated route and redirect the same way that the login component does on successful login, i.e. not always to the home page.