Added Login, TTS Login, Policies

This commit is contained in:
Tom 2024-10-25 19:09:34 +00:00
parent 0c7fbf1cb9
commit 65f4172bc2
10 changed files with 275 additions and 40 deletions

2
.gitignore vendored
View File

@ -40,3 +40,5 @@ testem.log
# System files # System files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
src/environments/*

View File

@ -1,15 +1,13 @@
import { CommonModule, DatePipe, isPlatformBrowser } from '@angular/common'; import { CommonModule, DatePipe, isPlatformBrowser } from '@angular/common';
import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy } from '@angular/core'; import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy } from '@angular/core';
import { Router, RouterModule, RouterOutlet, Routes } from '@angular/router'; import { Router, RouterOutlet } from '@angular/router';
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { HermesClientService } from './hermes-client.service'; import { HermesClientService } from './hermes-client.service';
import { AuthGuard } from './shared/auth/auth.guard' import { AuthGuard } from './shared/auth/auth.guard'
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { Policy, PolicyScope } from './shared/models/policy';
import { PolicyComponent } from "./policy/policy.component"; import { PolicyComponent } from "./policy/policy.component";
import { NavigationComponent } from "./navigation/navigation.component"; import { NavigationComponent } from "./navigation/navigation.component";
import EventService from './shared/services/EventService'; import EventService from './shared/services/EventService';
import { HttpClient } from '@angular/common/http';
import { ApiAuthenticationService } from './shared/services/api/api-authentication.service'; import { ApiAuthenticationService } from './shared/services/api/api-authentication.service';
@Component({ @Component({
@ -23,13 +21,14 @@ import { ApiAuthenticationService } from './shared/services/api/api-authenticati
export class AppComponent implements OnInit, OnDestroy { export class AppComponent implements OnInit, OnDestroy {
private isBrowser: boolean; private isBrowser: boolean;
private ngZone: NgZone; private ngZone: NgZone;
private subscription: Subscription | undefined; private subscriptions: Subscription[];
pipe = new DatePipe('en-US') pipe = new DatePipe('en-US')
constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private http: HttpClient, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) { constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {
this.ngZone = ngZone; this.ngZone = ngZone;
this.isBrowser = isPlatformBrowser(this.platformId) this.isBrowser = isPlatformBrowser(this.platformId);
this.subscriptions = [];
} }
ngOnInit(): void { ngOnInit(): void {
@ -38,13 +37,30 @@ export class AppComponent implements OnInit, OnDestroy {
this.auth.update(); this.auth.update();
const rand = Math.random() * 100000 | 0; this.addSubscription(this.events.listen('logoff', (message) => {
this.client.subscribe(4, (data) => console.log("Request ack received", rand, data)); localStorage.removeItem('jwt');
if (!document.location.href.includes('/login')) {
this.router.navigate(['/login?warning=' + message]);
}
}));
this.addSubscription(this.events.listen('login', (_) => {
if (document.location.href.includes('/login')) {
this.router.navigate(['/tts-login']);
}
}));
this.client.connect(); this.client.connect();
this.ngZone.runOutsideAngular(() => setInterval(() => this.client.heartbeat(), 15000)); this.ngZone.runOutsideAngular(() => setInterval(() => this.client.heartbeat(), 15000));
} }
ngOnDestroy() { ngOnDestroy() {
for (let s of this.subscriptions) {
s.unsubscribe();
}
}
private addSubscription(s: Subscription) {
this.subscriptions.push(s);
} }
} }

View File

@ -1,6 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Subscription } from 'rxjs';
import { HermesSocketService } from './hermes-socket.service'; import { HermesSocketService } from './hermes-socket.service';
import EventService from './shared/services/EventService'; import EventService from './shared/services/EventService';
@ -39,6 +38,15 @@ export class HermesClientService {
return this.listen(); return this.listen();
} }
public disconnect() {
if (!this.connected)
return;
this.socket.close();
this.connected = false;
this.events.emit('tts_logoff', null);
}
private send(op: number, data: any) { private send(op: number, data: any) {
if (op != 0) if (op != 0)
console.log("TX:", data); console.log("TX:", data);
@ -61,6 +69,54 @@ export class HermesClientService {
}); });
} }
public createPolicy(groupId: string, path: string, usage: number, timespan: number) {
if (!this.logged_in)
return;
this.send(3, {
request_id: null,
type: "create_policy",
data: {
groupId, path, count: usage, span: timespan
},
});
}
public deletePolicy(id: string) {
if (!this.logged_in)
return;
this.send(3, {
request_id: null,
type: "delete_policy",
data: {
id
},
});
}
public fetchPolicies() {
if (!this.logged_in)
return;
this.send(3, {
request_id: null,
type: "get_policies",
data: null,
});
}
public fetchPermissionsAndGroups() {
if (!this.logged_in)
return;
this.send(3, {
request_id: null,
type: "get_permissions",
data: null,
});
}
public heartbeat() { public heartbeat() {
const date = new Date() const date = new Date()
this.send(0, { this.send(0, {
@ -75,6 +131,19 @@ export class HermesClientService {
this.subscriptions[code].push(action); this.subscriptions[code].push(action);
} }
public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) {
if (!this.logged_in)
return;
this.send(3, {
request_id: null,
type: "update_policy",
data: {
id, groupId, path, count: usage, span: timespan
},
});
}
private listen() { private listen() {
return this.socket.subscribe({ return this.socket.subscribe({
next: (message: any) => { next: (message: any) => {

View File

@ -25,7 +25,7 @@ export class HermesSocketService implements OnInit {
private getNewWebSocket() { private getNewWebSocket() {
return webSocket({ return webSocket({
url: WS_ENDPOINT url: environment.WSS_ENDPOINT
}); });
} }

View File

@ -7,6 +7,7 @@ import { MatInputModule } from '@angular/material/input';
import { Policy } from '../shared/models/policy'; import { Policy } from '../shared/models/policy';
import EventService from '../shared/services/EventService'; import EventService from '../shared/services/EventService';
import { map, Observable, startWith } from 'rxjs'; import { map, Observable, startWith } from 'rxjs';
import { HermesClientService } from '../hermes-client.service';
const Policies = [ const Policies = [
{ path: "tts", description: "Anything to do with TTS" }, { path: "tts", description: "Anything to do with TTS" },
@ -48,7 +49,7 @@ export class PolicyAddFormComponent {
newPolicyName: string = ''; newPolicyName: string = '';
filteredPolicies: Observable<string[]>; filteredPolicies: Observable<string[]>;
constructor(private events: EventService) { constructor(private events: EventService, private hermes: HermesClientService) {
this.filteredPolicies = this.myControl.valueChanges.pipe( this.filteredPolicies = this.myControl.valueChanges.pipe(
startWith(''), startWith(''),
map(value => this._filter(value || '')), map(value => this._filter(value || '')),
@ -69,7 +70,7 @@ export class PolicyAddFormComponent {
} }
addNewPolicy() { addNewPolicy() {
console.log('new policy name given', this.newPolicyName) this.events.emit('addPolicy', this.newPolicyName);
this.events.emit('addPolicy', this.newPolicyName) this.newPolicyName = "";
} }
} }

View File

@ -6,26 +6,47 @@
<!-- Position Column --> <!-- Position Column -->
<ng-container matColumnDef="path"> <ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef>Path</th> <th mat-header-cell *matHeaderCellDef>Path</th>
<td mat-cell *matCellDef="let policy">
{{policy.path}}
</td>
</ng-container>
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef>Group</th>
<td mat-cell *matCellDef="let policy"> <td mat-cell *matCellDef="let policy">
@if (policy.editing) { @if (policy.editing) {
<input type="text" [(ngModel)]="policy.path" /> <input type="text" [(ngModel)]="policy.temp_group_name" />
} }
@if (!policy.editing) { @if (!policy.editing) {
{{policy.path}} {{groups[policy.group_id].name}}
} }
</td> </td>
</ng-container> </ng-container>
<!-- Name Column --> <!-- Name Column -->
<ng-container matColumnDef="usage"> <ng-container matColumnDef="usage">
<th mat-header-cell *matHeaderCellDef> Usage </th> <th mat-header-cell *matHeaderCellDef>Usage per span</th>
<td mat-cell *matCellDef="let policy"> {{policy.name}} </td> <td mat-cell *matCellDef="let policy">
@if (policy.editing) {
<input type="number" [(ngModel)]="policy.usage" (keypress)="($event.charCode >= 48 && $event.charCode < 58)" />
}
@if (!policy.editing) {
{{policy.usage}}
}
</td>
</ng-container> </ng-container>
<!-- Weight Column --> <!-- Weight Column -->
<ng-container matColumnDef="span"> <ng-container matColumnDef="span">
<th mat-header-cell *matHeaderCellDef> Span </th> <th mat-header-cell *matHeaderCellDef>Span (ms)</th>
<td mat-cell *matCellDef="let policy"> {{policy.weight}} </td> <td mat-cell *matCellDef="let policy">
@if (policy.editing) {
<input type="number" [(ngModel)]="policy.span" (keypress)="($event.charCode >= 48 && $event.charCode < 58)" />
}
@if (!policy.editing) {
{{policy.span}}
}
</td>
</ng-container> </ng-container>
<!-- Symbol Column --> <!-- Symbol Column -->

View File

@ -5,6 +5,7 @@ import EventService from '../shared/services/EventService';
import { Policy } from '../shared/models/policy'; import { Policy } from '../shared/models/policy';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { HermesClientService } from '../hermes-client.service';
@Component({ @Component({
selector: 'policy-table', selector: 'policy-table',
@ -15,23 +16,78 @@ import { FormsModule } from '@angular/forms';
}) })
export class PolicyTableComponent implements OnInit, OnDestroy { export class PolicyTableComponent implements OnInit, OnDestroy {
@Input() policies: Policy[] = [] @Input() policies: Policy[] = []
displayedColumns = ['path', 'usage', 'span', 'actions'] displayedColumns = ['path', 'group', 'usage', 'span', 'actions']
groups: { [id: string]: { id: string, name: string, priority: number } }
@ViewChild(MatTable) table: MatTable<Policy>; @ViewChild(MatTable) table: MatTable<Policy>;
private subscription: Subscription | undefined; private subscription: Subscription | undefined;
constructor(private events: EventService) { constructor(private events: EventService, private hermes: HermesClientService) {
this.table = {} as MatTable<Policy>; this.table = {} as MatTable<Policy>;
this.groups = {};
} }
ngOnInit(): void { ngOnInit(): void {
this.subscription = this.events.listen('addPolicy', (payload) => { this.subscription = this.events.listen('addPolicy', (payload) => {
console.log('adding policy', payload); if (!payload)
this.policies.push(new Policy(payload, 1, 5000, true, true)); return;
console.log(this.policies); if (this.policies.map(p => p.path).includes(payload)) {
return;
}
this.policies.push(new Policy("", "", payload, 1, 5000, "", true, true));
this.table.renderRows(); this.table.renderRows();
}); });
this.hermes.subscribe(4, (response: any) => {
console.log('request received: ', response);
if (response.request.type == "get_policies") {
for (let policy of response.data) {
this.policies.push(new Policy(policy.id, policy.group_id, policy.path, policy.usage, policy.span, "", false, false));
}
this.table.renderRows();
} else if (response.request.type == "create_policy") {
console.log("create policy", response);
const policy = this.policies.find(p => this.groups[response.data.group_id].name == p.temp_group_name && p.path == response.data.path);
if (policy == null) {
this.policies.push(new Policy(response.data.id, response.data.group_id, response.data.path, response.data.usage, response.data.span));
} else {
policy.id = response.data.id;
policy.group_id = response.data.group_id;
policy.editing = false;
policy.isNew = false;
}
this.table.renderRows();
} else if (response.request.type == "update_policy") {
console.log("update policy", response);
const policy = this.policies.find(p => p.id == response.data.id);
if (policy == null) {
this.policies.push(new Policy(response.data.id, response.data.group_id, response.data.path, response.data.usage, response.data.span));
} else {
policy.id = response.data.id;
policy.group_id = response.data.group_id;
policy.editing = false;
policy.isNew = false;
}
this.table.renderRows();
} else if (response.request.type == "delete_policy") {
console.log('delete policy', response.request.data.id);
const policy = this.policies.find(p => p.id == response.request.data.id);
if (!policy) {
console.log('Could not find the policy by id. Already deleted.');
return;
}
const index = this.policies.indexOf(policy);
if (index >= 0) {
this.policies.splice(index, 1);
this.table.renderRows();
}
} else if (response.request.type == "get_permissions") {
this.groups = Object.assign({}, ...response.data.groups.map((g: any) => ({ [g.id]: g })));
console.log(this.groups);
}
});
this.hermes.fetchPolicies();
this.hermes.fetchPermissionsAndGroups();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -40,27 +96,83 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
} }
cancel(policy: Policy) { cancel(policy: Policy) {
if (!policy.editing)
return;
policy.path = policy.old_path ?? '';
policy.usage = policy.old_usage ?? 1;
policy.span = policy.old_span ?? 5000;
policy.old_path = undefined;
policy.old_span = undefined;
policy.old_usage = undefined;
policy.editing = false; policy.editing = false;
this.table.renderRows();
} }
delete(policy: Policy) { delete(policy: Policy) {
const index = this.policies.indexOf(policy); this.hermes.deletePolicy(policy.id);
if (index >= 0) {
this.policies.splice(index, 1);
this.table.renderRows();
}
} }
edit(policy: Policy) { edit(policy: Policy) {
console.log('prior', policy.editing) policy.old_path = policy.path;
policy.old_span = policy.span;
policy.old_usage = policy.usage;
policy.temp_group_name = this.groups[policy.group_id].name
policy.editing = true; policy.editing = true;
} }
save(policy: Policy) { save(policy: Policy) {
policy.editing = false; if (!policy.temp_group_name) {
policy.isNew = false; console.log('group must be valid.');
this.table.renderRows(); return;
}
const group = Object.values(this.groups).find(g => g.name);
if (group == null) {
console.log('group does not exist.');
return;
}
if (isNaN(policy.usage)) {
console.log('usage must be a whole number.');
return;
}
if (policy.usage < 1 || policy.usage > 99) {
console.error('usage must be between 1 and 99.');
return;
}
if (policy.usage % 1.0 != 0) {
console.error('usage must be a whole number.');
return;
}
if (isNaN(policy.span)) {
console.log('span must be a whole number.');
return;
}
if (policy.span < 1000 || policy.span > 1800000) {
console.error('span must be between 1 and 1800000.');
return;
}
if (policy.span % 1.0 != 0) {
console.error('span must be a whole number.');
return;
}
let group_id = policy?.group_id;
for (let groupId in this.groups) {
if (this.groups[groupId].name == policy?.temp_group_name) {
group_id = groupId;
break;
}
}
if (policy?.temp_group_name != this.groups[group_id].name) {
console.log('no group found.');
return;
}
if (policy.isNew) {
this.hermes.createPolicy(group.id, policy.path, policy.usage, policy.span);
} else {
this.hermes.updatePolicy(policy.id, group.id, policy.path, policy.usage, policy.span);
}
} }
} }

View File

@ -4,7 +4,11 @@ export enum PolicyScope {
} }
export class Policy { export class Policy {
constructor(public path: string, public usage: number, public span: number, public editing: boolean = false, public isNew: boolean = false) { public old_path: string|undefined;
public old_usage: number|undefined;
public old_span: number|undefined;
constructor(public id: string, public group_id: string, public path: string, public usage: number, public span: number, public temp_group_name: string = "", public editing: boolean = false, public isNew: boolean = false) {
} }
} }

View File

@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import EventService from '../EventService';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -8,7 +9,7 @@ export class ApiAuthenticationService {
private authenticated: boolean; private authenticated: boolean;
private lastCheck: Date; private lastCheck: Date;
constructor(private http: HttpClient) { constructor(private http: HttpClient, private events: EventService) {
this.authenticated = false; this.authenticated = false;
this.lastCheck = new Date(); this.lastCheck = new Date();
} }
@ -36,7 +37,16 @@ export class ApiAuthenticationService {
} }
private updateAuthenticated(value: boolean) { private updateAuthenticated(value: boolean) {
const previous = this.authenticated;
this.authenticated = value; this.authenticated = value;
this.lastCheck = new Date(); this.lastCheck = new Date();
if (previous != value) {
if (value) {
this.events.emit('login', null);
} else {
this.events.emit('logoff', null);
}
}
} }
} }

View File

@ -32,7 +32,7 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
headers: { headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt') 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
} }
}).subscribe((data: any) => this.api_keys = data.map((d: any) => d.id)) }).subscribe((data: any) => this.api_keys = data.map((d: any) => d.id));
this.subscription = this.events.listen('tts_login_ack', _ => { this.subscription = this.events.listen('tts_login_ack', _ => {
if (document.location.href.includes('/tts-login')) { if (document.location.href.includes('/tts-login')) {