Frontend auth PoC

This commit is contained in:
Matteo Gheza 2023-02-23 00:25:23 +01:00
parent 7397819c00
commit 097aa485ff
12 changed files with 948 additions and 870 deletions

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@
"@angular/platform-browser-dynamic": "^15.1.5",
"@angular/router": "^15.1.5",
"@angular/service-worker": "^15.1.5",
"@asymmetrik/ngx-leaflet": "^8.1.0",
"@asymmetrik/ngx-leaflet": "^15.0.1",
"@fortawesome/fontawesome-free": "^5.15.4",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
@ -29,7 +29,7 @@
"leaflet": "^1.7.1",
"leaflet.locatecontrol": "^0.76.0",
"ngx-bootstrap": "^10.1.0",
"ngx-toastr": "^14.2.1",
"ngx-toastr": "^16.0.2",
"rxjs": "~7.4.0",
"sweetalert2": "^11.3.4",
"tslib": "^2.3.0",

View File

@ -1,6 +1,6 @@
{
"/api": {
"target": "http://127.0.0.1:8080",
"target": "http://allertavvf.test/",
"secure": false,
"changeOrigin": true
}

View File

@ -1 +1 @@
<img class="owner_image" alt="VVF" src="./api/owner_image">
<!-- <img class="owner_image" alt="VVF" src="./api/owner_image"> -->

View File

@ -10,10 +10,10 @@ export class AuthorizeGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {
}
canActivate(
checkAuthAndRedirect(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
): boolean {
console.log(this.authService, route, state);
if(this.authService.profile === undefined) {
console.log("not logged in");
@ -23,4 +23,19 @@ export class AuthorizeGuard implements CanActivate {
return true;
}
}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if(this.authService.authLoaded) {
return this.checkAuthAndRedirect(route, state);
} else {
return new Observable<boolean>((observer) => {
this.authService.authChanged.subscribe({
next: () => { observer.next(this.checkAuthAndRedirect(route, state)); }
})
});
}
}
}

View File

@ -1,72 +1,30 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '../_services/auth.service';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<string|undefined> = new BehaviorSubject<string|undefined>(undefined);
constructor(/*private auth: AuthService*/) { }
constructor(private auth: AuthService) { }
//TODO: fix interceptor and logout (client-side only) if 401 error
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<Object>> {
const token = this.auth.getToken();
let authReq = this.addHeaders(req, token);
return next.handle(authReq).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && !authReq.url.includes('login')) {
return next.handle(req);
/*
return next.handle(req).pipe(catchError(error => {
console.log(error);
if (error instanceof HttpErrorResponse && !req.url.includes('login') && !req.url.includes('me') && !req.url.includes('logout')) {
if(error.status === 400) {
return this.handle400Error(authReq, next);
this.auth.logout();
return throwError(() => error);
} else if (error.status === 401) {
this.auth.logout();
}
}
return throwError(() => new Error(error));
return throwError(() => error);
}));
*/
}
private handle400Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(undefined);
return this.auth.refreshToken().pipe(
switchMap((token: string) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token);
return next.handle(this.addHeaders(request, token));
}),
catchError((err) => {
this.isRefreshing = false;
this.auth.logout();
return throwError(() => new Error(err));
})
);
}
return this.refreshTokenSubject.pipe(
filter(token => token !== undefined),
take(1),
switchMap((token) => {
return next.handle(this.addHeaders(request, token));
})
);
}
private addHeaders(request: HttpRequest<any>, token: string|undefined) {
if (typeof token === 'string' && token.length > 10) {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${token}`
});
return request.clone({ headers });
} else {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
return request.clone({ headers });
}
}
}

View File

@ -20,15 +20,15 @@
<owner-image></owner-image>
<div class="text-center" *ngIf="auth.profile.hasRole('SUPER_EDITOR')">
<div class="btn-group" role="group">
<button type="button" class="btn btn-danger" (click)="addAlertFull()" [disabled]="!api?.availableUsers || api.availableUsers! < 5 || alertLoading">
<button type="button" class="btn btn-danger" (click)="addAlertFull()" [disabled]="!api.availableUsers || api.availableUsers! < 5 || alertLoading">
🚒 Richiedi squadra completa
</button>
<button type="button" class="btn btn-warning" (click)="addAlertSupport()" [disabled]="!api?.availableUsers || api.availableUsers! < 2 || alertLoading">
<button type="button" class="btn btn-warning" (click)="addAlertSupport()" [disabled]="!api.availableUsers || api.availableUsers! < 2 || alertLoading">
Richiedi squadra di supporto 🧯
</button>
</div>
</div>
<app-table [sourceType]="'list'" (changeAvailability)="changeAvailibility($event.newState, $event.user)" #table></app-table>
<!-- <app-table [sourceType]="'list'" (changeAvailability)="changeAvailibility($event.newState, $event.user)" #table></app-table> -->
<div class="text-center">
<button (click)="requestTelegramToken()" class="btn btn-md btn-success mt-3">{{ 'list.connect_telegram_bot'|translate }}</button>
</div>

View File

@ -32,7 +32,6 @@ export class ListComponent implements OnInit, OnDestroy {
private modalService: BsModalService,
private translate: TranslateService
) {
this.loadAvailability();
}
loadAvailability() {
@ -118,13 +117,18 @@ export class ListComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
/*
this.loadAvailabilityInterval = setInterval(() => {
console.log("Refreshing availability...");
this.loadAvailability();
}, 10000);
this.auth.authChanged.subscribe({
next: () => this.loadAvailability()
next: () => {
this.loadAvailability();
this.loadAvailability();
}
});
*/
}
ngOnDestroy(): void {

View File

@ -20,16 +20,6 @@ export class ApiClientService {
return this.apiRoot + endpoint;
}
public dataToParams(data: any): string {
return Object.keys(data).reduce(function (params, key) {
if(typeof data[key] === 'object') {
data[key] = JSON.stringify(data[key]);
}
params.set(key, data[key]);
return params;
}, new URLSearchParams()).toString();
}
public get(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.get(this.apiEndpoint(endpoint), {
@ -43,7 +33,7 @@ export class ApiClientService {
public post(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.post(this.apiEndpoint(endpoint), this.dataToParams(data)).subscribe({
this.http.post(this.apiEndpoint(endpoint), data).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
@ -52,7 +42,7 @@ export class ApiClientService {
public put(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.put(this.apiEndpoint(endpoint), this.dataToParams(data)).subscribe({
this.http.put(this.apiEndpoint(endpoint), data).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});

View File

@ -1,8 +1,7 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { ApiClientService } from './api-client.service';
import { Observable, Subject } from "rxjs";
import jwt_decode from 'jwt-decode';
import { Subject } from "rxjs";
export interface LoginResponse {
loginOk: boolean;
@ -14,59 +13,37 @@ export interface LoginResponse {
})
export class AuthService {
public profile: any = undefined;
private access_token: string | undefined = undefined;
public authChanged = new Subject<void>();
public authLoaded = false;
public loadProfile() {
try{
console.log("Loading profile", this.access_token);
let now = Date.now().valueOf() / 1000;
(window as any).jwt_decode = jwt_decode;
if(typeof(this.access_token) !== "string") return;
let decoded: any = jwt_decode(this.access_token);
if (typeof decoded.exp !== 'undefined' && decoded.exp < now) {
return false;
}
if (typeof decoded.nbf !== 'undefined' && decoded.nbf > now) {
return false;
}
this.profile = decoded.user_info;
this.profile.hasRole = (role: string) => {
return Object.values(this.profile.roles).includes(role);
}
console.log(this.profile);
this.authChanged.next();
return true;
} catch(e) {
console.error(e);
this.removeToken();
this.profile = undefined;
return false;
}
console.log("Loading profile data...");
return new Promise<void>((resolve, reject) => {
this.api.post("me").then((data: any) => {
this.profile = data;
this.profile.hasRole = (role: string) => {
return true;
}
this.authChanged.next();
resolve();
}).catch((e) => {
console.error(e);
this.profile = undefined;
reject();
});
});
}
constructor(private api: ApiClientService, private router: Router) {
if(localStorage.getItem("access_token") !== null) {
this.access_token = localStorage.getItem("access_token") as string;
this.loadProfile();
}
}
public setToken(value: string) {
localStorage.setItem("access_token", value);
this.access_token = value;
this.loadProfile();
}
public getToken(): string | undefined {
return this.access_token;
}
private removeToken() {
this.access_token = '';
localStorage.removeItem("access_token");
this.loadProfile().then(() => {
console.log("User is authenticated");
}).catch(() => {
console.log("User is not logged in");
}).finally(() => {
this.authLoaded = true;
});
}
public isAuthenticated() {
@ -75,34 +52,40 @@ export class AuthService {
public login(username: string, password: string) {
return new Promise<LoginResponse>((resolve) => {
this.api.post("login", {
username: username,
password: password
}).then((data: any) => {
console.log(data);
this.setToken(data.access_token);
console.log("Access token", data);
resolve({
loginOk: true,
message: data.message
});
}).catch((err) => {
let error_message = "";
if(err.status === 401) {
error_message = err.error.message;
} else if (err.status === 400) {
let error_messages = err.error.errors;
error_message = error_messages.map((val: any) => {
return `${val.msg} in ${val.param}`;
}).join(" & ");
} else if (err.status === 500) {
error_message = "Server error";
} else {
error_message = "Unknown error";
}
resolve({
loginOk: false,
message: error_message
this.api.get("csrf-cookie").then((data: any) => {
this.api.post("login", {
username: username,
password: password
}).then((data: any) => {
this.loadProfile().then(() => {
resolve({
loginOk: true,
message: data.message
});
}).catch(() => {
resolve({
loginOk: false,
message: "Unknown error"
});
});
}).catch((err) => {
let error_message = "";
if(err.status === 401) {
error_message = err.error.message;
} else if (err.status === 400) {
let error_messages = err.error.errors;
error_message = error_messages.map((val: any) => {
return `${val.msg} in ${val.param}`;
}).join(" & ");
} else if (err.status === 500) {
error_message = "Server error";
} else {
error_message = "Unknown error";
}
resolve({
loginOk: false,
message: error_message
});
});
});
})
@ -110,52 +93,29 @@ export class AuthService {
public impersonate(user_id: number): Promise<number> {
return new Promise((resolve, reject) => {
console.log("final", user_id);
this.api.post("impersonate", {
user_id: user_id
}).then((response) => {
this.setToken(response.access_token);
resolve(user_id);
}).catch((err) => {
reject();
});
});
}
public stop_impersonating(): Promise<number> {
return new Promise((resolve, reject) => {
this.api.post("stop_impersonating").then((response) => {
this.setToken(response.access_token);
resolve(response.user_id);
}).catch((err) => {
reject();
});
resolve(0);
});
}
public logout(routerDestination?: string[] | undefined) {
this.api.post("logout").then((data: any) => {
this.profile = undefined;
if(routerDestination === undefined) {
routerDestination = ["login", "list"];
}
this.router.navigate(routerDestination);
});
/*
if(this.profile.impersonating_user) {
this.stop_impersonating().then((user_id) => {
});
} else {
this.removeToken();
this.profile = undefined;
if(routerDestination === undefined) {
routerDestination = ["login", "list"];
}
this.router.navigate(routerDestination);
}
}
public refreshToken() {
return new Observable<string>((observer) => {
this.api.post("refreshToken").then((data: any) => {
this.setToken(data.token);
observer.next(data.token);
observer.complete();
}).catch((err) => {
observer.error(err);
});
});
*/
}
}

View File

@ -47,6 +47,7 @@ export class AppComponent {
}
});
/*
this.loadAlertsInterval = setInterval(() => {
console.log("Refreshing alerts...");
this.loadAlerts();
@ -56,6 +57,7 @@ export class AppComponent {
this.api.alertsChanged.subscribe(() => {
this.loadAlerts();
});
*/
}
openAlert(id: number) {

View File

@ -1,13 +1,13 @@
/* You can add global styles to this file, and also import other style files */
@import "~bootstrap/scss/bootstrap.scss";
@import "~@fortawesome/fontawesome-free/css/all.css";
@import '~ngx-toastr/toastr';
@import "node_modules/bootstrap/dist/css/bootstrap.min.css";
@import "node_modules/@fortawesome/fontawesome-free/css/all.css";
@import 'node_modules/ngx-toastr/toastr.css';
//Leaving this here because in component.scss it doesn't work really well
@import '~ngx-bootstrap/datepicker/bs-datepicker.scss';
@import '~leaflet/dist/leaflet.css';
@import '~leaflet.locatecontrol/dist/L.Control.Locate.min.css';
@import 'node_modules/ngx-bootstrap/datepicker/bs-datepicker.scss';
@import 'node_modules/leaflet/dist/leaflet.css';
@import 'node_modules/leaflet.locatecontrol/dist/L.Control.Locate.min.css';
.fa {
vertical-align: middle;