diff --git a/front/app/src/app/app.component.html b/front/app/src/app/app.component.html
index 322d6e13..e64b893e 100644
--- a/front/app/src/app/app.component.html
+++ b/front/app/src/app/app.component.html
@@ -4,6 +4,8 @@
+
diff --git a/front/app/src/app/app.module.ts b/front/app/src/app/app.module.ts
index b1fae44e..3e5798a1 100644
--- a/front/app/src/app/app.module.ts
+++ b/front/app/src/app/app.module.ts
@@ -2,6 +2,8 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons';
+import { ReactiveFormsModule } from "@angular/forms";
+import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -9,20 +11,31 @@ import { AppComponent } from './app.component';
import { SidenavComponent } from "./layout/sidenav/sidenav.component";
import { FlashmessagesComponent } from "./layout/flashmessages/flashmessages.component";
import { FlashmessagesService } from "./layout/flashmessages/flashmessages.service";
+import { LoginComponent, LogoutComponent } from "./layout/auth/auth.component";
+import { AuthService } from "./layout/auth/auth.service";
+import { AuthInterceptor } from "./layout/auth/auth.interceptor"
@NgModule({
declarations: [
AppComponent,
SidenavComponent,
- FlashmessagesComponent
+ FlashmessagesComponent,
+ LoginComponent,
+ LogoutComponent
],
imports: [
BrowserModule,
AppRoutingModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
+ ReactiveFormsModule,
+ HttpClientModule,
+ ],
+ providers: [
+ FlashmessagesService,
+ AuthService,
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
],
- providers: [FlashmessagesService],
bootstrap: [AppComponent]
})
export class AppModule { }
diff --git a/front/app/src/app/layout/auth/auth.component.ts b/front/app/src/app/layout/auth/auth.component.ts
new file mode 100644
index 00000000..05219468
--- /dev/null
+++ b/front/app/src/app/layout/auth/auth.component.ts
@@ -0,0 +1,103 @@
+import {Component, ElementRef, EventEmitter, Output, ViewChild} from "@angular/core";
+import {FormBuilder, FormGroup, Validators} from "@angular/forms";
+import {Router} from "@angular/router";
+import {AuthService} from "./auth.service";
+import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
+
+@Component({
+ selector: 'login',
+ template: `
+
+
+
+
+
+ `
+})
+export class LoginComponent {
+ @Output() loginSuccess = new EventEmitter
()
+
+ @ViewChild('loginModal')
+ loginModal!: ElementRef;
+
+ form: FormGroup;
+
+ constructor(private fb:FormBuilder,
+ private authService: AuthService,
+ private modalService: NgbModal
+
+ ) {
+ this.form = this.fb.group({
+ username: ['',Validators.required],
+ password: ['',Validators.required]
+ });
+
+ this.authService.onAuthenticationRequired$.subscribe((required: boolean) => {
+ if (required) {
+ this.openLoginModal()
+ }
+ })
+ }
+
+ openLoginModal() {
+ this.modalService.open(this.loginModal, { ariaLabelledBy: 'modal-basic-title' }).result.then(
+ );
+ }
+
+ login() {
+ const val = this.form.value;
+
+ if (val.username && val.password) {
+ this.authService.login(val.username, val.password)
+ .subscribe((answer: string) => {
+ this.modalService.dismissAll();
+ this.loginSuccess.emit(answer)
+ }
+ );
+ }
+ }
+}
+
+@Component({
+ selector: 'logout',
+ template: `
+
+ `
+})
+export class LogoutComponent {
+ @Output() logoutSuccess = new EventEmitter()
+
+ constructor(private authService: AuthService) {
+ }
+
+ logout() {
+ this.authService.logout()
+ .subscribe(() => {
+ this.logoutSuccess.emit()
+ }
+ );
+ }
+}
diff --git a/front/app/src/app/layout/auth/auth.interceptor.ts b/front/app/src/app/layout/auth/auth.interceptor.ts
new file mode 100644
index 00000000..ca358b12
--- /dev/null
+++ b/front/app/src/app/layout/auth/auth.interceptor.ts
@@ -0,0 +1,34 @@
+import { Injectable } from "@angular/core";
+import { HttpErrorResponse, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
+
+import { Observable, throwError } from 'rxjs';
+import { catchError } from "rxjs/operators";
+
+import { AuthService } from './auth.service';
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+
+ constructor(private auth: AuthService) {}
+
+ intercept(req: HttpRequest, next: HttpHandler) {
+ const authToken = this.auth.getAuthorizationToken();
+ let authReq: HttpRequest;
+ if (authToken) {
+ authReq = req.clone({
+ headers: req.headers.set('Authorization', authToken)
+ });
+ } else {
+ authReq = req;
+ }
+
+ return next.handle(authReq).pipe(
+ catchError((error: HttpErrorResponse) => {
+ if (error.status && error.status == 401) {
+ this.auth.requestAuth();
+ }
+ return throwError(() => error);
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/front/app/src/app/layout/auth/auth.service.ts b/front/app/src/app/layout/auth/auth.service.ts
new file mode 100644
index 00000000..0946a45d
--- /dev/null
+++ b/front/app/src/app/layout/auth/auth.service.ts
@@ -0,0 +1,63 @@
+import { Injectable } from "@angular/core";
+import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
+
+import { map } from "rxjs/operators";
+import { Subject } from "rxjs";
+
+import { FlashmessagesService } from "../flashmessages/flashmessages.service";
+
+export interface token_answer {
+ access_token: string,
+ token_type: string
+}
+
+@Injectable()
+export class AuthService {
+ private access_token: string | null;
+
+ private authenticationRequired = new Subject();
+ onAuthenticationRequired$ = this.authenticationRequired.asObservable();
+
+ constructor(private http: HttpClient,
+ private flashMessage: FlashmessagesService) {
+ this.access_token = localStorage.getItem('authtoken')
+ }
+
+ login(username:string, password:string ) {
+ const body = new HttpParams()
+ .set('username', username)
+ .set('password', password);
+
+ return this.http.post('/api/v1/auth/login', body.toString(),
+ {
+ headers: new HttpHeaders()
+ .set('Content-Type', 'application/x-www-form-urlencoded')
+ }).pipe(map( v => {
+ localStorage.setItem('authtoken', v.access_token);
+ this.access_token = v.access_token
+ this.flashMessage.success('Login successful. Welcome ' + username);
+ return username
+ } ));
+ }
+
+ logout() {
+ return this.http.post('/api/v1/auth/logout', '',
+ {
+ headers: new HttpHeaders()
+ }).pipe(map( v => {
+ localStorage.removeItem('authtoken');
+ this.access_token = null;
+ this.flashMessage.success('Logout successful. Goodbye');
+ } ));
+ }
+
+ getAuthorizationToken() {
+ return `Bearer ${this.access_token}`;
+ }
+
+ requestAuth() {
+ localStorage.removeItem('authtoken');
+ this.access_token = null;
+ this.authenticationRequired.next(true);
+ }
+}
\ No newline at end of file