From f60f6c3e2935421e0483b759327e64c5d5b176ea Mon Sep 17 00:00:00 2001 From: arturovt Date: Fri, 1 May 2020 02:06:54 +0300 Subject: [PATCH] feat: enable `strict` mode and cleanup the code --- src/app/admin/admin-user-guard.ts | 12 ++- src/app/app-routing.module.ts | 27 +++++ src/app/app-routing/app-routing.module.ts | 26 ----- src/app/app.component.html | 5 +- src/app/app.component.ts | 55 +++------- src/app/app.module.ts | 60 +++++------ src/app/auth/auth-guard.service.ts | 17 --- src/app/auth/auth.module.ts | 20 +--- src/app/auth/auth.service.ts | 71 ------------- src/app/auth/index.ts | 2 - src/app/auth/login/login.component.ts | 26 ++--- src/app/auth/register/register.component.html | 4 +- src/app/auth/register/register.component.ts | 84 ++++++++------- src/app/auth/token.storage.ts | 25 ----- src/app/header/header.component.html | 8 +- src/app/header/header.component.ts | 28 ++--- src/app/interceptors/header.interceptor.ts | 15 ++- .../interceptors/http-error.interceptor.ts | 30 +++--- src/app/shared/guards/auth.guard.ts | 25 +++++ src/app/shared/guards/index.ts | 1 + src/app/shared/interfaces/index.ts | 1 + src/app/shared/interfaces/user.interface.ts | 7 ++ .../services}/auth/auth.service.spec.ts | 0 src/app/shared/services/auth/auth.service.ts | 100 ++++++++++++++++++ src/app/shared/services/auth/token.storage.ts | 20 ++++ src/app/shared/services/index.ts | 1 + src/index.html | 43 +++++--- src/main.ts | 4 - src/typings.d.ts | 8 ++ tsconfig.app.json | 3 - tsconfig.json | 2 + 31 files changed, 376 insertions(+), 354 deletions(-) create mode 100644 src/app/app-routing.module.ts delete mode 100644 src/app/app-routing/app-routing.module.ts delete mode 100644 src/app/auth/auth-guard.service.ts delete mode 100644 src/app/auth/auth.service.ts delete mode 100644 src/app/auth/index.ts delete mode 100644 src/app/auth/token.storage.ts create mode 100644 src/app/shared/guards/auth.guard.ts create mode 100644 src/app/shared/guards/index.ts create mode 100644 src/app/shared/interfaces/index.ts create mode 100644 src/app/shared/interfaces/user.interface.ts rename src/app/{ => shared/services}/auth/auth.service.spec.ts (100%) create mode 100644 src/app/shared/services/auth/auth.service.ts create mode 100644 src/app/shared/services/auth/token.storage.ts create mode 100644 src/app/shared/services/index.ts diff --git a/src/app/admin/admin-user-guard.ts b/src/app/admin/admin-user-guard.ts index 33e594b6..a76a677a 100644 --- a/src/app/admin/admin-user-guard.ts +++ b/src/app/admin/admin-user-guard.ts @@ -1,10 +1,16 @@ import { Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { AuthService } from '@app/shared/services'; + @Injectable() export class OnlyAdminUsersGuard implements CanActivate { - canActivate() { - const user = (window).user; - return user && user.isAdmin; + constructor(private authService: AuthService) {} + + canActivate(): Observable { + return this.authService.getUser().pipe(map(user => !!user?.isAdmin)); } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts new file mode 100644 index 00000000..9ee71b2b --- /dev/null +++ b/src/app/app-routing.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { AuthGuard } from './shared/guards'; +import { HomeComponent } from './home/home.component'; + +const routes: Routes = [ + { + path: '', + component: HomeComponent, + canActivate: [AuthGuard], + }, + { + path: 'auth', + loadChildren: () => import('./auth/auth.module').then(m => m.AuthModule), + }, + { + path: 'admin', + loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/src/app/app-routing/app-routing.module.ts b/src/app/app-routing/app-routing.module.ts deleted file mode 100644 index e376efe4..00000000 --- a/src/app/app-routing/app-routing.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from '../auth/auth-guard.service'; -import { HomeComponent } from '../home/home.component'; - -const routes: Routes = [ - { - path: '', - component: HomeComponent - }, - { - path: 'auth', - loadChildren: () => import('../auth/auth.module').then(m => m.AuthModule) - }, - { - path: 'admin', - loadChildren: () => import('../admin/admin.module').then(m => m.AdminModule) - } -]; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule], - providers: [AuthGuard] -}) -export class AppRoutingModule {} diff --git a/src/app/app.component.html b/src/app/app.component.html index 25b03d3e..4b8560c9 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,6 +1,5 @@ - +
-
-
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 348ce62d..843cd29a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,56 +1,33 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; -import { AuthService } from './auth/auth.service'; +import { merge, Observable } from 'rxjs'; + +import { User } from './shared/interfaces'; +import { AuthService } from './shared/services'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + styleUrls: ['./app.component.scss'], }) -export class AppComponent implements OnInit { - private userSubscription: Subscription; - public user: any; +export class AppComponent { + user$: Observable = merge( + // Init on startup + this.authService.me(), + // Update after login/register/logout + this.authService.getUser() + ); constructor( - private authService: AuthService, - private router: Router, private domSanitizer: DomSanitizer, - private matIconRegistry: MatIconRegistry + private matIconRegistry: MatIconRegistry, + private authService: AuthService ) { this.registerSvgIcons(); } - public ngOnInit() { - // init this.user on startup - this.authService.me().subscribe(data => { - this.user = data.user; - }); - - // update this.user after login/register/logout - this.userSubscription = this.authService.$userSource.subscribe(user => { - this.user = user; - }); - } - - logout(): void { - this.authService.signOut(); - this.navigate(''); - } - - navigate(link): void { - this.router.navigate([link]); - } - - ngOnDestroy() { - if (this.userSubscription) { - this.userSubscription.unsubscribe(); - } - } - registerSvgIcons() { [ 'close', @@ -84,7 +61,7 @@ export class AppComponent implements OnInit { 'tow-truck', 'transportation', 'trolleybus', - 'water-transportation' + 'water-transportation', ].forEach(icon => { this.matIconRegistry.addSvgIcon( icon, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 898f2a8a..cf73d09a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,47 +1,43 @@ -import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgModule } from '@angular/core'; -import { RouterModule, PreloadAllModules } from '@angular/router'; +import { NgModule, APP_INITIALIZER } from '@angular/core'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { SharedModule } from './shared/shared.module'; -import { AuthModule } from './auth/auth.module'; import { AppComponent } from './app.component'; -import { AdminModule } from './admin/admin.module'; import { AuthHeaderInterceptor } from './interceptors/header.interceptor'; import { CatchErrorInterceptor } from './interceptors/http-error.interceptor'; -import { AppRoutingModule } from './app-routing/app-routing.module'; +import { AppRoutingModule } from './app-routing.module'; import { HeaderComponent } from './header/header.component'; import { HomeComponent } from './home/home.component'; +import { AuthService } from './shared/services'; + +export function appInitializerFactory(authService: AuthService) { + return () => authService.checkTheUserOnTheFirstLoad(); +} @NgModule({ - declarations: [ - AppComponent, - HeaderComponent, - HomeComponent, + imports: [BrowserAnimationsModule, HttpClientModule, SharedModule, AppRoutingModule], + declarations: [AppComponent, HeaderComponent, HomeComponent], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthHeaderInterceptor, + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: CatchErrorInterceptor, + multi: true, + }, + { + provide: APP_INITIALIZER, + useFactory: appInitializerFactory, + multi: true, + deps: [AuthService], + }, ], - imports: [ - BrowserModule, - BrowserAnimationsModule, - HttpClientModule, - RouterModule, - SharedModule, - AuthModule, - AdminModule, - AppRoutingModule, - ], - providers: [{ - provide: HTTP_INTERCEPTORS, - useClass: AuthHeaderInterceptor, - multi: true, - }, { - provide: HTTP_INTERCEPTORS, - useClass: CatchErrorInterceptor, - multi: true, - }], - entryComponents: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/auth/auth-guard.service.ts b/src/app/auth/auth-guard.service.ts deleted file mode 100644 index 16d6d16c..00000000 --- a/src/app/auth/auth-guard.service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@angular/core'; -import { CanActivate, Router } from '@angular/router'; - -@Injectable() -export class AuthGuard implements CanActivate { - - constructor(public router: Router) {} - - canActivate() { - const user = (window).user; - if (user) return true; - - // not logged in so redirect to login page with the return url - this.router.navigate(['/auth/login']); - return false; - } -} diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts index 1affc4ec..f1f64ea1 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/auth/auth.module.ts @@ -1,27 +1,13 @@ import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { LoginComponent } from './login/login.component'; import { RegisterComponent } from './register/register.component'; -import { AuthService } from './auth.service'; -import { TokenStorage } from './token.storage'; import { AuthRoutingModule } from './auth-routing.module'; @NgModule({ - imports: [ - CommonModule, - SharedModule, - AuthRoutingModule, - ], - declarations: [ - LoginComponent, - RegisterComponent - ], - providers: [ - AuthService, - TokenStorage - ] + imports: [SharedModule, AuthRoutingModule], + declarations: [LoginComponent, RegisterComponent], }) -export class AuthModule { } +export class AuthModule {} diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts deleted file mode 100644 index 6b58aa20..00000000 --- a/src/app/auth/auth.service.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, Subject } from 'rxjs'; - -import { TokenStorage } from './token.storage'; - -@Injectable() -export class AuthService { - - constructor(private http : HttpClient, private token: TokenStorage) {} - - public $userSource = new Subject(); - - login(email : string, password : string) : Observable { - return Observable.create(observer => { - this.http.post('/api/auth/login', { - email, - password - }).subscribe((data : any) => { - observer.next({user: data.user}); - this.setUser(data.user); - this.token.saveToken(data.token); - observer.complete(); - }) - }); - } - - register(fullname : string, email : string, password : string, repeatPassword : string) : Observable { - return Observable.create(observer => { - this.http.post('/api/auth/register', { - fullname, - email, - password, - repeatPassword - }).subscribe((data : any) => { - observer.next({user: data.user}); - this.setUser(data.user); - this.token.saveToken(data.token); - observer.complete(); - }) - }); - } - - setUser(user): void { - if (user) user.isAdmin = (user.roles.indexOf('admin') > -1); - this.$userSource.next(user); - (window).user = user; - } - - getUser(): Observable { - return this.$userSource.asObservable(); - } - - me(): Observable { - return Observable.create(observer => { - const tokenVal = this.token.getToken(); - if (!tokenVal) return observer.complete(); - this.http.get('/api/auth/me').subscribe((data : any) => { - observer.next({user: data.user}); - this.setUser(data.user); - observer.complete(); - }) - }); - } - - signOut(): void { - this.token.signOut(); - this.setUser(null); - delete (window).user; - } -} diff --git a/src/app/auth/index.ts b/src/app/auth/index.ts deleted file mode 100644 index e6b89d56..00000000 --- a/src/app/auth/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// export * from './auth.module'; - diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts index b920c330..ad5a33c6 100644 --- a/src/app/auth/login/login.component.ts +++ b/src/app/auth/login/login.component.ts @@ -1,28 +1,22 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Router } from '@angular/router'; -import {AuthService} from '../auth.service'; +import { AuthService } from '@app/shared/services'; @Component({ selector: 'app-login', templateUrl: './login.component.html', - styleUrls: ['../auth.component.scss'] + styleUrls: ['../auth.component.scss'], }) -export class LoginComponent implements OnInit { +export class LoginComponent { + email: string | null = null; + password: string | null = null; - constructor(private authService: AuthService, private router: Router) { } - - email: string; - password: string; - - ngOnInit() { - } + constructor(private router: Router, private authService: AuthService) {} login(): void { - this.authService.login(this.email, this.password) - .subscribe(data => { - this.router.navigate(['']); - }) + this.authService.login(this.email!, this.password!).subscribe(() => { + this.router.navigateByUrl('/'); + }); } - } diff --git a/src/app/auth/register/register.component.html b/src/app/auth/register/register.component.html index 42f2eac1..37b77c5c 100644 --- a/src/app/auth/register/register.component.html +++ b/src/app/auth/register/register.component.html @@ -16,7 +16,7 @@ - Invalid email address + Invalid email address @@ -31,7 +31,7 @@ - Password mismatch + Password mismatch diff --git a/src/app/auth/register/register.component.ts b/src/app/auth/register/register.component.ts index fbdbca2d..13355ed7 100644 --- a/src/app/auth/register/register.component.ts +++ b/src/app/auth/register/register.component.ts @@ -1,56 +1,64 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Router } from '@angular/router'; -import { FormGroup, FormControl, Validators, ValidationErrors } from '@angular/forms'; +import { + FormGroup, + FormControl, + Validators, + ValidationErrors, + AbstractControl, +} from '@angular/forms'; - -import {AuthService} from '../auth.service'; +import { AuthService } from '@app/shared/services'; @Component({ selector: 'app-register', templateUrl: './register.component.html', - styleUrls: ['../auth.component.scss'] + styleUrls: ['../auth.component.scss'], }) -export class RegisterComponent implements OnInit { +export class RegisterComponent { + constructor(private router: Router, private authService: AuthService) {} - constructor(private authService: AuthService, private router: Router) { } - - ngOnInit() { - } - - passwordsMatchValidator(control: FormControl): ValidationErrors { - let password = control.root.get('password'); - return password && control.value !== password.value ? { - passwordMatch: true - }: null; + passwordsMatchValidator(control: FormControl): ValidationErrors | null { + const password = control.root.get('password'); + return password && control.value !== password.value + ? { + passwordMatch: true, + } + : null; } userForm = new FormGroup({ fullname: new FormControl('', [Validators.required]), email: new FormControl('', [Validators.required, Validators.email]), password: new FormControl('', [Validators.required]), - repeatPassword: new FormControl('', [Validators.required, this.passwordsMatchValidator]) - }) + repeatPassword: new FormControl('', [Validators.required, this.passwordsMatchValidator]), + }); - get fullname(): any { return this.userForm.get('fullname'); } - get email(): any { return this.userForm.get('email'); } - get password(): any { return this.userForm.get('password'); } - get repeatPassword(): any { return this.userForm.get('repeatPassword'); } - - register() { - - if(!this.userForm.valid) return; - - let { - fullname, - email, - password, - repeatPassword - } = this.userForm.getRawValue(); - - this.authService.register(fullname, email, password, repeatPassword) - .subscribe(data => { - this.router.navigate(['']); - }) + get fullname(): AbstractControl { + return this.userForm.get('fullname')!; } + get email(): AbstractControl { + return this.userForm.get('email')!; + } + + get password(): AbstractControl { + return this.userForm.get('password')!; + } + + get repeatPassword(): AbstractControl { + return this.userForm.get('repeatPassword')!; + } + + register(): void { + if (this.userForm.invalid) { + return; + } + + const { fullname, email, password, repeatPassword } = this.userForm.getRawValue(); + + this.authService.register(fullname, email, password, repeatPassword).subscribe(data => { + this.router.navigate(['']); + }); + } } diff --git a/src/app/auth/token.storage.ts b/src/app/auth/token.storage.ts deleted file mode 100644 index 68ca9c2e..00000000 --- a/src/app/auth/token.storage.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@angular/core'; - - -const TOKEN_KEY = 'AuthToken'; - -@Injectable() -export class TokenStorage { - - constructor() { } - - signOut() { - window.localStorage.removeItem(TOKEN_KEY); - window.localStorage.clear(); - } - - public saveToken(token: string) { - if (!token) return; - window.localStorage.removeItem(TOKEN_KEY); - window.localStorage.setItem(TOKEN_KEY, token); - } - - public getToken(): string { - return localStorage.getItem(TOKEN_KEY); - } -} \ No newline at end of file diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 226a06c3..d48b1854 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -1,14 +1,14 @@
- + - Login + Login diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index d44cf4a2..32a0feb3 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -1,32 +1,22 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { AuthService } from '../auth/auth.service'; +import { User } from '@app/shared/interfaces'; + +import { AuthService } from '@app/shared/services'; @Component({ selector: 'app-header', templateUrl: './header.component.html', - styleUrls: ['./header.component.scss'] + styleUrls: ['./header.component.scss'], }) -export class HeaderComponent implements OnInit { +export class HeaderComponent { + @Input() user: User | null = null; - @Input() user: any = {}; - - constructor( - private authService: AuthService, - private router: Router - ) { } - - ngOnInit() { - } + constructor(private router: Router, private authService: AuthService) {} logout(): void { this.authService.signOut(); - this.navigate('/auth/login'); + this.router.navigateByUrl('/auth/login'); } - - navigate(link): void { - this.router.navigate([link]); - } - } diff --git a/src/app/interceptors/header.interceptor.ts b/src/app/interceptors/header.interceptor.ts index bdf2d460..c958708b 100644 --- a/src/app/interceptors/header.interceptor.ts +++ b/src/app/interceptors/header.interceptor.ts @@ -1,20 +1,19 @@ import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; + import { Observable } from 'rxjs'; -import { TokenStorage } from '../auth/token.storage'; +import { AuthService } from '@app/shared/services'; @Injectable() export class AuthHeaderInterceptor implements HttpInterceptor { + constructor(private authService: AuthService) {} + intercept(req: HttpRequest, next: HttpHandler): Observable> { - // Clone the request to add the new header - const token = new TokenStorage(); - const tokenVal = token.getToken(); - const clonedRequest = req.clone({ - headers: req.headers.set('Authorization', tokenVal ? `Bearer ${tokenVal}` : '') + req = req.clone({ + setHeaders: this.authService.getAuthorizationHeaders(), }); - // Pass the cloned request instead of the original request to the next handle - return next.handle(clonedRequest); + return next.handle(req); } } diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts index 94678c55..02818e77 100644 --- a/src/app/interceptors/http-error.interceptor.ts +++ b/src/app/interceptors/http-error.interceptor.ts @@ -4,24 +4,30 @@ import { HttpInterceptor, HttpHandler, HttpRequest, - HttpErrorResponse + HttpErrorResponse, } from '@angular/common/http'; +import { MatSnackBar } from '@angular/material/snack-bar'; + import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Injectable() export class CatchErrorInterceptor implements HttpInterceptor { - intercept(request: HttpRequest, next: HttpHandler): Observable> { - return next.handle(request).pipe( - catchError(error => { - if (error instanceof HttpErrorResponse) { - const text = - error.error && error.error.message ? error.error.message : error.statusText; - (window).globalEvents.emit('open error dialog', text); - } + constructor(private snackBar: MatSnackBar) {} - return throwError(error); - }) - ); + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe(catchError(this.showSnackBar)); } + + private showSnackBar = (response: HttpErrorResponse): Observable => { + const text: string | undefined = response.error?.message ?? response.error.statusText; + + if (text) { + this.snackBar.open(text, 'Close', { + duration: 2000, + }); + } + + return throwError(response); + }; } diff --git a/src/app/shared/guards/auth.guard.ts b/src/app/shared/guards/auth.guard.ts new file mode 100644 index 00000000..a820a302 --- /dev/null +++ b/src/app/shared/guards/auth.guard.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { AuthService } from '../services'; + +@Injectable({ providedIn: 'root' }) +export class AuthGuard implements CanActivate { + constructor(private router: Router, private authService: AuthService) {} + + canActivate(): Observable { + return this.authService.getUser().pipe( + map(user => { + if (user !== null) { + return true; + } + + this.router.navigateByUrl('/auth/login'); + return false; + }) + ); + } +} diff --git a/src/app/shared/guards/index.ts b/src/app/shared/guards/index.ts new file mode 100644 index 00000000..b41e34a4 --- /dev/null +++ b/src/app/shared/guards/index.ts @@ -0,0 +1 @@ +export * from './auth.guard'; diff --git a/src/app/shared/interfaces/index.ts b/src/app/shared/interfaces/index.ts new file mode 100644 index 00000000..4bd6e45c --- /dev/null +++ b/src/app/shared/interfaces/index.ts @@ -0,0 +1 @@ +export * from './user.interface'; diff --git a/src/app/shared/interfaces/user.interface.ts b/src/app/shared/interfaces/user.interface.ts new file mode 100644 index 00000000..4b3a4eed --- /dev/null +++ b/src/app/shared/interfaces/user.interface.ts @@ -0,0 +1,7 @@ +export interface User { + _id: string; + fullname: string; + createdAt: string; + roles: string[]; + isAdmin: boolean; +} diff --git a/src/app/auth/auth.service.spec.ts b/src/app/shared/services/auth/auth.service.spec.ts similarity index 100% rename from src/app/auth/auth.service.spec.ts rename to src/app/shared/services/auth/auth.service.spec.ts diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts new file mode 100644 index 00000000..2730f12d --- /dev/null +++ b/src/app/shared/services/auth/auth.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable, BehaviorSubject, EMPTY } from 'rxjs'; +import { tap, pluck } from 'rxjs/operators'; + +import { User } from '@app/shared/interfaces'; + +import { TokenStorage } from './token.storage'; + +interface AuthResponse { + token: string; + user: User; +} + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private user$ = new BehaviorSubject(null); + + constructor(private http: HttpClient, private tokenStorage: TokenStorage) {} + + login(email: string, password: string): Observable { + return this.http + .post('/api/auth/login', { email, password }) + .pipe( + tap(({ token, user }) => { + this.setUser(user); + this.tokenStorage.saveToken(token); + }), + pluck('user') + ); + } + + register( + fullname: string, + email: string, + password: string, + repeatPassword: string + ): Observable { + return this.http + .post('/api/auth/register', { + fullname, + email, + password, + repeatPassword, + }) + .pipe( + tap(({ token, user }) => { + this.setUser(user); + this.tokenStorage.saveToken(token); + }), + pluck('user') + ); + } + + setUser(user: User | null): void { + if (user) { + user.isAdmin = user.roles.includes('admin'); + } + + this.user$.next(user); + window.user = user; + } + + getUser(): Observable { + return this.user$.asObservable(); + } + + me(): Observable { + const token: string | null = this.tokenStorage.getToken(); + + if (token === null) { + return EMPTY; + } + + return this.http.get('/api/auth/me').pipe( + tap(({ user }) => this.setUser(user)), + pluck('user') + ); + } + + signOut(): void { + this.tokenStorage.signOut(); + this.setUser(null); + delete window.user; + } + + getAuthorizationHeaders() { + const token: string | null = this.tokenStorage.getToken() || ''; + return { Authorization: `Bearer ${token}` }; + } + + /** + * Let's try to get user's information if he was logged in previously, + * thus we can ensure that the user is able to access the `/` (home) page. + */ + checkTheUserOnTheFirstLoad(): Promise { + return this.me().toPromise(); + } +} diff --git a/src/app/shared/services/auth/token.storage.ts b/src/app/shared/services/auth/token.storage.ts new file mode 100644 index 00000000..5ac08f16 --- /dev/null +++ b/src/app/shared/services/auth/token.storage.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class TokenStorage { + private tokenKey = 'authToken'; + + signOut(): void { + localStorage.removeItem(this.tokenKey); + localStorage.clear(); + } + + saveToken(token?: string): void { + if (!token) return; + localStorage.setItem(this.tokenKey, token); + } + + getToken(): string | null { + return localStorage.getItem(this.tokenKey); + } +} diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts new file mode 100644 index 00000000..fd46be57 --- /dev/null +++ b/src/app/shared/services/index.ts @@ -0,0 +1 @@ +export * from './auth/auth.service'; diff --git a/src/index.html b/src/index.html index e2bb1628..49181eb5 100644 --- a/src/index.html +++ b/src/index.html @@ -1,16 +1,33 @@ - + - - - Mean - + + + Mean + - - - - - - - - + + + + + + + + + + + + diff --git a/src/main.ts b/src/main.ts index 6bcf97a1..5aa3bf02 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,9 @@ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { EventEmitter } from 'events'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; -(window as any).global = window; -(window as any).globalEvents = new EventEmitter(); - if (environment.production) { enableProdMode(); } diff --git a/src/typings.d.ts b/src/typings.d.ts index bbab69d2..717189d7 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -1,4 +1,12 @@ +import { User } from '@app/shared/interfaces'; + declare module '*.json' { const value: any; export default value; } + +declare global { + interface Window { + user: User | null; + } +} diff --git a/tsconfig.app.json b/tsconfig.app.json index f7c4c734..5573550b 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,8 +1,5 @@ { "extends": "./tsconfig.json", - "compilerOptions": { - "module": "ESNext" - }, "files": [ "src/polyfills.ts", "src/main.ts" diff --git a/tsconfig.json b/tsconfig.json index 032afb3f..85afcdf8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,9 @@ "ESNext", "DOM" ], + "module": "ESNext", "moduleResolution": "node", + "strict": true, "sourceMap": true, "declaration": false, "importHelpers": true,