diff --git a/.gitignore b/.gitignore index cc7b141..013a731 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ testem.log # System files .DS_Store Thumbs.db + +src/environments/* \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index d816890..20620ad 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,15 +1,13 @@ import { CommonModule, DatePipe, isPlatformBrowser } from '@angular/common'; 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 { HermesClientService } from './hermes-client.service'; import { AuthGuard } from './shared/auth/auth.guard' import { Subscription } from 'rxjs'; -import { Policy, PolicyScope } from './shared/models/policy'; import { PolicyComponent } from "./policy/policy.component"; import { NavigationComponent } from "./navigation/navigation.component"; import EventService from './shared/services/EventService'; -import { HttpClient } from '@angular/common/http'; import { ApiAuthenticationService } from './shared/services/api/api-authentication.service'; @Component({ @@ -23,13 +21,14 @@ import { ApiAuthenticationService } from './shared/services/api/api-authenticati export class AppComponent implements OnInit, OnDestroy { private isBrowser: boolean; private ngZone: NgZone; - private subscription: Subscription | undefined; + private subscriptions: Subscription[]; 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.isBrowser = isPlatformBrowser(this.platformId) + this.isBrowser = isPlatformBrowser(this.platformId); + this.subscriptions = []; } ngOnInit(): void { @@ -38,13 +37,30 @@ export class AppComponent implements OnInit, OnDestroy { this.auth.update(); - const rand = Math.random() * 100000 | 0; - this.client.subscribe(4, (data) => console.log("Request ack received", rand, data)); - + this.addSubscription(this.events.listen('logoff', (message) => { + 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.ngZone.runOutsideAngular(() => setInterval(() => this.client.heartbeat(), 15000)); } ngOnDestroy() { + for (let s of this.subscriptions) { + s.unsubscribe(); + } + } + + private addSubscription(s: Subscription) { + this.subscriptions.push(s); } } \ No newline at end of file diff --git a/src/app/hermes-client.service.ts b/src/app/hermes-client.service.ts index a055f11..eef3710 100644 --- a/src/app/hermes-client.service.ts +++ b/src/app/hermes-client.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; import { DatePipe } from '@angular/common'; -import { Subscription } from 'rxjs'; import { HermesSocketService } from './hermes-socket.service'; import EventService from './shared/services/EventService'; @@ -39,6 +38,15 @@ export class HermesClientService { 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) { if (op != 0) 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() { const date = new Date() this.send(0, { @@ -75,6 +131,19 @@ export class HermesClientService { 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() { return this.socket.subscribe({ next: (message: any) => { diff --git a/src/app/hermes-socket.service.ts b/src/app/hermes-socket.service.ts index 5e511e2..8a44de5 100644 --- a/src/app/hermes-socket.service.ts +++ b/src/app/hermes-socket.service.ts @@ -25,7 +25,7 @@ export class HermesSocketService implements OnInit { private getNewWebSocket() { return webSocket({ - url: WS_ENDPOINT + url: environment.WSS_ENDPOINT }); } diff --git a/src/app/policy-add-form/policy-add-form.component.ts b/src/app/policy-add-form/policy-add-form.component.ts index 0e69cac..2cc163f 100644 --- a/src/app/policy-add-form/policy-add-form.component.ts +++ b/src/app/policy-add-form/policy-add-form.component.ts @@ -7,6 +7,7 @@ import { MatInputModule } from '@angular/material/input'; import { Policy } from '../shared/models/policy'; import EventService from '../shared/services/EventService'; import { map, Observable, startWith } from 'rxjs'; +import { HermesClientService } from '../hermes-client.service'; const Policies = [ { path: "tts", description: "Anything to do with TTS" }, @@ -48,7 +49,7 @@ export class PolicyAddFormComponent { newPolicyName: string = ''; filteredPolicies: Observable; - constructor(private events: EventService) { + constructor(private events: EventService, private hermes: HermesClientService) { this.filteredPolicies = this.myControl.valueChanges.pipe( startWith(''), map(value => this._filter(value || '')), @@ -69,7 +70,7 @@ export class PolicyAddFormComponent { } addNewPolicy() { - console.log('new policy name given', this.newPolicyName) - this.events.emit('addPolicy', this.newPolicyName) + this.events.emit('addPolicy', this.newPolicyName); + this.newPolicyName = ""; } } diff --git a/src/app/policy-table/policy-table.component.html b/src/app/policy-table/policy-table.component.html index d4720a8..d43dd39 100644 --- a/src/app/policy-table/policy-table.component.html +++ b/src/app/policy-table/policy-table.component.html @@ -6,26 +6,47 @@ Path + + {{policy.path}} + + + + + Group @if (policy.editing) { - + } @if (!policy.editing) { - {{policy.path}} + {{groups[policy.group_id].name}} } - Usage - {{policy.name}} + Usage per span + + @if (policy.editing) { + + } + @if (!policy.editing) { + {{policy.usage}} + } + - Span - {{policy.weight}} + Span (ms) + + @if (policy.editing) { + + } + @if (!policy.editing) { + {{policy.span}} + } + diff --git a/src/app/policy-table/policy-table.component.ts b/src/app/policy-table/policy-table.component.ts index 4ee3beb..11e7024 100644 --- a/src/app/policy-table/policy-table.component.ts +++ b/src/app/policy-table/policy-table.component.ts @@ -5,6 +5,7 @@ import EventService from '../shared/services/EventService'; import { Policy } from '../shared/models/policy'; import { Subscription } from 'rxjs'; import { FormsModule } from '@angular/forms'; +import { HermesClientService } from '../hermes-client.service'; @Component({ selector: 'policy-table', @@ -15,23 +16,78 @@ import { FormsModule } from '@angular/forms'; }) export class PolicyTableComponent implements OnInit, OnDestroy { @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; - private subscription: Subscription | undefined; - constructor(private events: EventService) { + constructor(private events: EventService, private hermes: HermesClientService) { this.table = {} as MatTable; + this.groups = {}; } ngOnInit(): void { this.subscription = this.events.listen('addPolicy', (payload) => { - console.log('adding policy', payload); - this.policies.push(new Policy(payload, 1, 5000, true, true)); - console.log(this.policies); + if (!payload) + return; + if (this.policies.map(p => p.path).includes(payload)) { + return; + } + + this.policies.push(new Policy("", "", payload, 1, 5000, "", true, true)); 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 { @@ -40,27 +96,83 @@ export class PolicyTableComponent implements OnInit, OnDestroy { } 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; - this.table.renderRows(); } delete(policy: Policy) { - const index = this.policies.indexOf(policy); - if (index >= 0) { - this.policies.splice(index, 1); - this.table.renderRows(); - } + this.hermes.deletePolicy(policy.id); } 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; - } save(policy: Policy) { - policy.editing = false; - policy.isNew = false; - this.table.renderRows(); + if (!policy.temp_group_name) { + console.log('group must be valid.'); + 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); + } } } diff --git a/src/app/shared/models/policy.ts b/src/app/shared/models/policy.ts index ae31cb5..5cb6ce9 100644 --- a/src/app/shared/models/policy.ts +++ b/src/app/shared/models/policy.ts @@ -4,7 +4,11 @@ export enum PolicyScope { } 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) { } } \ No newline at end of file diff --git a/src/app/shared/services/api/api-authentication.service.ts b/src/app/shared/services/api/api-authentication.service.ts index 79cfa32..6a1f02b 100644 --- a/src/app/shared/services/api/api-authentication.service.ts +++ b/src/app/shared/services/api/api-authentication.service.ts @@ -1,5 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import EventService from '../EventService'; @Injectable({ providedIn: 'root' @@ -8,7 +9,7 @@ export class ApiAuthenticationService { private authenticated: boolean; private lastCheck: Date; - constructor(private http: HttpClient) { + constructor(private http: HttpClient, private events: EventService) { this.authenticated = false; this.lastCheck = new Date(); } @@ -36,7 +37,16 @@ export class ApiAuthenticationService { } private updateAuthenticated(value: boolean) { + const previous = this.authenticated; this.authenticated = value; this.lastCheck = new Date(); + + if (previous != value) { + if (value) { + this.events.emit('login', null); + } else { + this.events.emit('logoff', null); + } + } } } diff --git a/src/app/tts-login/tts-login.component.ts b/src/app/tts-login/tts-login.component.ts index 12c158b..34446e1 100644 --- a/src/app/tts-login/tts-login.component.ts +++ b/src/app/tts-login/tts-login.component.ts @@ -32,7 +32,7 @@ export class TtsLoginComponent implements OnInit, OnDestroy { headers: { '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', _ => { if (document.location.href.includes('/tts-login')) {