From 2bca44ebe5ce51bf1adbea01293664b4f681e0e1 Mon Sep 17 00:00:00 2001 From: Charles Showalter Date: Mon, 23 May 2022 15:30:07 -0700 Subject: [PATCH] Login, Registration Forms Added. --- client/src/app/account/account.service.ts | 29 +++++++++-- .../app/account/login/login.component.html | 15 ++---- .../src/app/account/login/login.component.ts | 18 ++++++- .../account/register/register.component.html | 18 ++++++- .../account/register/register.component.ts | 48 ++++++++++++++++++- client/src/app/app-routing.module.ts | 3 +- client/src/app/app.component.ts | 17 ++++++- client/src/app/core/core.module.ts | 2 + client/src/app/core/guards/auth.guard.ts | 24 ++++++++++ .../core/interceptors/loading.interceptor.ts | 5 +- .../app/core/nav-bar/nav-bar.component.html | 25 +++++++++- .../src/app/core/nav-bar/nav-bar.component.ts | 12 ++++- .../text-inputs/text-inputs.component.html | 21 ++++++++ .../text-inputs/text-inputs.component.scss | 7 +++ .../text-inputs/text-inputs.component.ts | 40 ++++++++++++++++ client/src/app/shared/shared.module.ts | 11 +++-- 16 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 client/src/app/core/guards/auth.guard.ts create mode 100644 client/src/app/shared/components/text-inputs/text-inputs.component.html create mode 100644 client/src/app/shared/components/text-inputs/text-inputs.component.scss create mode 100644 client/src/app/shared/components/text-inputs/text-inputs.component.ts diff --git a/client/src/app/account/account.service.ts b/client/src/app/account/account.service.ts index a5ebcc6..fb6f06a 100644 --- a/client/src/app/account/account.service.ts +++ b/client/src/app/account/account.service.ts @@ -1,20 +1,38 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; -import { BehaviorSubject, map } from 'rxjs'; +import { map, of, ReplaySubject } from 'rxjs'; import { environment } from 'src/environments/environment'; import { IUser } from '../shared/models/user'; @Injectable({ providedIn: 'root' }) -export class AccoutService { +export class AccountService { baseUrl = environment.apiUrl; - private currentUserSource = new BehaviorSubject(null); + private currentUserSource = new ReplaySubject(1); currentUser$ = this.currentUserSource.asObservable(); constructor(private http: HttpClient, private router: Router) { } + loadCurrentUser(token: string){ + if(token === null){ + this.currentUserSource.next(null); + return of(null); + } + + let headers = new HttpHeaders(); + headers = headers.set('Authorization', `Bearer ${token}`); + return this.http.get(this.baseUrl + 'account', {headers}).pipe( + map((user: IUser) => { + if(user){ + localStorage.setItem('token', user.token); + this.currentUserSource.next(user); + } + }) + ) + } + login(values: any){ return this.http.post(this.baseUrl + 'account/login', values).pipe( map((user: IUser) =>{ @@ -31,6 +49,7 @@ export class AccoutService { map((user:IUser) => { if(user){ localStorage.setItem('token', user.token); + this.currentUserSource.next(user); } }) ); @@ -43,6 +62,6 @@ export class AccoutService { } checkEmailExists(email: string){ - return this.http.get(this.baseUrl + '/account/emailexists?email=' + email); + return this.http.get(this.baseUrl + 'account/emailexists?email=' + email); } } diff --git a/client/src/app/account/login/login.component.html b/client/src/app/account/login/login.component.html index 4af8ac5..b6c829f 100644 --- a/client/src/app/account/login/login.component.html +++ b/client/src/app/account/login/login.component.html @@ -1,17 +1,10 @@
-
+

Login

- -
- - -
-
- - -
- + + +
diff --git a/client/src/app/account/login/login.component.ts b/client/src/app/account/login/login.component.ts index c23d266..39358a3 100644 --- a/client/src/app/account/login/login.component.ts +++ b/client/src/app/account/login/login.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AccountService } from '../account.service'; @Component({ selector: 'app-login', @@ -8,18 +10,30 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; }) export class LoginComponent implements OnInit { loginForm: FormGroup; +returnUrl: string; - constructor() { } + constructor(private accountService: AccountService, private router: Router, private activatedRoute: ActivatedRoute) { } ngOnInit(): void { + this.returnUrl = this.activatedRoute.snapshot.queryParams.returnUrl || '/shop'; + this.createLoginForm(); } createLoginForm(){ this.loginForm = new FormGroup({ - email: new FormControl('', Validators.required), + email: new FormControl('', [Validators.required, Validators.pattern('^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}')]), password: new FormControl('', Validators.required) }); } + onSubmit(){ + this.accountService.login(this.loginForm.value).subscribe({ + next: () => { this.router.navigateByUrl(this.returnUrl); }, + error: (e: any) => { console.log(e) }, + complete: () => { console.log('complete')} + + }); + } + } diff --git a/client/src/app/account/register/register.component.html b/client/src/app/account/register/register.component.html index 6b0ba2e..af4e384 100644 --- a/client/src/app/account/register/register.component.html +++ b/client/src/app/account/register/register.component.html @@ -1 +1,17 @@ -

register works!

+
+
+
+

Register

+ + + +
    +
  • + {{error}} +
  • +
+ +
+
+
+ diff --git a/client/src/app/account/register/register.component.ts b/client/src/app/account/register/register.component.ts index 8f62eda..2d67fc9 100644 --- a/client/src/app/account/register/register.component.ts +++ b/client/src/app/account/register/register.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { AsyncValidatorFn, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { map, of, switchMap, timer } from 'rxjs'; +import { AccountService } from '../account.service'; @Component({ selector: 'app-register', @@ -6,10 +10,52 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./register.component.scss'] }) export class RegisterComponent implements OnInit { + registerForm: FormGroup; + errors: string[]; - constructor() { } + constructor(private fb: FormBuilder, private accountService: AccountService, private router: Router) { } ngOnInit(): void { + this.createRegisterForm(); + } + + createRegisterForm(){ + this.registerForm = this.fb.group({ + displayName: [null, [Validators.required]], + email: [null, + [Validators.required, Validators.pattern('^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}')], + [this.validateEmailNotTaken()] + ], + password: [null, [Validators.required],] + }); + } + + onSubmit(){ + this.accountService.register(this.registerForm.value).subscribe({ + next: (response) => { this.router.navigateByUrl('/shop')}, + error: (e: any) => { + console.log(e); + this.errors = e.errors; + }, + complete: () => { console.log('completed') } + }); + } + + validateEmailNotTaken(): AsyncValidatorFn { + return control => { + return timer(500).pipe( + switchMap(() => { + if(!control.value){ + return of(null); + } + return this.accountService.checkEmailExists(control.value).pipe( + map(res => { + return res ? {emailExists: true} : null; + }) + ); + }) + ); + } } } diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 7fc0121..39ac0c4 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from './core/guards/auth.guard'; import { NotFoundComponent } from './core/not-found/not-found.component'; import { ServerErrorComponent } from './core/server-error/server-error.component'; import { TestErrorComponent } from './core/test-error/test-error.component'; @@ -12,7 +13,7 @@ const routes: Routes = [ {path: 'not-found', component: NotFoundComponent, data: {breadcrumb: 'Not Found'}}, {path: 'shop', loadChildren: ()=> import('./shop/shop.module').then(mod => mod.ShopModule), data: {breadcrumb: 'Shop'}}, {path: 'basket', loadChildren: ()=> import('./basket/basket.module').then(mod => mod.BasketModule), data: {breadcrumb: 'Shopping Cart'}}, - {path: 'checkout', loadChildren: ()=> import('./checkout/checkout.module').then(mod => mod.CheckoutModule), data: {breadcrumb: 'Checkout'}}, + {path: 'checkout', canActivate: [AuthGuard], loadChildren: ()=> import('./checkout/checkout.module').then(mod => mod.CheckoutModule), data: {breadcrumb: 'Checkout'}}, {path: 'account', loadChildren: ()=> import('./account/account.module').then(mod => mod.AccountModule), data: {breadcrumb: {skip: true}}}, {path: '**', redirectTo: 'not-found', pathMatch: 'full'} ]; diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index b5d4843..9e684ac 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { BasketService } from './basket/basket.service'; +import { AccountService } from './account/account.service'; @Component({ selector: 'app-root', @@ -9,9 +10,23 @@ import { BasketService } from './basket/basket.service'; export class AppComponent implements OnInit { title = 'SkiNet'; - constructor(private basketService: BasketService) {} + constructor(private basketService: BasketService, private accountService: AccountService) {} ngOnInit(): void { + this.loadBasket(); + this.loadCurrentUser(); + } + + loadCurrentUser(){ + const token = localStorage.getItem('token'); + this.accountService.loadCurrentUser(token).subscribe({ + next: () => { console.log('loader user') }, + error: (e: any) => { console.log(e) }, + complete: () => { console.log('completed') } + }); + } + + loadBasket(){ const basketId = localStorage.getItem('basket_id'); if(basketId){ this.basketService.getBasket(basketId).subscribe({ diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index cd64fa7..1f52c40 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts @@ -8,6 +8,7 @@ import { ServerErrorComponent } from './server-error/server-error.component'; import { ToastrModule } from 'ngx-toastr'; import { SectionHeaderComponent } from './section-header/section-header.component'; import { BreadcrumbModule } from 'xng-breadcrumb'; +import { SharedModule } from '../shared/shared.module'; @@ -17,6 +18,7 @@ import { BreadcrumbModule } from 'xng-breadcrumb'; CommonModule, RouterModule, BreadcrumbModule, + SharedModule, ToastrModule.forRoot({ positionClass: 'toast-bottom-right', preventDuplicates: true diff --git a/client/src/app/core/guards/auth.guard.ts b/client/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000..e7ca5c5 --- /dev/null +++ b/client/src/app/core/guards/auth.guard.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { map, Observable } from 'rxjs'; +import { AccountService } from 'src/app/account/account.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + constructor(private accountService: AccountService, private router: Router) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable { + return this.accountService.currentUser$.pipe( + map(auth => { + if(auth){ + return true; + } + this.router.navigate(['account/login'], {queryParams: {returnUrl: state.url}}); + }) + ); + } +} diff --git a/client/src/app/core/interceptors/loading.interceptor.ts b/client/src/app/core/interceptors/loading.interceptor.ts index 445fe2a..a9d96b2 100644 --- a/client/src/app/core/interceptors/loading.interceptor.ts +++ b/client/src/app/core/interceptors/loading.interceptor.ts @@ -8,7 +8,10 @@ export class LoadingInterceptor implements HttpInterceptor { constructor(private busyService: BusyService) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { - this.busyService.busy(); + if(!req.url.includes('emailexists')){ + this.busyService.busy(); + } + return next.handle(req).pipe( delay(1000), finalize(()=> { diff --git a/client/src/app/core/nav-bar/nav-bar.component.html b/client/src/app/core/nav-bar/nav-bar.component.html index 8a707b4..fbf41c1 100644 --- a/client/src/app/core/nav-bar/nav-bar.component.html +++ b/client/src/app/core/nav-bar/nav-bar.component.html @@ -10,8 +10,29 @@
{{basket.items.length}}
- Login - Sign up + + Login + Sign up + + + + diff --git a/client/src/app/core/nav-bar/nav-bar.component.ts b/client/src/app/core/nav-bar/nav-bar.component.ts index f4fc5b3..a56ec20 100644 --- a/client/src/app/core/nav-bar/nav-bar.component.ts +++ b/client/src/app/core/nav-bar/nav-bar.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { AccountService } from 'src/app/account/account.service'; import { BasketService } from 'src/app/basket/basket.service'; import { IBasket } from 'src/app/shared/models/baskset'; +import { IUser } from 'src/app/shared/models/user'; @Component({ selector: 'app-nav-bar', @@ -9,12 +11,18 @@ import { IBasket } from 'src/app/shared/models/baskset'; styleUrls: ['./nav-bar.component.scss'] }) export class NavBarComponent implements OnInit { - basket$ : Observable + basket$ : Observable; + currentUser$: Observable; - constructor(private basketService: BasketService) { } + constructor(private basketService: BasketService, private accountService: AccountService) { } ngOnInit(): void { this.basket$ = this.basketService.basket$; + this.currentUser$ = this.accountService.currentUser$; + } + + logout() { + this.accountService.logout(); } } diff --git a/client/src/app/shared/components/text-inputs/text-inputs.component.html b/client/src/app/shared/components/text-inputs/text-inputs.component.html new file mode 100644 index 0000000..dc026f2 --- /dev/null +++ b/client/src/app/shared/components/text-inputs/text-inputs.component.html @@ -0,0 +1,21 @@ +
+ +
+ +
+ {{label}} is required + Email is invalid +
+
+ Email Address is in use. +
+
diff --git a/client/src/app/shared/components/text-inputs/text-inputs.component.scss b/client/src/app/shared/components/text-inputs/text-inputs.component.scss new file mode 100644 index 0000000..3627e09 --- /dev/null +++ b/client/src/app/shared/components/text-inputs/text-inputs.component.scss @@ -0,0 +1,7 @@ +.loader { + position: absolute; + width: auto; + top: 20px; + right: 10px; + margin-top: 0; +} diff --git a/client/src/app/shared/components/text-inputs/text-inputs.component.ts b/client/src/app/shared/components/text-inputs/text-inputs.component.ts new file mode 100644 index 0000000..a982d9b --- /dev/null +++ b/client/src/app/shared/components/text-inputs/text-inputs.component.ts @@ -0,0 +1,40 @@ +import { Component, ElementRef, Input, OnInit, Self, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NgControl } from '@angular/forms'; + +@Component({ + selector: 'app-text-inputs', + templateUrl: './text-inputs.component.html', + styleUrls: ['./text-inputs.component.scss'] +}) +export class TextInputsComponent implements OnInit, ControlValueAccessor { + @ViewChild('input', {static: true}) input: ElementRef; + @Input() type = 'text'; + @Input() label: string; + + constructor(@Self() public controlDir: NgControl) { + this.controlDir.valueAccessor = this; + } + + ngOnInit(): void { + const control = this.controlDir.control; + const validators = control.validator ? [control.validator] : []; + const asyncValidators = control.asyncValidator ? [control.asyncValidator] : []; + + control.setValidators(validators); + control.setAsyncValidators(asyncValidators); + control.updateValueAndValidity(); + } + + onChange(evt) {} + onTouched(evt?) {} + + writeValue(obj: any): void { + this.input.nativeElement.value = obj || ''; + } + registerOnChange(fn: any): void { + this.onChange = fn; + } + registerOnTouched(fn: any): void { + this.onTouched = fn; + } +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 00e9a3e..a0a30c1 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -6,19 +6,22 @@ import { CarouselModule } from 'ngx-bootstrap/carousel'; import { PagingHeaderComponent } from './components/paging-header/paging-header.component'; import { PagerComponent } from './components/pager/pager.component'; import { OrderTotalsComponent } from './components/order-totals/order-totals.component'; - +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { TextInputsComponent } from './components/text-inputs/text-inputs.component'; @NgModule({ declarations: [ PagingHeaderComponent, PagerComponent, - OrderTotalsComponent + OrderTotalsComponent, + TextInputsComponent ], imports: [ CommonModule, PaginationModule.forRoot(), CarouselModule.forRoot(), + BsDropdownModule.forRoot(), ReactiveFormsModule ], exports: [ @@ -27,7 +30,9 @@ import { OrderTotalsComponent } from './components/order-totals/order-totals.com PagerComponent, CarouselModule, OrderTotalsComponent, - ReactiveFormsModule + ReactiveFormsModule, + BsDropdownModule, + TextInputsComponent ] }) export class SharedModule { }