Merge pull request #1978 from arturovt/ng-9-strict

feat: enable `strict` mode and cleanup the code
This commit is contained in:
Lior Kesos 2020-05-13 00:33:44 +03:00 committed by GitHub
commit 5a97be51ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 376 additions and 354 deletions

View File

@ -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 = (<any>window).user;
return user && user.isAdmin;
constructor(private authService: AuthService) {}
canActivate(): Observable<boolean> {
return this.authService.getUser().pipe(map(user => !!user?.isAdmin));
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -1,6 +1,5 @@
<app-header [user]="user"></app-header>
<app-header [user]="user$ | async"></app-header>
<div class="wrapper-app">
<router-outlet></router-outlet>
</div>
<footer>
</footer>
<footer></footer>

View File

@ -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<User | null> = 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,

View File

@ -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 {}

View File

@ -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 = (<any>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;
}
}

View File

@ -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 {}

View File

@ -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<any>();
login(email : string, password : string) : Observable <any> {
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 <any> {
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);
(<any>window).user = user;
}
getUser(): Observable<any> {
return this.$userSource.asObservable();
}
me(): Observable<any> {
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 (<any>window).user;
}
}

View File

@ -1,2 +0,0 @@
// export * from './auth.module';

View File

@ -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('/');
});
}
}

View File

@ -16,7 +16,7 @@
<td>
<mat-form-field>
<input matInput placeholder="Email" formControlName="email" name="email" required>
<mat-error *ngIf="email.invalid && email.errors.email">Invalid email address</mat-error>
<mat-error *ngIf="email.invalid && email.hasError('email')">Invalid email address</mat-error>
</mat-form-field>
</td>
</tr>
@ -31,7 +31,7 @@
<td>
<mat-form-field>
<input matInput placeholder="Repeat Password" formControlName="repeatPassword" type="password" name="repeatPassword" required>
<mat-error *ngIf="repeatPassword.invalid && repeatPassword.errors.passwordMatch">Password mismatch</mat-error>
<mat-error *ngIf="repeatPassword.invalid && repeatPassword.hasError('passwordMatch')">Password mismatch</mat-error>
</mat-form-field>
</td>
</tr>

View File

@ -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(['']);
});
}
}

View File

@ -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);
}
}

View File

@ -1,14 +1,14 @@
<header>
<mat-toolbar color="primary">
<a [routerLink]="['/']" class="logo"></a>
<a routerLink="/" class="logo"></a>
<span class="example-spacer"></span>
<a class="links side" [routerLink]="['/auth/login']" *ngIf="!user">Login</a>
<a class="links side" routerLink="/auth/login" *ngIf="!user">Login</a>
<div>
<a class="links side" *ngIf="user" [matMenuTriggerFor]="menu">
<mat-icon>account_circle</mat-icon>{{user.fullname}}
<mat-icon>account_circle</mat-icon>{{ user.fullname }}
</a>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngIf="user && user.isAdmin" [routerLink]="['/admin']">admin</button>
<button mat-menu-item *ngIf="user?.isAdmin" routerLink="/admin">admin</button>
<button mat-menu-item (click)="logout()">logout</button>
</mat-menu>
</div>

View File

@ -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]);
}
}

View File

@ -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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 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);
}
}

View File

@ -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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse) {
const text =
error.error && error.error.message ? error.error.message : error.statusText;
(<any>window).globalEvents.emit('open error dialog', text);
}
constructor(private snackBar: MatSnackBar) {}
return throwError(error);
})
);
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(this.showSnackBar));
}
private showSnackBar = (response: HttpErrorResponse): Observable<never> => {
const text: string | undefined = response.error?.message ?? response.error.statusText;
if (text) {
this.snackBar.open(text, 'Close', {
duration: 2000,
});
}
return throwError(response);
};
}

View File

@ -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<boolean> {
return this.authService.getUser().pipe(
map(user => {
if (user !== null) {
return true;
}
this.router.navigateByUrl('/auth/login');
return false;
})
);
}
}

View File

@ -0,0 +1 @@
export * from './auth.guard';

View File

@ -0,0 +1 @@
export * from './user.interface';

View File

@ -0,0 +1,7 @@
export interface User {
_id: string;
fullname: string;
createdAt: string;
roles: string[];
isAdmin: boolean;
}

View File

@ -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<User | null>(null);
constructor(private http: HttpClient, private tokenStorage: TokenStorage) {}
login(email: string, password: string): Observable<User> {
return this.http
.post<AuthResponse>('/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<User> {
return this.http
.post<AuthResponse>('/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<User | null> {
return this.user$.asObservable();
}
me(): Observable<User> {
const token: string | null = this.tokenStorage.getToken();
if (token === null) {
return EMPTY;
}
return this.http.get<AuthResponse>('/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<User> {
return this.me().toPromise();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
export * from './auth/auth.service';

View File

@ -1,16 +1,33 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Mean</title>
<base href="/">
<head>
<meta charset="utf-8" />
<title>Mean</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Exo:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link
rel="prefetch"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
as="style"
crossorigin
/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link
rel="preload"
href="https://fonts.googleapis.com/css?family=Exo:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i"
as="style"
crossorigin
/>
<link
href="https://fonts.googleapis.com/css?family=Exo:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i"
rel="stylesheet"
/>
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -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();
}

8
src/typings.d.ts vendored
View File

@ -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;
}
}

View File

@ -1,8 +1,5 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext"
},
"files": [
"src/polyfills.ts",
"src/main.ts"

View File

@ -14,7 +14,9 @@
"ESNext",
"DOM"
],
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"sourceMap": true,
"declaration": false,
"importHelpers": true,