Switch from RabbitMQ to server-sent events

(not fully working yet)
This commit is contained in:
2021-05-03 20:58:40 +02:00
parent 533749c7c8
commit daa7209742
38 changed files with 22158 additions and 672 deletions

17692
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,6 @@
"moment": "^2.29.1",
"ng2-charts": "^2.4.2",
"ngx-mapbox-gl": "^4.8.1",
"ngx-mqtt": "^7.0.14",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"

View File

@@ -1,5 +1,4 @@
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
import { resolve } from 'dns';
import { APIService, UserType } from '../api.service';
import { AlertService } from '../_alert/alert.service';

View File

@@ -1,12 +1,11 @@
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MqttService } from 'ngx-mqtt';
import { BehaviorSubject } from 'rxjs';
import * as moment from 'moment';
import { error } from 'protractor';
import { AlertService } from './_alert/alert.service';
import { AlertService, AlertType } from './_alert/alert.service';
/*
* ==========================
* DEFINITION OF TYPE
*/
export interface ILogin {
@@ -44,7 +43,7 @@ export enum UserType {
export interface IUser {
_id: string;
name: string;
brokerToken: string;
eventToken: string;
type: UserType;
lastLogin: Date;
twoFASecret?: string;
@@ -84,8 +83,13 @@ export interface INotification extends Document {
user: IUser;
}
interface CustomEvent extends Event {
data: any;
}
/*
* END OF THE DEFINITION OF TYPE
* END OF THE TYPE DEFINITION
* ==========================
*/
@Injectable({
@@ -94,9 +98,9 @@ export interface INotification extends Document {
export class APIService {
private token: string;
private events: EventSource | undefined;
username: string;
rabbitmq: any;
// Passthough data (not useful for api but a way for components to share data)
showFilter = true;
@@ -112,7 +116,7 @@ export class APIService {
user: IUser = {
_id: '',
name: '',
brokerToken: '',
eventToken: '',
lastLogin: new Date(2020, 3, 1),
type: UserType.GUEST,
createdAt: new Date(),
@@ -128,42 +132,66 @@ export class APIService {
API_ENDPOINT = 'http://192.168.178.26:8040';
constructor(private httpClient: HttpClient, private mqtt: MqttService, private alert: AlertService) { }
constructor(private httpClient: HttpClient, private alert: AlertService) { }
private mqttInit(): void {
// Connect with RabbitMQ after we received our user information
this.mqtt.connect({
hostname: '192.168.178.26',
port: 15675,
protocol: 'ws',
path: '/ws',
username: this.user.name,
password: this.user.brokerToken
});
/**
* This functions opens a new http connection that stays open, forever, to receive events from the backend.
*/
async subscribeToEvents() {
let shownError = false;
this.mqtt.observe(this.user._id).subscribe(async message => {
if (message !== undefined || message !== null) {
const obj = JSON.parse(message.payload.toString());
console.log('Received message:', obj);
// If there is already a event, close it.
if (this.events !== undefined) {
this.events.close();
}
if (obj.type === 'beat') {
this.events = new EventSource(`${this.API_ENDPOINT}/user/events?token=${this.user.eventToken}`);
this.events.onopen = event => {
console.info('Connection to event stream is open. Awaiting incoming events ...');
shownError = false;
};
this.events.onerror = error => {
if (shownError) return;
console.error('Connection to event stream has failed! Error: ' + error);
this.alert.error('Could not subscribe to events', 'Events');
shownError = true;
}
this.events.onmessage = async event => {
const jsonData = JSON.parse(event.data);
console.debug(`[SSE] ${event.type}: ${event.data}`);
switch (event.type) {
case 'beat':
if (this.beats !== undefined) {
this.beats.push(obj);
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
this.beats.push(jsonData);
this.beatsEvent.next([jsonData]); // We just push one, so the map doesn't has to rebuild everything from scratch.
this.beatStats.totalBeats++;
}
} else if (obj.type === 'phone_available') {
this.alert.dynamic(`Device ${obj.displayName} is now online`, obj.severity, 'Device');
} else if (obj.type === 'phone_register') {
console.debug('Received count:', jsonData);
break;
case 'message':
this.alert.info(event.data.message, 'SSE');
break;
case 'phone_available':
this.alert.dynamic(`Device ${jsonData.displayName} is now online`, jsonData.severity, 'Device');
break;
case 'phone_register':
await this.getPhones();
this.alert.dynamic(`New device "${obj.displayName}"`, obj.severity, 'New device');
} else if (obj.type === 'phone_alive') {
this.alert.dynamic('Device is now active', obj.severity, obj.displayName);
} else if (obj.type === 'phone_dead') {
this.alert.dynamic('Device is now offline', obj.severity, obj.displayName);
}
this.alert.dynamic(`New device "${jsonData.displayName}"`, jsonData.severity, 'New device');
break;
case 'phone_alive':
this.alert.dynamic('Device is now active', jsonData.severity, jsonData.displayName);
break;
case 'phone_dead':
this.alert.dynamic('Device is now offline', jsonData.severity, jsonData.displayName);
break;
}
});
};
}
/*
@@ -171,6 +199,7 @@ export class APIService {
*/
async login(username: string, password: string): Promise<ILogin> {
return new Promise<ILogin>(async (resolve, reject) => {
if (this.token !== undefined) reject('User is already logged in.');
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
.subscribe(async token => {
console.log(token);
@@ -181,9 +210,9 @@ export class APIService {
await this.getUserInfo();
await this.getNotifications();
this.mqttInit();
this.subscribeToEvents();
await this.getBeats();
await this.getBeats({ from: moment().startOf('day').unix(), to: moment().unix() });
await this.getBeatStats();
this.loginEvent.next(true);
this.alert.success('Login successful', 'Login', { duration: 2 });
@@ -385,7 +414,7 @@ export class APIService {
});
}
/* HELPER CLASSES */
/* HELPER FUNCTIONS */
degreesToRadians(degrees: number): number {
return degrees * Math.PI / 180;
}
@@ -429,7 +458,14 @@ export class APIService {
return this.beats[this.beats.length - 1];
}
/**
* @returns `true` if user is logged in and `false` if not.
*/
hasSession(): boolean {
return this.token !== undefined;
}
getToken(): string {
return this.token;
}
}

View File

@@ -22,7 +22,6 @@ export class AppComponent implements OnInit{
async ngOnInit(): Promise<void> {
await this.api.login('admin', '$1KDaNCDlyXAOg');
this.alert.error('Audio test');
return;
}
}

View File

@@ -1,4 +1,4 @@
import { HttpClientModule } from '@angular/common/http';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
@@ -14,13 +14,13 @@ import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
import { UserComponent } from './user/user.component';
import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
import { MqttModule } from 'ngx-mqtt';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { AdminComponent } from './admin/admin.component';
import { AlertComponent } from './_alert/alert/alert.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NotificationsComponent } from './notifications/notifications.component';
import { BackendInterceptor } from './interceptor';
@NgModule({
declarations: [
@@ -42,7 +42,6 @@ import { NotificationsComponent } from './notifications/notifications.component'
FontAwesomeModule,
FormsModule,
HttpClientModule,
MqttModule.forRoot({}),
NgxMapboxGLModule.withConfig({
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
}),
@@ -50,7 +49,13 @@ import { NotificationsComponent } from './notifications/notifications.component'
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
FontAwesomeModule
],
providers: [],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: BackendInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable } from 'rxjs';
import { APIService } from './api.service';
@Injectable()
export class BackendInterceptor implements HttpInterceptor {
constructor (private api: APIService) {}
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.api.hasSession()) {
console.debug('Inject token for', httpRequest.url);
return next.handle(httpRequest.clone({ setHeaders: { token: this.api.getToken() } }));
}
return next.handle(httpRequest);
}
}

View File

@@ -35,7 +35,7 @@ addEventListener('message', ({ data }) => {
const isNearPoint = isPointInRadius(
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
0.025
0.005
);
if (isNearPoint) {

View File

@@ -1,4 +1,4 @@
<mgl-map [style]="'mapbox://styles/mapbox/dark-v10'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></mgl-geojson-source>