Début d'ajout pour la gestion du login

This commit is contained in:
Sébastien André
2020-03-11 11:22:35 +01:00
parent bf695b431c
commit 8a29fd7de9
19 changed files with 296 additions and 39 deletions

View File

@@ -17,9 +17,9 @@ import { NewGearComponent } from './new-gear/new-gear.component';
import { NewDropZoneComponent } from './new-drop-zone/new-drop-zone.component'; import { NewDropZoneComponent } from './new-drop-zone/new-drop-zone.component';
import { NewJumpTypeComponent } from './new-jump-type/new-jump-type.component'; import { NewJumpTypeComponent } from './new-jump-type/new-jump-type.component';
import { DefaultComponent } from './default/default.component'; import { DefaultComponent } from './default/default.component';
import { LoginComponent } from './login/login.component';
import { DateService } from '../services/date.service'; import { DateService } from '../services/date.service';
import { AircraftService } from '../services/aircraft.service'; import { AircraftService } from '../services/aircraft.service';
import { DropzoneService } from '../services/dropzone.service'; import { DropzoneService } from '../services/dropzone.service';
import { GearService } from '../services/gear.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 { JumpTypeService } from '../services/jump-type.service';
import { StatsService } from '../services/stats.service'; import { StatsService } from '../services/stats.service';
import { ServiceComm } from '../services/service-comm.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 { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -43,19 +45,23 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RequestCache } from '../services/request-cache.service'; import { CachingInterceptor } from '../interceptor/caching.interceptor';
import { CachingInterceptor } from '../services/caching-interceptor.service'; import { BasicAuthInterceptor } from '../interceptor/basic-auth.interceptor';
import { ErrorInterceptor } from '../interceptor/error.interceptor';
const appRoutes: Routes = [ const appRoutes: Routes = [
{ path: '', component: DefaultComponent }, { path: '', component: DefaultComponent, canActivate: [AuthGuardService] },
{ path: 'summary', component: SummaryComponent }, { path: 'summary', component: SummaryComponent, canActivate: [AuthGuardService] },
{ path: 'jumps', component: ListOfJumpsComponent }, { path: 'jumps', component: ListOfJumpsComponent, canActivate: [AuthGuardService] },
{ path: 'dzs', component: ListOfDzsComponent }, { path: 'dzs', component: ListOfDzsComponent, canActivate: [AuthGuardService] },
{ path: 'newjump', component: NewJumpComponent }, { path: 'newjump', component: NewJumpComponent, canActivate: [AuthGuardService] },
{ path: 'aircrafts', component: ListOfAircraftsComponent }, { path: 'aircrafts', component: ListOfAircraftsComponent, canActivate: [AuthGuardService] },
{ path: 'jumpTypes', component: ListOfJumpTypesComponent }, { path: 'jumpTypes', component: ListOfJumpTypesComponent, canActivate: [AuthGuardService] },
{ path: 'gears', component: ListOfGearsComponent }, { path: 'gears', component: ListOfGearsComponent, canActivate: [AuthGuardService] },
{ path: 'login', component: LoginComponent },
{ path: '**', redirectTo: '' } { path: '**', redirectTo: '' }
]; ];
@@ -74,7 +80,8 @@ const appRoutes: Routes = [
NewGearComponent, NewGearComponent,
NewDropZoneComponent, NewDropZoneComponent,
NewJumpTypeComponent, NewJumpTypeComponent,
DefaultComponent DefaultComponent,
LoginComponent
], ],
imports: [ imports: [
RouterModule.forRoot( RouterModule.forRoot(
@@ -111,6 +118,8 @@ const appRoutes: Routes = [
DateService, DateService,
RequestCache, RequestCache,
// { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true } // { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: BasicAuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@@ -0,0 +1,34 @@
<div class="col-md-6 offset-md-3 mt-5">
<div class="alert alert-info">
Username: test<br />
Password: test
</div>
<div class="card">
<h4 class="card-header">Angular 8 Basic Auth Login Example</h4>
<div class="card-body">
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="username">Username</label>
<input type="text" formControlName="username" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
<div *ngIf="submitted && f.username.errors" class="invalid-feedback">
<div *ngIf="f.username.errors.required">Username is required</div>
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" formControlName="password" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
<div *ngIf="submitted && f.password.errors" class="invalid-feedback">
<div *ngIf="f.password.errors.required">Password is required</div>
</div>
</div>
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Login
</button>
<div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
</form>
</div>
</div>
</div>

View File

@@ -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<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@@ -1,5 +1,5 @@
export const environment = { export const environment = {
production: true, production: true,
urlApi: "https://skydivelogsapi.azurewebsites.net", apiUrl: 'https://skydivelogsapi.azurewebsites.net',
debugMode: false debugMode: false
}; };

View File

@@ -5,6 +5,6 @@
export const environment = { export const environment = {
production: false, production: false,
urlApi: "http://localhost:5000", apiUrl: 'http://localhost:5000',
debugMode: false debugMode: false
}; };

View File

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

View File

@@ -11,7 +11,7 @@ import { Observable } from 'rxjs/Observable';
import { startWith, tap } from 'rxjs/operators'; import { startWith, tap } from 'rxjs/operators';
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import { RequestCache } from './request-cache.service'; import { RequestCache } from '../services/request-cache.service';
@Injectable() @Injectable()
export class CachingInterceptor implements HttpInterceptor { export class CachingInterceptor implements HttpInterceptor {

View File

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

View File

@@ -0,0 +1,8 @@
export class User {
id: number;
username: string;
password: string;
firstName: string;
lastName: string;
authdata?: string;
}

View File

@@ -8,13 +8,13 @@ import { AircraftResp, AircraftReq } from '../models/aircraft';
export class AircraftService { export class AircraftService {
private readonly headers = new HttpHeaders({ private readonly headers = new HttpHeaders({
'Access-Control-Allow-Origin': environment.urlApi 'Access-Control-Allow-Origin': environment.apiUrl
}); });
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
public getListOfAircrafts(): Observable<Array<AircraftResp>> { public getListOfAircrafts(): Observable<Array<AircraftResp>> {
return this.http.get<Array<AircraftResp>>( return this.http.get<Array<AircraftResp>>(
`${environment.urlApi}/api/Aircraft`, `${environment.apiUrl}/api/Aircraft`,
{ headers: this.headers } { headers: this.headers }
); );
} }
@@ -26,7 +26,7 @@ export class AircraftService {
}; };
this.http this.http
.post(`${environment.urlApi}/api/Aircraft`, bodyNewAircraft, { .post(`${environment.apiUrl}/api/Aircraft`, bodyNewAircraft, {
headers: this.headers headers: this.headers
}) })
.subscribe(data => console.log(data)); .subscribe(data => console.log(data));

View File

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

View File

@@ -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<User>;
public currentUser: Observable<User>;
constructor(private http: HttpClient) {
this.currentUserSubject = new BehaviorSubject<User>(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<any>(`${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);
}
}

View File

@@ -10,13 +10,13 @@ import { DropZoneResp } from '../models/dropzone';
export class DropzoneService { export class DropzoneService {
private readonly headers = new HttpHeaders({ private readonly headers = new HttpHeaders({
'Access-Control-Allow-Origin': environment.urlApi 'Access-Control-Allow-Origin': environment.apiUrl
}); });
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
public getListOfDropZones(): Observable<Array<DropZoneResp>> { public getListOfDropZones(): Observable<Array<DropZoneResp>> {
return this.http return this.http
.get<Array<DropZoneResp>>(`${environment.urlApi}/api/DropZone`, { .get<Array<DropZoneResp>>(`${environment.apiUrl}/api/DropZone`, {
headers: this.headers headers: this.headers
}) })
.pipe( .pipe(
@@ -30,7 +30,7 @@ export class DropzoneService {
public SetFavoriteDropZone(selectedDz: DropZoneResp): boolean { public SetFavoriteDropZone(selectedDz: DropZoneResp): boolean {
selectedDz.isFavorite = true; selectedDz.isFavorite = true;
this.http this.http
.put(`${environment.urlApi}/api/DropZone/${selectedDz.id}`, selectedDz, { .put(`${environment.apiUrl}/api/DropZone/${selectedDz.id}`, selectedDz, {
headers: this.headers headers: this.headers
}) })
.subscribe(data => console.log(data)); .subscribe(data => console.log(data));
@@ -41,7 +41,7 @@ export class DropzoneService {
public RemoveFavoriteDropZone(selectedDz: DropZoneResp): boolean { public RemoveFavoriteDropZone(selectedDz: DropZoneResp): boolean {
selectedDz.isFavorite = false; selectedDz.isFavorite = false;
this.http this.http
.put(`${environment.urlApi}/api/DropZone/${selectedDz.id}`, selectedDz, { .put(`${environment.apiUrl}/api/DropZone/${selectedDz.id}`, selectedDz, {
headers: this.headers headers: this.headers
}) })
.subscribe(data => console.log(data)); .subscribe(data => console.log(data));

View File

@@ -9,12 +9,12 @@ import { GearResp, GearReq } from '../models/gear';
export class GearService { export class GearService {
private readonly headers = new HttpHeaders({ private readonly headers = new HttpHeaders({
'Access-Control-Allow-Origin': environment.urlApi 'Access-Control-Allow-Origin': environment.apiUrl
}); });
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
public getListOfGears(): Observable<Array<GearResp>> { public getListOfGears(): Observable<Array<GearResp>> {
return this.http.get<Array<GearResp>>(`${environment.urlApi}/api/Gear`, { return this.http.get<Array<GearResp>>(`${environment.apiUrl}/api/Gear`, {
headers: this.headers headers: this.headers
}); });
} }
@@ -38,7 +38,7 @@ export class GearService {
}; };
this.http this.http
.post(`${environment.urlApi}/api/Gear`, bodyNewGear, { .post(`${environment.apiUrl}/api/Gear`, bodyNewGear, {
headers: this.headers headers: this.headers
}) })
.subscribe(data => console.log(data)); .subscribe(data => console.log(data));

View File

@@ -8,13 +8,13 @@ import { JumpTypeResp } from '../models/jumpType';
export class JumpTypeService { export class JumpTypeService {
private readonly headers = new HttpHeaders({ private readonly headers = new HttpHeaders({
'Access-Control-Allow-Origin': environment.urlApi 'Access-Control-Allow-Origin': environment.apiUrl
}); });
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
public getListOfJumpTypes(): Observable<Array<JumpTypeResp>> { public getListOfJumpTypes(): Observable<Array<JumpTypeResp>> {
return this.http.get<Array<JumpTypeResp>>( return this.http.get<Array<JumpTypeResp>>(
`${environment.urlApi}/api/JumpType`, `${environment.apiUrl}/api/JumpType`,
{ headers: this.headers } { headers: this.headers }
); );
} }

View File

@@ -10,12 +10,12 @@ import { JumpResp, JumpReq } from '../models/jump';
export class JumpService { export class JumpService {
private readonly headers = new HttpHeaders({ private readonly headers = new HttpHeaders({
'Access-Control-Allow-Origin': environment.urlApi 'Access-Control-Allow-Origin': environment.apiUrl
}); });
constructor(private http: HttpClient, private dateService: DateService) { } constructor(private http: HttpClient, private dateService: DateService) { }
public getListOfJumps(): Observable<Array<JumpResp>> { public getListOfJumps(): Observable<Array<JumpResp>> {
return this.http.get<Array<JumpResp>>(`${environment.urlApi}/api/Jump`, { return this.http.get<Array<JumpResp>>(`${environment.apiUrl}/api/Jump`, {
headers: this.headers headers: this.headers
}) })
.pipe( .pipe(
@@ -98,7 +98,7 @@ export class JumpService {
}; };
this.http this.http
.post(`${environment.urlApi}/api/Jump`, bodyNewjump, { .post(`${environment.apiUrl}/api/Jump`, bodyNewjump, {
headers: this.headers headers: this.headers
}) })
.subscribe(data => console.log(data)); .subscribe(data => console.log(data));

View File

@@ -18,7 +18,7 @@ import {
@Injectable() @Injectable()
export class StatsService { export class StatsService {
private readonly headers = new HttpHeaders({ private readonly headers = new HttpHeaders({
'Access-Control-Allow-Origin': environment.urlApi 'Access-Control-Allow-Origin': environment.apiUrl
}); });
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
@@ -42,7 +42,7 @@ export class StatsService {
private getSimpleSummary(): Observable<SimpleSummary> { private getSimpleSummary(): Observable<SimpleSummary> {
return this.http return this.http
.get<Array<SimpleSummary>>(`${environment.urlApi}/api/Stats/Simple`, { .get<Array<SimpleSummary>>(`${environment.apiUrl}/api/Stats/Simple`, {
headers: this.headers headers: this.headers
}) })
.pipe( .pipe(
@@ -55,7 +55,7 @@ export class StatsService {
private getStatsByDz(): Observable<Array<StatsByDzResp>> { private getStatsByDz(): Observable<Array<StatsByDzResp>> {
return this.http return this.http
.get<Array<StatsByDzResp>>(`${environment.urlApi}/api/Stats/ByDz`, { .get<Array<StatsByDzResp>>(`${environment.apiUrl}/api/Stats/ByDz`, {
headers: this.headers headers: this.headers
}) })
.pipe( .pipe(
@@ -69,7 +69,7 @@ export class StatsService {
private getStatsByAircraft(): Observable<Array<StatsByAircraftResp>> { private getStatsByAircraft(): Observable<Array<StatsByAircraftResp>> {
return this.http return this.http
.get<Array<StatsByAircraftResp>>( .get<Array<StatsByAircraftResp>>(
`${environment.urlApi}/api/Stats/ByAircraft`, `${environment.apiUrl}/api/Stats/ByAircraft`,
{ headers: this.headers } { headers: this.headers }
) )
.pipe( .pipe(
@@ -83,7 +83,7 @@ export class StatsService {
private getStatsByJumpType(): Observable<Array<StatsByJumpTypeResp>> { private getStatsByJumpType(): Observable<Array<StatsByJumpTypeResp>> {
return this.http return this.http
.get<Array<StatsByJumpTypeResp>>( .get<Array<StatsByJumpTypeResp>>(
`${environment.urlApi}/api/Stats/ByJumpType`, `${environment.apiUrl}/api/Stats/ByJumpType`,
{ headers: this.headers } { headers: this.headers }
) )
.pipe( .pipe(
@@ -96,7 +96,7 @@ export class StatsService {
private getStatsByGear(): Observable<Array<StatsByGearResp>> { private getStatsByGear(): Observable<Array<StatsByGearResp>> {
return this.http return this.http
.get<Array<StatsByGearResp>>(`${environment.urlApi}/api/Stats/ByGear`, { .get<Array<StatsByGearResp>>(`${environment.apiUrl}/api/Stats/ByGear`, {
headers: this.headers headers: this.headers
}) })
.pipe( .pipe(
@@ -109,7 +109,7 @@ export class StatsService {
private getStatsByYear(): Observable<Array<StatsByYearResp>> { private getStatsByYear(): Observable<Array<StatsByYearResp>> {
return this.http return this.http
.get<Array<StatsByYearResp>>(`${environment.urlApi}/api/Stats/ByYear`, { .get<Array<StatsByYearResp>>(`${environment.apiUrl}/api/Stats/ByYear`, {
headers: this.headers headers: this.headers
}) })
.pipe( .pipe(
@@ -123,7 +123,7 @@ export class StatsService {
private getStatsOfLastYear(): Observable<StatsForLastYearResp> { private getStatsOfLastYear(): Observable<StatsForLastYearResp> {
return this.http return this.http
.get<StatsForLastYearResp>( .get<StatsForLastYearResp>(
`${environment.urlApi}/api/Stats/ForLastYear`, `${environment.apiUrl}/api/Stats/ForLastYear`,
{ headers: this.headers } { headers: this.headers }
) )
.pipe( .pipe(
@@ -141,7 +141,7 @@ export class StatsService {
private getStatsOfLastMonth(): Observable<StatsForLastMonthResp> { private getStatsOfLastMonth(): Observable<StatsForLastMonthResp> {
return this.http return this.http
.get<StatsForLastYearResp>( .get<StatsForLastYearResp>(
`${environment.urlApi}/api/Stats/ForLastMonth`, `${environment.apiUrl}/api/Stats/ForLastMonth`,
{ headers: this.headers } { headers: this.headers }
) )
.pipe( .pipe(