diff --git a/Front/skydivelogs-app/src/app/app.module.ts b/Front/skydivelogs-app/src/app/app.module.ts index 68ae247..15e7e19 100644 --- a/Front/skydivelogs-app/src/app/app.module.ts +++ b/Front/skydivelogs-app/src/app/app.module.ts @@ -17,9 +17,9 @@ import { NewGearComponent } from './new-gear/new-gear.component'; import { NewDropZoneComponent } from './new-drop-zone/new-drop-zone.component'; import { NewJumpTypeComponent } from './new-jump-type/new-jump-type.component'; import { DefaultComponent } from './default/default.component'; +import { LoginComponent } from './login/login.component'; import { DateService } from '../services/date.service'; - import { AircraftService } from '../services/aircraft.service'; import { DropzoneService } from '../services/dropzone.service'; import { GearService } from '../services/gear.service'; @@ -27,6 +27,8 @@ import { JumpService } from '../services/jump.service'; import { JumpTypeService } from '../services/jump-type.service'; import { StatsService } from '../services/stats.service'; import { ServiceComm } from '../services/service-comm.service'; +import { RequestCache } from '../services/request-cache.service'; +import { AuthGuardService } from '../services/auth-guard.service'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -43,19 +45,23 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; -import { RequestCache } from '../services/request-cache.service'; -import { CachingInterceptor } from '../services/caching-interceptor.service'; +import { CachingInterceptor } from '../interceptor/caching.interceptor'; +import { BasicAuthInterceptor } from '../interceptor/basic-auth.interceptor'; +import { ErrorInterceptor } from '../interceptor/error.interceptor'; + const appRoutes: Routes = [ - { path: '', component: DefaultComponent }, + { path: '', component: DefaultComponent, canActivate: [AuthGuardService] }, - { path: 'summary', component: SummaryComponent }, - { path: 'jumps', component: ListOfJumpsComponent }, - { path: 'dzs', component: ListOfDzsComponent }, - { path: 'newjump', component: NewJumpComponent }, - { path: 'aircrafts', component: ListOfAircraftsComponent }, - { path: 'jumpTypes', component: ListOfJumpTypesComponent }, - { path: 'gears', component: ListOfGearsComponent }, + { path: 'summary', component: SummaryComponent, canActivate: [AuthGuardService] }, + { path: 'jumps', component: ListOfJumpsComponent, canActivate: [AuthGuardService] }, + { path: 'dzs', component: ListOfDzsComponent, canActivate: [AuthGuardService] }, + { path: 'newjump', component: NewJumpComponent, canActivate: [AuthGuardService] }, + { path: 'aircrafts', component: ListOfAircraftsComponent, canActivate: [AuthGuardService] }, + { path: 'jumpTypes', component: ListOfJumpTypesComponent, canActivate: [AuthGuardService] }, + { path: 'gears', component: ListOfGearsComponent, canActivate: [AuthGuardService] }, + + { path: 'login', component: LoginComponent }, { path: '**', redirectTo: '' } ]; @@ -74,7 +80,8 @@ const appRoutes: Routes = [ NewGearComponent, NewDropZoneComponent, NewJumpTypeComponent, - DefaultComponent + DefaultComponent, + LoginComponent ], imports: [ RouterModule.forRoot( @@ -111,6 +118,8 @@ const appRoutes: Routes = [ DateService, RequestCache, // { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true } + { provide: HTTP_INTERCEPTORS, useClass: BasicAuthInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, ], bootstrap: [AppComponent] }) diff --git a/Front/skydivelogs-app/src/app/login/login.component.css b/Front/skydivelogs-app/src/app/login/login.component.css new file mode 100644 index 0000000..e69de29 diff --git a/Front/skydivelogs-app/src/app/login/login.component.html b/Front/skydivelogs-app/src/app/login/login.component.html new file mode 100644 index 0000000..bf55e88 --- /dev/null +++ b/Front/skydivelogs-app/src/app/login/login.component.html @@ -0,0 +1,34 @@ +
+
+ Username: test
+ Password: test +
+
+

Angular 8 Basic Auth Login Example

+
+
+
+ + +
+
Username is required
+
+
+
+ + +
+
Password is required
+
+
+ +
{{error}}
+
+
+
+
diff --git a/Front/skydivelogs-app/src/app/login/login.component.spec.ts b/Front/skydivelogs-app/src/app/login/login.component.spec.ts new file mode 100644 index 0000000..d6d85a8 --- /dev/null +++ b/Front/skydivelogs-app/src/app/login/login.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Front/skydivelogs-app/src/app/login/login.component.ts b/Front/skydivelogs-app/src/app/login/login.component.ts new file mode 100644 index 0000000..f92a831 --- /dev/null +++ b/Front/skydivelogs-app/src/app/login/login.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { first } from 'rxjs/operators'; + +import { AuthenticationService } from '../../services/authentication.service'; + + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] +}) +export class LoginComponent implements OnInit { + loginForm: FormGroup; + loading = false; + submitted = false; + returnUrl: string; + error = ''; + + constructor( + private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private authenticationService: AuthenticationService + ) { + // redirect to home if already logged in + if (this.authenticationService.currentUserValue) { + this.router.navigate(['/']); + } + } + + ngOnInit() { + this.loginForm = this.formBuilder.group({ + username: ['', Validators.required], + password: ['', Validators.required] + }); + + // get return url from route parameters or default to '/' + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; + } + + // convenience getter for easy access to form fields + get f() { return this.loginForm.controls; } + + onSubmit() { + this.submitted = true; + + // stop here if form is invalid + if (this.loginForm.invalid) { + return; + } + + this.loading = true; + this.authenticationService.login(this.f.username.value, this.f.password.value) + .pipe(first()) + .subscribe( + data => { + this.router.navigate([this.returnUrl]); + }, + error => { + this.error = error; + this.loading = false; + }); + } +} diff --git a/Front/skydivelogs-app/src/environments/environment.prod.ts b/Front/skydivelogs-app/src/environments/environment.prod.ts index 9374e8d..2b01f99 100644 --- a/Front/skydivelogs-app/src/environments/environment.prod.ts +++ b/Front/skydivelogs-app/src/environments/environment.prod.ts @@ -1,5 +1,5 @@ export const environment = { production: true, - urlApi: "https://skydivelogsapi.azurewebsites.net", + apiUrl: 'https://skydivelogsapi.azurewebsites.net', debugMode: false }; diff --git a/Front/skydivelogs-app/src/environments/environment.ts b/Front/skydivelogs-app/src/environments/environment.ts index 0457f23..787668f 100644 --- a/Front/skydivelogs-app/src/environments/environment.ts +++ b/Front/skydivelogs-app/src/environments/environment.ts @@ -5,6 +5,6 @@ export const environment = { production: false, - urlApi: "http://localhost:5000", + apiUrl: 'http://localhost:5000', debugMode: false }; diff --git a/Front/skydivelogs-app/src/interceptor/basic-auth.interceptor.ts b/Front/skydivelogs-app/src/interceptor/basic-auth.interceptor.ts new file mode 100644 index 0000000..df89505 --- /dev/null +++ b/Front/skydivelogs-app/src/interceptor/basic-auth.interceptor.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { AuthenticationService } from '../services/authentication.service'; + +@Injectable() +export class BasicAuthInterceptor implements HttpInterceptor { + constructor(private authenticationService: AuthenticationService) { } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + // add authorization header with basic auth credentials if available + const currentUser = this.authenticationService.currentUserValue; + if (currentUser && currentUser.authdata) { + request = request.clone({ + setHeaders: { + Authorization: `Basic ${currentUser.authdata}` + } + }); + } + + return next.handle(request); + } +} \ No newline at end of file diff --git a/Front/skydivelogs-app/src/services/caching-interceptor.service.ts b/Front/skydivelogs-app/src/interceptor/caching.interceptor.ts similarity index 93% rename from Front/skydivelogs-app/src/services/caching-interceptor.service.ts rename to Front/skydivelogs-app/src/interceptor/caching.interceptor.ts index acb26f0..ec44a40 100644 --- a/Front/skydivelogs-app/src/services/caching-interceptor.service.ts +++ b/Front/skydivelogs-app/src/interceptor/caching.interceptor.ts @@ -11,7 +11,7 @@ import { Observable } from 'rxjs/Observable'; import { startWith, tap } from 'rxjs/operators'; import 'rxjs/add/observable/of'; -import { RequestCache } from './request-cache.service'; +import { RequestCache } from '../services/request-cache.service'; @Injectable() export class CachingInterceptor implements HttpInterceptor { diff --git a/Front/skydivelogs-app/src/interceptor/error.interceptor.ts b/Front/skydivelogs-app/src/interceptor/error.interceptor.ts new file mode 100644 index 0000000..0f1d9f2 --- /dev/null +++ b/Front/skydivelogs-app/src/interceptor/error.interceptor.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +import { AuthenticationService } from '../services/authentication.service'; + +@Injectable() +export class ErrorInterceptor implements HttpInterceptor { + constructor(private authenticationService: AuthenticationService) { } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe(catchError(err => { + if (err.status === 401) { + // auto logout if 401 response returned from api + this.authenticationService.logout(); + location.reload(true); + } + + const error = err.error.message || err.statusText; + return throwError(error); + })) + } +} \ No newline at end of file diff --git a/Front/skydivelogs-app/src/models/user.ts b/Front/skydivelogs-app/src/models/user.ts new file mode 100644 index 0000000..2a232f8 --- /dev/null +++ b/Front/skydivelogs-app/src/models/user.ts @@ -0,0 +1,8 @@ +export class User { + id: number; + username: string; + password: string; + firstName: string; + lastName: string; + authdata?: string; +} \ No newline at end of file diff --git a/Front/skydivelogs-app/src/services/aircraft.service.ts b/Front/skydivelogs-app/src/services/aircraft.service.ts index 2763822..b784ddc 100644 --- a/Front/skydivelogs-app/src/services/aircraft.service.ts +++ b/Front/skydivelogs-app/src/services/aircraft.service.ts @@ -8,13 +8,13 @@ import { AircraftResp, AircraftReq } from '../models/aircraft'; export class AircraftService { private readonly headers = new HttpHeaders({ - 'Access-Control-Allow-Origin': environment.urlApi + 'Access-Control-Allow-Origin': environment.apiUrl }); constructor(private http: HttpClient) { } public getListOfAircrafts(): Observable> { return this.http.get>( - `${environment.urlApi}/api/Aircraft`, + `${environment.apiUrl}/api/Aircraft`, { headers: this.headers } ); } @@ -26,7 +26,7 @@ export class AircraftService { }; this.http - .post(`${environment.urlApi}/api/Aircraft`, bodyNewAircraft, { + .post(`${environment.apiUrl}/api/Aircraft`, bodyNewAircraft, { headers: this.headers }) .subscribe(data => console.log(data)); diff --git a/Front/skydivelogs-app/src/services/auth-guard.service.ts b/Front/skydivelogs-app/src/services/auth-guard.service.ts new file mode 100644 index 0000000..3e71ce4 --- /dev/null +++ b/Front/skydivelogs-app/src/services/auth-guard.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { AuthenticationService } from './authentication.service'; + +@Injectable({ providedIn: 'root' }) +export class AuthGuardService implements CanActivate { + constructor( + private router: Router, + private authenticationService: AuthenticationService + ) { } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + const currentUser = this.authenticationService.currentUserValue; + if (currentUser) { + // logged in so return true + return true; + } + + // not logged in so redirect to login page with the return url + this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); + return false; + } +} diff --git a/Front/skydivelogs-app/src/services/authentication.service.ts b/Front/skydivelogs-app/src/services/authentication.service.ts new file mode 100644 index 0000000..4aee6f6 --- /dev/null +++ b/Front/skydivelogs-app/src/services/authentication.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { environment } from '../environments/environment'; +import { User } from '../models/user'; + + +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationService { + private currentUserSubject: BehaviorSubject; + public currentUser: Observable; + + constructor(private http: HttpClient) { + this.currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser'))); + this.currentUser = this.currentUserSubject.asObservable(); + } + + public get currentUserValue(): User { + return this.currentUserSubject.value; + } + + login(username: string, password: string) { + return this.http.post(`${environment.apiUrl}/users/authenticate`, { username, password }) + .pipe(map(user => { + // store user details and basic auth credentials in local storage to keep user logged in between page refreshes + user.authdata = window.btoa(username + ':' + password); + localStorage.setItem('currentUser', JSON.stringify(user)); + this.currentUserSubject.next(user); + return user; + })); + } + + logout() { + // remove user from local storage to log user out + localStorage.removeItem('currentUser'); + this.currentUserSubject.next(null); + } +} diff --git a/Front/skydivelogs-app/src/services/dropzone.service.ts b/Front/skydivelogs-app/src/services/dropzone.service.ts index a5c9f1d..30959fa 100644 --- a/Front/skydivelogs-app/src/services/dropzone.service.ts +++ b/Front/skydivelogs-app/src/services/dropzone.service.ts @@ -10,13 +10,13 @@ import { DropZoneResp } from '../models/dropzone'; export class DropzoneService { private readonly headers = new HttpHeaders({ - 'Access-Control-Allow-Origin': environment.urlApi + 'Access-Control-Allow-Origin': environment.apiUrl }); constructor(private http: HttpClient) { } public getListOfDropZones(): Observable> { return this.http - .get>(`${environment.urlApi}/api/DropZone`, { + .get>(`${environment.apiUrl}/api/DropZone`, { headers: this.headers }) .pipe( @@ -30,7 +30,7 @@ export class DropzoneService { public SetFavoriteDropZone(selectedDz: DropZoneResp): boolean { selectedDz.isFavorite = true; this.http - .put(`${environment.urlApi}/api/DropZone/${selectedDz.id}`, selectedDz, { + .put(`${environment.apiUrl}/api/DropZone/${selectedDz.id}`, selectedDz, { headers: this.headers }) .subscribe(data => console.log(data)); @@ -41,7 +41,7 @@ export class DropzoneService { public RemoveFavoriteDropZone(selectedDz: DropZoneResp): boolean { selectedDz.isFavorite = false; this.http - .put(`${environment.urlApi}/api/DropZone/${selectedDz.id}`, selectedDz, { + .put(`${environment.apiUrl}/api/DropZone/${selectedDz.id}`, selectedDz, { headers: this.headers }) .subscribe(data => console.log(data)); diff --git a/Front/skydivelogs-app/src/services/gear.service.ts b/Front/skydivelogs-app/src/services/gear.service.ts index 3173c50..bac7469 100644 --- a/Front/skydivelogs-app/src/services/gear.service.ts +++ b/Front/skydivelogs-app/src/services/gear.service.ts @@ -9,12 +9,12 @@ import { GearResp, GearReq } from '../models/gear'; export class GearService { private readonly headers = new HttpHeaders({ - 'Access-Control-Allow-Origin': environment.urlApi + 'Access-Control-Allow-Origin': environment.apiUrl }); constructor(private http: HttpClient) { } public getListOfGears(): Observable> { - return this.http.get>(`${environment.urlApi}/api/Gear`, { + return this.http.get>(`${environment.apiUrl}/api/Gear`, { headers: this.headers }); } @@ -38,7 +38,7 @@ export class GearService { }; this.http - .post(`${environment.urlApi}/api/Gear`, bodyNewGear, { + .post(`${environment.apiUrl}/api/Gear`, bodyNewGear, { headers: this.headers }) .subscribe(data => console.log(data)); diff --git a/Front/skydivelogs-app/src/services/jump-type.service.ts b/Front/skydivelogs-app/src/services/jump-type.service.ts index c0ba3cc..fd657d8 100644 --- a/Front/skydivelogs-app/src/services/jump-type.service.ts +++ b/Front/skydivelogs-app/src/services/jump-type.service.ts @@ -8,13 +8,13 @@ import { JumpTypeResp } from '../models/jumpType'; export class JumpTypeService { private readonly headers = new HttpHeaders({ - 'Access-Control-Allow-Origin': environment.urlApi + 'Access-Control-Allow-Origin': environment.apiUrl }); constructor(private http: HttpClient) { } public getListOfJumpTypes(): Observable> { return this.http.get>( - `${environment.urlApi}/api/JumpType`, + `${environment.apiUrl}/api/JumpType`, { headers: this.headers } ); } diff --git a/Front/skydivelogs-app/src/services/jump.service.ts b/Front/skydivelogs-app/src/services/jump.service.ts index dfed75d..a732d22 100644 --- a/Front/skydivelogs-app/src/services/jump.service.ts +++ b/Front/skydivelogs-app/src/services/jump.service.ts @@ -10,12 +10,12 @@ import { JumpResp, JumpReq } from '../models/jump'; export class JumpService { private readonly headers = new HttpHeaders({ - 'Access-Control-Allow-Origin': environment.urlApi + 'Access-Control-Allow-Origin': environment.apiUrl }); constructor(private http: HttpClient, private dateService: DateService) { } public getListOfJumps(): Observable> { - return this.http.get>(`${environment.urlApi}/api/Jump`, { + return this.http.get>(`${environment.apiUrl}/api/Jump`, { headers: this.headers }) .pipe( @@ -98,7 +98,7 @@ export class JumpService { }; this.http - .post(`${environment.urlApi}/api/Jump`, bodyNewjump, { + .post(`${environment.apiUrl}/api/Jump`, bodyNewjump, { headers: this.headers }) .subscribe(data => console.log(data)); diff --git a/Front/skydivelogs-app/src/services/stats.service.ts b/Front/skydivelogs-app/src/services/stats.service.ts index 1fee099..91adbf1 100644 --- a/Front/skydivelogs-app/src/services/stats.service.ts +++ b/Front/skydivelogs-app/src/services/stats.service.ts @@ -18,7 +18,7 @@ import { @Injectable() export class StatsService { private readonly headers = new HttpHeaders({ - 'Access-Control-Allow-Origin': environment.urlApi + 'Access-Control-Allow-Origin': environment.apiUrl }); constructor(private http: HttpClient) { } @@ -42,7 +42,7 @@ export class StatsService { private getSimpleSummary(): Observable { return this.http - .get>(`${environment.urlApi}/api/Stats/Simple`, { + .get>(`${environment.apiUrl}/api/Stats/Simple`, { headers: this.headers }) .pipe( @@ -55,7 +55,7 @@ export class StatsService { private getStatsByDz(): Observable> { return this.http - .get>(`${environment.urlApi}/api/Stats/ByDz`, { + .get>(`${environment.apiUrl}/api/Stats/ByDz`, { headers: this.headers }) .pipe( @@ -69,7 +69,7 @@ export class StatsService { private getStatsByAircraft(): Observable> { return this.http .get>( - `${environment.urlApi}/api/Stats/ByAircraft`, + `${environment.apiUrl}/api/Stats/ByAircraft`, { headers: this.headers } ) .pipe( @@ -83,7 +83,7 @@ export class StatsService { private getStatsByJumpType(): Observable> { return this.http .get>( - `${environment.urlApi}/api/Stats/ByJumpType`, + `${environment.apiUrl}/api/Stats/ByJumpType`, { headers: this.headers } ) .pipe( @@ -96,7 +96,7 @@ export class StatsService { private getStatsByGear(): Observable> { return this.http - .get>(`${environment.urlApi}/api/Stats/ByGear`, { + .get>(`${environment.apiUrl}/api/Stats/ByGear`, { headers: this.headers }) .pipe( @@ -109,7 +109,7 @@ export class StatsService { private getStatsByYear(): Observable> { return this.http - .get>(`${environment.urlApi}/api/Stats/ByYear`, { + .get>(`${environment.apiUrl}/api/Stats/ByYear`, { headers: this.headers }) .pipe( @@ -123,7 +123,7 @@ export class StatsService { private getStatsOfLastYear(): Observable { return this.http .get( - `${environment.urlApi}/api/Stats/ForLastYear`, + `${environment.apiUrl}/api/Stats/ForLastYear`, { headers: this.headers } ) .pipe( @@ -141,7 +141,7 @@ export class StatsService { private getStatsOfLastMonth(): Observable { return this.http .get( - `${environment.urlApi}/api/Stats/ForLastMonth`, + `${environment.apiUrl}/api/Stats/ForLastMonth`, { headers: this.headers } ) .pipe(