Frontend auth PoC
This commit is contained in:
parent
7397819c00
commit
097aa485ff
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"/api": {
|
||||
"target": "http://127.0.0.1:8080",
|
||||
"target": "http://allertavvf.test/",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<img class="owner_image" alt="VVF" src="./api/owner_image">
|
||||
<!-- <img class="owner_image" alt="VVF" src="./api/owner_image"> -->
|
|
@ -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)); }
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue