From 64b3d1323574cbfeb068d81befb3f1819df838e3 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 14:42:50 +0000 Subject: [PATCH 01/13] DON-752: Create blank login component Purely the result of running `ng generate component --standalone login` --- src/app/login/login.component.css | 0 src/app/login/login.component.cy.ts | 7 +++++++ src/app/login/login.component.html | 1 + src/app/login/login.component.ts | 13 +++++++++++++ 4 files changed, 21 insertions(+) create mode 100644 src/app/login/login.component.css create mode 100644 src/app/login/login.component.cy.ts create mode 100644 src/app/login/login.component.html create mode 100644 src/app/login/login.component.ts diff --git a/src/app/login/login.component.css b/src/app/login/login.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/login/login.component.cy.ts b/src/app/login/login.component.cy.ts new file mode 100644 index 000000000..e03fd8eab --- /dev/null +++ b/src/app/login/login.component.cy.ts @@ -0,0 +1,7 @@ +import { LoginComponent } from './login.component' + +describe('LoginComponent', () => { + it('should mount', () => { + cy.mount(LoginComponent) + }) +}) \ No newline at end of file diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html new file mode 100644 index 000000000..147cfc4f8 --- /dev/null +++ b/src/app/login/login.component.html @@ -0,0 +1 @@ +

login works!

diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts new file mode 100644 index 000000000..21661fe91 --- /dev/null +++ b/src/app/login/login.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule], + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] +}) +export class LoginComponent { + +} From 4aa02823fd9aa61404050a3e3bcf7e16a5e0e893 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 14:48:41 +0000 Subject: [PATCH 02/13] DON-752: Route /login to login page if not in production --- src/app/app-routing.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/app/app-routing.ts b/src/app/app-routing.ts index 47543741d..6ed0302ef 100644 --- a/src/app/app-routing.ts +++ b/src/app/app-routing.ts @@ -4,6 +4,8 @@ import { CampaignListResolver } from './campaign-list.resolver'; import { CampaignResolver } from './campaign.resolver'; import { CharityCampaignsResolver } from './charity-campaigns.resolver'; import {campaignStatsResolver} from "./campaign-stats-resolver"; +import {LoginComponent} from "./login/login.component"; +import {environment} from "../environments/environment"; const routes: Routes = [ { @@ -122,6 +124,12 @@ const routes: Routes = [ loadChildren: () => import('./my-account/my-account.module') .then(c => c.MyAccountModule), }, + { + path: 'my-account', + pathMatch: 'full', + loadChildren: () => import('./my-account/my-account.module') + .then(c => c.MyAccountModule), + }, // This is effectively our 404 handler because we support any string as meta-campaign // slug. So check `CampaignResolver` for adjusting what happens if the slug doesn't // match a campaign. @@ -136,4 +144,14 @@ const routes: Routes = [ }, ]; +if (environment.environmentId !== 'production') { + routes.unshift( + { + path: 'login', + pathMatch: 'full', + component: LoginComponent, + }, + ); +} + export {routes}; From f8b7f200b6a83586c1bb5765558373898b1d5a9e Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 15:10:57 +0000 Subject: [PATCH 03/13] DON-752: Get login page basically working by copying code from login modal There may be scope to tidy up by removing some duplication, or it might be that the code needs to be tweaked a bit for the different context and so two separate copies makes more sense. --- src/app/login/login.component.html | 89 +++++++++++++++++++++++- src/app/login/login.component.ts | 106 ++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 3 deletions(-) diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 147cfc4f8..8be83324a 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -1 +1,88 @@ -

login works!

+
+ +
diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 21661fe91..187f0822e 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -1,13 +1,115 @@ -import { Component } from '@angular/core'; +import {Component, 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"; @Component({ selector: 'app-login', standalone: true, - imports: [CommonModule], + imports: [CommonModule, ComponentsModule, MatButtonModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatProgressSpinnerModule, ReactiveFormsModule, RecaptchaModule], templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent { + @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; + + constructor( + private formBuilder: FormBuilder, + private identityService: IdentityService, + ) { + } + + ngOnInit() { + this.loginForm = this.formBuilder.group({ + emailAddress: [null, [ + Validators.required, + Validators.pattern(EMAIL_REGEXP), + ]], + password: [null, [ + Validators.required, + Validators.minLength(10), + ]], + }); + + this.resetPasswordForm = this.formBuilder.group({ + emailAddress: [null, [ + Validators.required, + Validators.pattern(EMAIL_REGEXP), + ]], + }); + } + + login(): void { + this.loggingIn = true; + 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((response: { id: string, jwt: string }) => { + this.identityService.saveJWT(response.id, response.jwt); + this.loggingIn = false; + }, (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, + }); + } + } } From d5296532a7a45af22c86bbd6a2b2ffb8f1689b46 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 15:18:35 +0000 Subject: [PATCH 04/13] DON-752: Refactor, remove deprecated usage of Observable.subscribe --- src/app/login/login.component.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 187f0822e..b2bf8e4bc 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -94,15 +94,17 @@ export class LoginComponent { raw_password: this.loginForm.value.password, }; - this.identityService.login(credentials).subscribe((response: { id: string, jwt: string }) => { - this.identityService.saveJWT(response.id, response.jwt); - this.loggingIn = false; - }, (error) => { - this.captcha.reset(); - const errorDescription = error.error.error.description; - this.loginError = errorDescription || error.message || 'Unknown error'; - this.loggingIn = false; - }); + this.identityService.login(credentials).subscribe({ + next: (response: { id: string, jwt: string }) => { + this.identityService.saveJWT(response.id, response.jwt); + this.loggingIn = false; + }, + 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) { From 789e62f10f43fa7a6f116510932be8db4e085ea0 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 15:34:27 +0000 Subject: [PATCH 05/13] DON-752: Navigate to home and display message on successful login (I'd like the message not to be red though) --- src/app/login/login.component.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index b2bf8e4bc..fcb2f3fef 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -12,11 +12,17 @@ import {Credentials} from "../credentials.model"; import {IdentityService} from "../identity.service"; import {environment} from "../../environments/environment"; import {EMAIL_REGEXP} from "../validators/patterns"; +import {Router} from "@angular/router"; +import {MatSnackBar, MatSnackBarModule} from "@angular/material/snack-bar"; + +// fred@biggive.org.uk +// Ye0uluThYe0uluTh + @Component({ selector: 'app-login', standalone: true, - imports: [CommonModule, ComponentsModule, MatButtonModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatProgressSpinnerModule, ReactiveFormsModule, RecaptchaModule], + imports: [CommonModule, ComponentsModule, MatButtonModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatProgressSpinnerModule, ReactiveFormsModule, RecaptchaModule, MatSnackBarModule], templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) @@ -35,6 +41,8 @@ export class LoginComponent { constructor( private formBuilder: FormBuilder, private identityService: IdentityService, + private router: Router, + private snackBar: MatSnackBar, ) { } @@ -97,7 +105,14 @@ export class LoginComponent { this.identityService.login(credentials).subscribe({ next: (response: { id: string, jwt: string }) => { this.identityService.saveJWT(response.id, response.jwt); - this.loggingIn = false; + this.snackBar.open( + "You are now logged in", + undefined, + { + duration: 3_000, + panelClass: 'snack-bar', + }); + this.router.navigate(['/']); }, error: (error) => { this.captcha.reset(); From f0383836e8cfc2b0e26e55d1d6c8419ae0b69331 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 15:54:47 +0000 Subject: [PATCH 06/13] DON-752: Redirect from /login to /home if already logged in --- src/app/app-routing.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/app-routing.ts b/src/app/app-routing.ts index 6ed0302ef..7ee7b40ac 100644 --- a/src/app/app-routing.ts +++ b/src/app/app-routing.ts @@ -1,4 +1,4 @@ -import { Routes } from '@angular/router'; +import {Router, Routes} from '@angular/router'; import { CampaignListResolver } from './campaign-list.resolver'; import { CampaignResolver } from './campaign.resolver'; @@ -6,6 +6,20 @@ import { CharityCampaignsResolver } from './charity-campaigns.resolver'; import {campaignStatsResolver} from "./campaign-stats-resolver"; import {LoginComponent} from "./login/login.component"; import {environment} from "../environments/environment"; +import {inject} from "@angular/core"; +import {IdentityService} from "./identity.service"; + +const redirectToHomeIfLoggedIn = () => { + const identityService = inject(IdentityService); + const router = inject(Router); + + const isLoggedIn = identityService.probablyHaveLoggedInPerson(); + if (! isLoggedIn) { + return true; + } else { + return router.parseUrl('/'); + } +}; const routes: Routes = [ { @@ -150,6 +164,9 @@ if (environment.environmentId !== 'production') { path: 'login', pathMatch: 'full', component: LoginComponent, + canActivate: [ + redirectToHomeIfLoggedIn, + ], }, ); } From 6774ecf5e31139f17da64654908097a167a651fd Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 16:11:44 +0000 Subject: [PATCH 07/13] DON-752 Remove OK button on forgot password page (makes sense in the modal, doesn't make sense in the full page version) --- src/app/login/login.component.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 8be83324a..094158d97 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -42,9 +42,6 @@

Reset Password

Please check your email and click on the password reset link received.

Sorry, we encountered a problem. Please contact us for help.

- - - From c6dc3d06669dd5e80c2b110c9db4b0d0e423cb12 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 16:28:19 +0000 Subject: [PATCH 08/13] DON-752: Remove Cancel buttuns from login page: Cancel was copied from modal, doesn't make sense in page context --- src/app/login/login.component.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 094158d97..11c1fca74 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -34,7 +34,6 @@

Reset Password

(click)="resetPasswordClicked()" color="primary" >Reset Password - @@ -75,7 +74,6 @@

Log in

(click)="login()" color="primary" >Log in - From d9c0d664d50f7b81425ef61d5ddf80d6af557299 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 16:44:38 +0000 Subject: [PATCH 09/13] DON-752: Full page reload to reload menus on donor login --- src/app/login/login.component.html | 2 +- src/app/login/login.component.ts | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 11c1fca74..c33f1160a 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -45,7 +45,7 @@

Reset Password

-

Log in

+

Donor Log in

diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index fcb2f3fef..d18262e97 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -15,10 +15,6 @@ import {EMAIL_REGEXP} from "../validators/patterns"; import {Router} from "@angular/router"; import {MatSnackBar, MatSnackBarModule} from "@angular/material/snack-bar"; -// fred@biggive.org.uk -// Ye0uluThYe0uluTh - - @Component({ selector: 'app-login', standalone: true, @@ -105,14 +101,11 @@ export class LoginComponent { this.identityService.login(credentials).subscribe({ next: (response: { id: string, jwt: string }) => { this.identityService.saveJWT(response.id, response.jwt); - this.snackBar.open( - "You are now logged in", - undefined, - { - duration: 3_000, - panelClass: 'snack-bar', - }); - this.router.navigate(['/']); + // 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 = '/'; }, error: (error) => { this.captcha.reset(); From b761e7652e044ddfb4bdc27c90fd652f605c90e3 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 17:12:06 +0000 Subject: [PATCH 10/13] DON-752: Allow redirecting to any specified URL within the donate frontend on login This can be used to make the login page work as a gate in front of any other page that will require login to see --- src/app/login/login.component.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index d18262e97..45468eb3e 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -1,4 +1,4 @@ -import {Component, ViewChild} from '@angular/core'; +import {Component, OnInit, ViewChild} from '@angular/core'; import { CommonModule } from '@angular/common'; import {ComponentsModule} from "@biggive/components-angular"; import {MatButtonModule} from "@angular/material/button"; @@ -12,7 +12,7 @@ import {Credentials} from "../credentials.model"; import {IdentityService} from "../identity.service"; import {environment} from "../../environments/environment"; import {EMAIL_REGEXP} from "../validators/patterns"; -import {Router} from "@angular/router"; +import {ActivatedRoute, Router} from "@angular/router"; import {MatSnackBar, MatSnackBarModule} from "@angular/material/snack-bar"; @Component({ @@ -22,7 +22,7 @@ import {MatSnackBar, MatSnackBarModule} from "@angular/material/snack-bar"; templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) -export class LoginComponent { +export class LoginComponent implements OnInit{ @ViewChild('captcha') captcha: RecaptchaComponent; protected forgotPassword = false; protected loggingIn = false; @@ -32,13 +32,12 @@ export class LoginComponent { 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 router: Router, - private snackBar: MatSnackBar, + private activatedRoute: ActivatedRoute, ) { } @@ -60,6 +59,14 @@ export class LoginComponent { 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 { @@ -100,12 +107,13 @@ export class LoginComponent { 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 = '/'; + window.location.href = this.targetUrl.href; }, error: (error) => { this.captcha.reset(); From 2cbbd19ab7767f5e3842c4b1921d46ea6537bc62 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 17:15:17 +0000 Subject: [PATCH 11/13] DON-752: Add todo comment --- src/app/login/login.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 45468eb3e..0068a84c9 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -70,7 +70,7 @@ export class LoginComponent implements OnInit{ } login(): void { - this.loggingIn = true; + this.loggingIn = true; // todo - add visible indication of this to page, e.g. spinner and removal of button this.captcha.reset(); this.captcha.execute(); } From dfa0e4601aca1159f8b25b77070a61097cfb479e Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 17:30:04 +0000 Subject: [PATCH 12/13] Delete duplicate route --- src/app/app-routing.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/app-routing.ts b/src/app/app-routing.ts index 7ee7b40ac..131d96272 100644 --- a/src/app/app-routing.ts +++ b/src/app/app-routing.ts @@ -138,12 +138,6 @@ const routes: Routes = [ loadChildren: () => import('./my-account/my-account.module') .then(c => c.MyAccountModule), }, - { - path: 'my-account', - pathMatch: 'full', - loadChildren: () => import('./my-account/my-account.module') - .then(c => c.MyAccountModule), - }, // This is effectively our 404 handler because we support any string as meta-campaign // slug. So check `CampaignResolver` for adjusting what happens if the slug doesn't // match a campaign. From 1f01376a3696a2fe3f73f588f89b552c47774660 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Mon, 27 Nov 2023 17:44:03 +0000 Subject: [PATCH 13/13] DON-752: Fix linting errors --- src/app/login/login.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 0068a84c9..e58077c2a 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -12,8 +12,8 @@ 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"; +import {ActivatedRoute} from "@angular/router"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; @Component({ selector: 'app-login',