Added pages to see, create, modify & delete redeemable actions. User card top right with disconnect & log out. Code clean up.
This commit is contained in:
parent
11dfde9a03
commit
d595c3500e
@ -0,0 +1,94 @@
|
|||||||
|
<body>
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title-group>
|
||||||
|
<mat-card-title>{{isNew ? "New Action" : previousName}}</mat-card-title>
|
||||||
|
<mat-card-subtitle>{{isNew ? 'Creating a new action' : 'Modifying an existing action'}}</mat-card-subtitle>
|
||||||
|
</mat-card-title-group>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
<form class="grid" [formGroup]="formGroup">
|
||||||
|
<div class="item">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Redeemable Action Name</mat-label>
|
||||||
|
<input matInput type="text" formControlName="name">
|
||||||
|
@if (isNew && formGroup.get('name')?.invalid && (formGroup.get('name')?.dirty ||
|
||||||
|
formGroup.get('name')?.touched)) {
|
||||||
|
@if (formGroup.get('name')?.hasError('required')) {
|
||||||
|
<small class="error">The name is required.</small>
|
||||||
|
}
|
||||||
|
@if (formGroup.get('name')?.hasError('itemExistsInArray')) {
|
||||||
|
<small class="error">The name is already in use.</small>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Type</mat-label>
|
||||||
|
<mat-select matInput formControlName="type" (selectionChange)="action.type = $event.value">
|
||||||
|
@for (type of actionTypes; track $index) {
|
||||||
|
<mat-option value="{{type}}">{{type}}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (isNew && formGroup.get('type')?.invalid && (formGroup.get('type')?.dirty ||
|
||||||
|
formGroup.get('type')?.touched)) {
|
||||||
|
@if (formGroup.get('type')?.hasError('required')) {
|
||||||
|
<small class="error">The type is required.</small>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if (actionEntries.hasOwnProperty(action.type)) {
|
||||||
|
<section class="grid">
|
||||||
|
@for (field of actionEntries[action.type]; track $index) {
|
||||||
|
<div class="item">
|
||||||
|
@if (field.type == 'text') {
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>{{field.label}}</mat-label>
|
||||||
|
<input matInput type="text" placeholder="{{field.placeholder}}" [formControl]="field.control"
|
||||||
|
[(ngModel)]="action.data[field.key]">
|
||||||
|
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
|
||||||
|
@if (field.control.hasError('required')) {
|
||||||
|
<small class="error">This field is required.</small>
|
||||||
|
}
|
||||||
|
@if (field.control.hasError('minlength')) {
|
||||||
|
<small class="error">The value needs to be longer.</small>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
@else if (field.type == 'number') {
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>{{field.label}}</mat-label>
|
||||||
|
<input matInput type="number" [formControl]="field.control">
|
||||||
|
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
|
||||||
|
@if (field.control.hasError('required')) {
|
||||||
|
<small class="error">This field is required.</small>
|
||||||
|
}
|
||||||
|
@if (field.control.hasError('min')) {
|
||||||
|
<small class="error">The value must be higher.</small>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</mat-card-content>
|
||||||
|
|
||||||
|
<mat-card-actions class="actions" align="end">
|
||||||
|
@if (!isNew) {
|
||||||
|
<button mat-raised-button class="delete" (click)="deleteAction(action)">Delete</button>
|
||||||
|
}
|
||||||
|
<button mat-raised-button (click)="dialogRef.close()">Cancel</button>
|
||||||
|
<button mat-raised-button disabled="{{!formsDirty || !formsValidity}}" (click)="save()">Save</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
</body>
|
@ -0,0 +1,30 @@
|
|||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-auto-flow: row dense;
|
||||||
|
grid-gap: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
display: block;
|
||||||
|
color: #ba1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
background-color: #ea5151;
|
||||||
|
color: #ba1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-button ~ .mdc-button {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ActionItemEditComponent } from './action-item-edit.component';
|
||||||
|
|
||||||
|
describe('ActionItemEditComponent', () => {
|
||||||
|
let component: ActionItemEditComponent;
|
||||||
|
let fixture: ComponentFixture<ActionItemEditComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ActionItemEditComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ActionItemEditComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
267
src/app/actions/action-item-edit/action-item-edit.component.ts
Normal file
267
src/app/actions/action-item-edit/action-item-edit.component.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { createItemExistsInArrayValidator } from '../../shared/validators/item-exists-in-array';
|
||||||
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'action-item-edit',
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule
|
||||||
|
],
|
||||||
|
templateUrl: './action-item-edit.component.html',
|
||||||
|
styleUrl: './action-item-edit.component.scss'
|
||||||
|
})
|
||||||
|
export class ActionItemEditComponent implements OnInit {
|
||||||
|
readonly client = inject(HermesClientService);
|
||||||
|
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
||||||
|
readonly data = inject<{ action: RedeemableAction, actions:RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||||
|
action = this.data.action;
|
||||||
|
actions = this.data.actions;
|
||||||
|
readonly actionEntries: ({ [key: string]: any[] }) = {
|
||||||
|
'SLEEP': [
|
||||||
|
{
|
||||||
|
key: 'sleep',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Sleep (ms)',
|
||||||
|
control: new FormControl(this.action.data['sleep'], [Validators.required, Validators.min(500)]),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'APPEND_TO_FILE': [
|
||||||
|
{
|
||||||
|
key: 'file_path',
|
||||||
|
type: 'text',
|
||||||
|
label: 'File Path',
|
||||||
|
placeholder: '%userprofile%/Desktop/file.txt',
|
||||||
|
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'file_content',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Content',
|
||||||
|
placeholder: '%chatter% from %broadcaster%\'s chat says hi.',
|
||||||
|
control: new FormControl(this.action.data['file_content'], [Validators.required]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'WRITE_TO_FILE': [
|
||||||
|
{
|
||||||
|
key: 'file_path',
|
||||||
|
type: 'text',
|
||||||
|
label: 'File Path',
|
||||||
|
placeholder: '%userprofile%/Desktop/file.txt',
|
||||||
|
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'file_content',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Content',
|
||||||
|
placeholder: '%chatter% from %broadcaster%\'s chat says hi.',
|
||||||
|
control: new FormControl(this.action.data['file_content'], [Validators.required]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'OBS_TRANSFORM': [
|
||||||
|
{
|
||||||
|
key: 'scene_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Name',
|
||||||
|
placeholder: 'Main Scene',
|
||||||
|
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'scene_item_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Item Name',
|
||||||
|
placeholder: 'Item',
|
||||||
|
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'position_x',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Position X',
|
||||||
|
placeholder: 'x + 50',
|
||||||
|
control: new FormControl(this.action.data['position_x'], []),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'position_y',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Position Y',
|
||||||
|
placeholder: 'x - 166',
|
||||||
|
control: new FormControl(this.action.data['position_y'], []),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rotation',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Rotation (in degrees)',
|
||||||
|
placeholder: 'mod(x + 45, 360)',
|
||||||
|
control: new FormControl(this.action.data['rotation'], []),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'AUDIO_FILE': [
|
||||||
|
{
|
||||||
|
key: 'file_path',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Audio File Path',
|
||||||
|
placeholder: '%userprofile%/Desktop/audio.mp3',
|
||||||
|
control: new FormControl(this.action.data['file_path'], [Validators.required]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'RANDOM_TTS_VOICE': [],
|
||||||
|
'SPECIFIC_TTS_VOICE': [
|
||||||
|
{
|
||||||
|
key: 'tts_voice',
|
||||||
|
type: 'text',
|
||||||
|
label: 'TTS Voice Name',
|
||||||
|
placeholder: 'Brian',
|
||||||
|
control: new FormControl(this.action.data['tts_voice'], [Validators.required, Validators.minLength(2)]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'TOGGLE_OBS_VISIBILITY': [
|
||||||
|
{
|
||||||
|
key: 'scene_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Name',
|
||||||
|
placeholder: 'Main Scene',
|
||||||
|
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'scene_item_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Item Name',
|
||||||
|
placeholder: 'Item',
|
||||||
|
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'SPECIFIC_OBS_VISIBILITY': [
|
||||||
|
{
|
||||||
|
key: 'scene_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Name',
|
||||||
|
placeholder: 'Main Scene',
|
||||||
|
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'scene_item_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Item Name',
|
||||||
|
placeholder: 'Item',
|
||||||
|
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'obs_visible',
|
||||||
|
type: 'text-values',
|
||||||
|
label: 'Visibility',
|
||||||
|
values: ['visible', 'hidden'],
|
||||||
|
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'SPECIFIC_OBS_INDEX': [
|
||||||
|
{
|
||||||
|
key: 'scene_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Name',
|
||||||
|
placeholder: 'Main Scene',
|
||||||
|
control: new FormControl(this.action.data['scene_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'scene_item_name',
|
||||||
|
type: 'text',
|
||||||
|
label: 'OBS Scene Item Name',
|
||||||
|
placeholder: 'Item',
|
||||||
|
control: new FormControl(this.action.data['scene_item_name'], [Validators.required]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'obs_index',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Visibility',
|
||||||
|
control: new FormControl(this.action.data['scene_item_name'], [Validators.required, Validators.min(0)]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'NIGHTBOT_PLAY': [],
|
||||||
|
'VEADOTUBE_SET_STATE': [
|
||||||
|
{
|
||||||
|
key: 'state',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Veadotube State name',
|
||||||
|
placeholder: 'state #1',
|
||||||
|
control: new FormControl(this.action.data['state'], [Validators.required]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
readonly actionTypes = Object.keys(this.actionEntries);
|
||||||
|
|
||||||
|
isNew: boolean = true;
|
||||||
|
previousName: string = this.action.name;
|
||||||
|
|
||||||
|
readonly formGroup = new FormGroup({
|
||||||
|
name: new FormControl(this.action.name, [Validators.required]),
|
||||||
|
type: new FormControl(this.action.type, [Validators.required]),
|
||||||
|
});
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.isNew = this.action.name.length <= 0;
|
||||||
|
this.previousName = this.action.name;
|
||||||
|
if (!this.isNew)
|
||||||
|
this.formGroup.get('name')!.disable()
|
||||||
|
else {
|
||||||
|
this.formGroup.get('name')?.addValidators(createItemExistsInArrayValidator(this.actions, a => a.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get exists(): boolean {
|
||||||
|
return this.actions.some(a => a.name == this.action.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get formsValidity(): boolean {
|
||||||
|
return this.formGroup.valid && this.action.type in this.actionEntries
|
||||||
|
&& this.actionEntries[this.action.type].every(f => f.control.valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
get formsDirty(): boolean {
|
||||||
|
return this.formGroup.dirty || this.action.type in this.actionEntries
|
||||||
|
&& this.actionEntries[this.action.type].some(f => f.control.dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAction(action: RedeemableAction): void {
|
||||||
|
if (this.isNew)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.client.deleteRedeemableAction(action.name);
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = this.actionEntries[this.action.type];
|
||||||
|
if (fields.some(f => f.control.invalid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.action.name = this.formGroup.get('name')!.value!;
|
||||||
|
this.action.type = this.formGroup.get('type')!.value!;
|
||||||
|
this.action.data = {}
|
||||||
|
for (const entry of this.actionEntries[this.action.type]) {
|
||||||
|
this.action.data[entry.key] = entry.control.value!.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(this.action.type in this.actionEntries)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogRef.close(this.action);
|
||||||
|
}
|
||||||
|
}
|
11
src/app/actions/action-list/action-list.component.html
Normal file
11
src/app/actions/action-list/action-list.component.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<main>
|
||||||
|
@for (action of actions; track $index) {
|
||||||
|
<button type="button" class="container" (click)="modify(action)">
|
||||||
|
<span class="title">{{action.name}}</span>
|
||||||
|
<span class="subtitle">{{action.type}}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button type="button" class="container" (click)="create()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</button>
|
||||||
|
</main>
|
56
src/app/actions/action-list/action-list.component.scss
Normal file
56
src/app/actions/action-list/action-list.component.scss
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
|
||||||
|
@media (min-width:1200px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width:1650px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width:2200px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
grid-auto-flow: row dense;
|
||||||
|
grid-gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #fafafa;
|
||||||
|
width: 80%;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
& .container {
|
||||||
|
border-color: grey;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid grey;
|
||||||
|
padding: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .title {
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .subtitle {
|
||||||
|
font-size: smaller;
|
||||||
|
color: lightgrey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& article:first-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
23
src/app/actions/action-list/action-list.component.spec.ts
Normal file
23
src/app/actions/action-list/action-list.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ActionListComponent } from './action-list.component';
|
||||||
|
|
||||||
|
describe('ActionListComponent', () => {
|
||||||
|
let component: ActionListComponent;
|
||||||
|
let fixture: ComponentFixture<ActionListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ActionListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ActionListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
66
src/app/actions/action-list/action-list.component.ts
Normal file
66
src/app/actions/action-list/action-list.component.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||||
|
import { MatListModule } from '@angular/material/list';
|
||||||
|
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActionItemEditComponent } from '../action-item-edit/action-item-edit.component';
|
||||||
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'action-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule, MatFormFieldModule, MatIconModule, MatListModule],
|
||||||
|
templateUrl: './action-list.component.html',
|
||||||
|
styleUrl: './action-list.component.scss'
|
||||||
|
})
|
||||||
|
export class ActionListComponent {
|
||||||
|
@Input() actions: RedeemableAction[] = []
|
||||||
|
@Output() actionsChange = new EventEmitter<RedeemableAction>();
|
||||||
|
readonly dialog = inject(MatDialog);
|
||||||
|
readonly client = inject(HermesClientService);
|
||||||
|
opened = false;
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
this.openDialog({ user_id: '', name: '', type: '', data: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(action: RedeemableAction): void {
|
||||||
|
this.openDialog(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private openDialog(action: RedeemableAction): void {
|
||||||
|
if (this.opened)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.opened = true;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(ActionItemEditComponent, {
|
||||||
|
data: { action: {user_id: action.user_id, name: action.name, type: action.type, data: action.data }, actions: this.actions },
|
||||||
|
});
|
||||||
|
const isNewAction = action.name.length <= 0;
|
||||||
|
const requestType = isNewAction ? 'create_redeemable_action' : 'update_redeemable_action';
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((result: RedeemableAction) => {
|
||||||
|
this.opened = false;
|
||||||
|
if (!result)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.client.first((d: any) => d.op == 4 && d.d.request.type == requestType && d.d.data.name == result.name)
|
||||||
|
?.subscribe(_ => {
|
||||||
|
if (isNewAction) {
|
||||||
|
this.actionsChange.emit(result);
|
||||||
|
} else {
|
||||||
|
action.type = result.type;
|
||||||
|
action.data = result.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNewAction)
|
||||||
|
this.client.createRedeemableAction(result.name, result.type, result.data);
|
||||||
|
else
|
||||||
|
this.client.updateRedeemableAction(result.name, result.type, result.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
17
src/app/actions/actions.module.ts
Normal file
17
src/app/actions/actions.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ActionsComponent } from './actions/actions.component';
|
||||||
|
import { ActionListComponent } from './action-list/action-list.component';
|
||||||
|
import { ActionItemComponent } from './action-item/action-item.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
ActionsComponent,
|
||||||
|
ActionListComponent,
|
||||||
|
ActionItemComponent,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ActionsModule { }
|
31
src/app/actions/actions/actions.component.html
Normal file
31
src/app/actions/actions/actions.component.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<body>
|
||||||
|
<h3>Redeemable Actions</h3>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<article>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Filter</mat-label>
|
||||||
|
<mat-select (selectionChange)="onFilterChange($event.value)" value="0">
|
||||||
|
<mat-select-trigger>
|
||||||
|
<mat-icon matPrefix>filter_list</mat-icon> {{filter.name}}
|
||||||
|
</mat-select-trigger>
|
||||||
|
@for (item of filters; track $index) {
|
||||||
|
<mat-option value="{{$index}}">{{item.name}}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Search</mat-label>
|
||||||
|
<input matInput
|
||||||
|
type="text"
|
||||||
|
placeholder="Name of action"
|
||||||
|
[formControl]="searchControl"
|
||||||
|
[(ngModel)]="search">
|
||||||
|
<mat-icon matPrefix>search</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<action-list [actions]="actions" (actionsChange)="items.push($event)" />
|
||||||
|
</body>
|
23
src/app/actions/actions/actions.component.scss
Normal file
23
src/app/actions/actions/actions.component.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
body, h3 {
|
||||||
|
background-color: #fafafa;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 70%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
@media (max-width:1250px) {
|
||||||
|
display: block;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
display: flex;
|
||||||
|
justify-content:space-around;
|
||||||
|
}
|
||||||
|
}
|
23
src/app/actions/actions/actions.component.spec.ts
Normal file
23
src/app/actions/actions/actions.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ActionsComponent } from './actions.component';
|
||||||
|
|
||||||
|
describe('ActionsComponent', () => {
|
||||||
|
let component: ActionsComponent;
|
||||||
|
let fixture: ComponentFixture<ActionsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ActionsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ActionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
93
src/app/actions/actions/actions.component.ts
Normal file
93
src/app/actions/actions/actions.component.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { ActionListComponent } from "../action-list/action-list.component";
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||||
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
interface IActionFilter {
|
||||||
|
name: string
|
||||||
|
filter: (action: any) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'actions',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ActionListComponent,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule
|
||||||
|
],
|
||||||
|
templateUrl: './actions.component.html',
|
||||||
|
styleUrl: './actions.component.scss'
|
||||||
|
})
|
||||||
|
export class ActionsComponent implements OnInit {
|
||||||
|
filters: IActionFilter[] = [
|
||||||
|
{ name: 'All', filter: _ => true },
|
||||||
|
{ name: 'Local File', filter: data => data.type.includes('_FILE') },
|
||||||
|
{ name: 'Nightbot', filter: data => data.type.includes('NIGHTBOT_') },
|
||||||
|
{ name: 'OBS', filter: data => data.type.includes('OBS_') },
|
||||||
|
{ name: 'Sleep', filter: data => data.type == "SLEEP" },
|
||||||
|
{ name: 'TTS', filter: data => data.type.includes('TTS') },
|
||||||
|
{ name: 'Veadotube', filter: data => data.type.includes('VEADOTUBE') },
|
||||||
|
];
|
||||||
|
|
||||||
|
client = inject(HermesClientService);
|
||||||
|
filter = this.filters[0];
|
||||||
|
searchControl = new FormControl('');
|
||||||
|
search = '';
|
||||||
|
items: RedeemableAction[] = [];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.client.subscribeToRequests('get_redeemable_actions', d => {
|
||||||
|
this.items = d.data;
|
||||||
|
});
|
||||||
|
this.client.subscribeToRequests('create_redeemable_action', d => {
|
||||||
|
if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actions.push(d.data);
|
||||||
|
});
|
||||||
|
this.client.subscribeToRequests('update_redeemable_action', d => {
|
||||||
|
if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = this.actions.find(a => a.name == d.data.name);
|
||||||
|
if (action) {
|
||||||
|
action.type = d.data.type;
|
||||||
|
action.data = d.data.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.client.subscribeToRequests('delete_redeemable_action', d => {
|
||||||
|
// if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.items = this.actions.filter(a => a.name != d.request.data.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.fetchRedeemableActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
get actions(): RedeemableAction[] {
|
||||||
|
const searchLower = this.search.toLowerCase();
|
||||||
|
return this.items.filter(this.filter.filter)
|
||||||
|
.filter((action) => action.name.toLowerCase().includes(searchLower));
|
||||||
|
}
|
||||||
|
|
||||||
|
set actions(value) {
|
||||||
|
this.items = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange(event: any): void {
|
||||||
|
this.filter = this.filters[event];
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideHttpClient(
|
provideHttpClient(
|
||||||
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||||
console.log(req.url);
|
|
||||||
return next(req);
|
return next(req);
|
||||||
}])
|
}])
|
||||||
),
|
),
|
||||||
|
@ -6,8 +6,8 @@ import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
|
|||||||
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
|
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
|
||||||
import { FiltersComponent } from './tts-filters/filters/filters.component';
|
import { FiltersComponent } from './tts-filters/filters/filters.component';
|
||||||
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
||||||
import { ActionComponent } from './actions/action/action.component';
|
|
||||||
import { AuthVisitorGuard } from './shared/auth/auth.visitor.guard';
|
import { AuthVisitorGuard } from './shared/auth/auth.visitor.guard';
|
||||||
|
import { ActionsComponent } from './actions/actions/actions.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -22,7 +22,7 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'actions',
|
path: 'actions',
|
||||||
component: ActionComponent,
|
component: ActionsComponent,
|
||||||
canActivate: [AuthAdminGuard],
|
canActivate: [AuthAdminGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2,15 +2,15 @@ import { NgModule } from '@angular/core';
|
|||||||
import { LoginComponent } from './login/login.component';
|
import { LoginComponent } from './login/login.component';
|
||||||
import { TtsLoginComponent } from './tts-login/tts-login.component';
|
import { TtsLoginComponent } from './tts-login/tts-login.component';
|
||||||
import { ImpersonationComponent } from './impersonation/impersonation.component';
|
import { ImpersonationComponent } from './impersonation/impersonation.component';
|
||||||
|
import { UserCardComponent } from './user-card/user-card.component';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
imports: [
|
imports: [
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
TtsLoginComponent,
|
TtsLoginComponent,
|
||||||
ImpersonationComponent
|
ImpersonationComponent,
|
||||||
|
UserCardComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AuthModule { }
|
export class AuthModule { }
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
@if (isAdmin()) {
|
@if (isAdmin()) {
|
||||||
<mat-card appearance="outlined">
|
<main>
|
||||||
<mat-card-header>
|
<mat-form-field>
|
||||||
<mat-card-title> Impersonation</mat-card-title>
|
<mat-label>User to impersonate</mat-label>
|
||||||
<mat-card-subtitle>Impersonate as another user</mat-card-subtitle>
|
<mat-select (selectionChange)="onChange($event)" [(value)]="impersonated">
|
||||||
</mat-card-header>
|
<mat-option>{{getUsername()}}</mat-option>
|
||||||
<mat-card-actions>
|
@for (user of users; track user.id) {
|
||||||
<mat-form-field>
|
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
||||||
<mat-label>User to impersonate</mat-label>
|
}
|
||||||
<mat-select (selectionChange)="onChange($event)" [(value)]="impersonated">
|
</mat-select>
|
||||||
<mat-option>{{getUsername()}}</mat-option>
|
</mat-form-field>
|
||||||
@for (user of users; track user.id) {
|
</main>
|
||||||
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</mat-card-actions>
|
|
||||||
</mat-card>
|
|
||||||
}
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
@ -51,7 +51,6 @@ export class ImpersonationComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onChange(e: any) {
|
public onChange(e: any) {
|
||||||
console.log('impersonate befre', e.value);
|
|
||||||
if (!e.value) {
|
if (!e.value) {
|
||||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
<h4>TTS Login</h4>
|
<main>
|
||||||
<div class="main-div">
|
<mat-card class="main-card">
|
||||||
<mat-card class="main-card">
|
<mat-card-header class="header">
|
||||||
<mat-form-field>
|
<mat-card-title-group>
|
||||||
<mat-label>API Key</mat-label>
|
<mat-card-title>TTS Login</mat-card-title>
|
||||||
<mat-select [(value)]="selected_api_key">
|
<mat-card-subtitle>Web Access to Tom-to-Speech</mat-card-subtitle>
|
||||||
@for (key of api_keys; track key.id) {
|
</mat-card-title-group>
|
||||||
<mat-option [value]="key.id">{{key.label}}</mat-option>
|
</mat-card-header>
|
||||||
}
|
<mat-card-content class="content">
|
||||||
</mat-select>
|
<mat-form-field>
|
||||||
</mat-form-field>
|
<mat-label>API Key</mat-label>
|
||||||
<button mat-raised-button (click)="login()">Log In</button>
|
<mat-select [(value)]="selected_api_key">
|
||||||
</mat-card>
|
@for (key of api_keys; track key.id) {
|
||||||
</div>
|
<mat-option [value]="key.id">{{key.label}}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions align="end">
|
||||||
|
<button mat-raised-button (click)="login()">Log In</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
</main>
|
@ -1,14 +1,7 @@
|
|||||||
.main-div {
|
main {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
|
|
||||||
h4 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-card {
|
|
||||||
width: 20%;
|
|
||||||
}
|
}
|
@ -10,61 +10,59 @@ import { Router } from '@angular/router';
|
|||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { MatCard } from '@angular/material/card';
|
import { MatCard, MatCardModule } from '@angular/material/card';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tts-login',
|
selector: 'tts-login',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatButtonModule, MatCard, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
|
imports: [MatButtonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
|
||||||
templateUrl: './tts-login.component.html',
|
templateUrl: './tts-login.component.html',
|
||||||
styleUrl: './tts-login.component.scss'
|
styleUrl: './tts-login.component.scss'
|
||||||
})
|
})
|
||||||
export class TtsLoginComponent implements OnInit, OnDestroy {
|
export class TtsLoginComponent implements OnInit, OnDestroy {
|
||||||
api_keys: { id: string, label: string }[];
|
api_keys: { id: string, label: string }[];
|
||||||
selected_api_key: string|undefined;
|
selected_api_key: string | undefined;
|
||||||
|
|
||||||
private subscription: Subscription|undefined;
|
private subscription: Subscription | undefined;
|
||||||
|
|
||||||
constructor(private hermes: HermesClientService, private events: EventService, private http: HttpClient, private router: Router) {
|
constructor(private hermes: HermesClientService, private events: EventService, private http: HttpClient, private router: Router) {
|
||||||
this.api_keys = [];
|
this.api_keys = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.http.get(environment.API_HOST + '/keys', {
|
this.http.get(environment.API_HOST + '/keys', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
}
|
}
|
||||||
}).subscribe((data: any) => this.api_keys = data);
|
}).subscribe((data: any) => this.api_keys = data);
|
||||||
|
|
||||||
this.subscription = this.events.listen('tts_login_ack', _ => {
|
this.subscription = this.events.listen('tts_login_ack', _ => {
|
||||||
if (document.location.href.includes('/tts-login')) {
|
this.router.navigate(['/policies'])
|
||||||
this.router.navigate(['/policies'])
|
});
|
||||||
}
|
this.events.listen('tts_logoff', _ => {
|
||||||
});
|
this.selected_api_key = undefined;
|
||||||
this.events.listen('tts_logoff', _ => {
|
this.router.navigate(['/tts-login'])
|
||||||
this.selected_api_key = undefined;
|
});
|
||||||
});
|
this.events.listen('impersonation', _ => {
|
||||||
this.events.listen('impersonation', _ => {
|
this.selected_api_key = undefined;
|
||||||
this.selected_api_key = undefined;
|
|
||||||
|
|
||||||
this.http.get(environment.API_HOST + '/keys', {
|
this.http.get(environment.API_HOST + '/keys', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
}
|
}
|
||||||
}).subscribe((data: any) => this.api_keys = data);
|
}).subscribe((data: any) => this.api_keys = data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.subscription)
|
if (this.subscription)
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
console.log('api key for login', this.selected_api_key)
|
if (!this.selected_api_key)
|
||||||
if (!this.selected_api_key)
|
return;
|
||||||
return;
|
|
||||||
|
|
||||||
this.hermes.login(this.selected_api_key);
|
this.hermes.login(this.selected_api_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
src/app/auth/user-card/user-card.component.html
Normal file
20
src/app/auth/user-card/user-card.component.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@if (auth.isAuthenticated()) {
|
||||||
|
<main>
|
||||||
|
<mat-card appearance="outlined" class="card">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{username}}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<impersonation />
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions class="actions">
|
||||||
|
<div>
|
||||||
|
@if (isTTSLoggedIn) {
|
||||||
|
<button mat-raised-button (click)="client.disconnect()"><span class="disconnect">Disconnect</span></button>
|
||||||
|
}
|
||||||
|
<button mat-raised-button (click)="auth.logout()"><span class="logoff">Log Off</span></button>
|
||||||
|
</div>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
</main>
|
||||||
|
}
|
23
src/app/auth/user-card/user-card.component.scss
Normal file
23
src/app/auth/user-card/user-card.component.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 0 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect, .logoff {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-button ~ .mdc-button {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
23
src/app/auth/user-card/user-card.component.spec.ts
Normal file
23
src/app/auth/user-card/user-card.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UserCardComponent } from './user-card.component';
|
||||||
|
|
||||||
|
describe('UserCardComponent', () => {
|
||||||
|
let component: UserCardComponent;
|
||||||
|
let fixture: ComponentFixture<UserCardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [UserCardComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(UserCardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
26
src/app/auth/user-card/user-card.component.ts
Normal file
26
src/app/auth/user-card/user-card.component.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ImpersonationComponent } from '../impersonation/impersonation.component';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'user-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [ImpersonationComponent, MatButtonModule, MatCardModule],
|
||||||
|
templateUrl: './user-card.component.html',
|
||||||
|
styleUrl: './user-card.component.scss'
|
||||||
|
})
|
||||||
|
export class UserCardComponent {
|
||||||
|
auth = inject(ApiAuthenticationService);
|
||||||
|
client = inject(HermesClientService);
|
||||||
|
|
||||||
|
get isTTSLoggedIn() {
|
||||||
|
return this.client.logged_in;
|
||||||
|
}
|
||||||
|
|
||||||
|
get username() {
|
||||||
|
return this.auth.getUsername();
|
||||||
|
}
|
||||||
|
}
|
@ -15,14 +15,11 @@ export interface Message {
|
|||||||
})
|
})
|
||||||
export class HermesClientService {
|
export class HermesClientService {
|
||||||
pipe = new DatePipe('en-US');
|
pipe = new DatePipe('en-US');
|
||||||
session_id: string|undefined;
|
session_id: string | undefined;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
logged_in: boolean;
|
logged_in: boolean;
|
||||||
|
|
||||||
private subscriptions: { [key: number]: ((data: any) => void)[] }
|
|
||||||
|
|
||||||
constructor(private socket: HermesSocketService, private events: EventService) {
|
constructor(private socket: HermesSocketService, private events: EventService) {
|
||||||
this.subscriptions = {};
|
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.logged_in = false;
|
this.logged_in = false;
|
||||||
|
|
||||||
@ -92,6 +89,18 @@ export class HermesClientService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public createRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
||||||
|
if (!this.logged_in)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.send(3, {
|
||||||
|
request_id: null,
|
||||||
|
type: "create_redeemable_action",
|
||||||
|
data: { name, type, data: d },
|
||||||
|
nounce: this.session_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public createTTSFilter(search: string, replace: string) {
|
public createTTSFilter(search: string, replace: string) {
|
||||||
if (!this.logged_in)
|
if (!this.logged_in)
|
||||||
return;
|
return;
|
||||||
@ -114,6 +123,18 @@ export class HermesClientService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deleteRedeemableAction(name: string) {
|
||||||
|
if (!this.logged_in)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.send(3, {
|
||||||
|
request_id: null,
|
||||||
|
type: "delete_redeemable_action",
|
||||||
|
data: { name },
|
||||||
|
nounce: this.session_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public deleteTTSFilter(id: string) {
|
public deleteTTSFilter(id: string) {
|
||||||
if (!this.logged_in)
|
if (!this.logged_in)
|
||||||
return;
|
return;
|
||||||
@ -159,6 +180,17 @@ export class HermesClientService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchRedeemableActions() {
|
||||||
|
if (!this.logged_in)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.send(3, {
|
||||||
|
request_id: null,
|
||||||
|
type: "get_redeemable_actions",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public heartbeat() {
|
public heartbeat() {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
this.send(0, {
|
this.send(0, {
|
||||||
@ -166,11 +198,21 @@ export class HermesClientService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public subscribe(code: number, action: (data: any) => void) {
|
public subscribe(code: number, next: (data: any) => void) {
|
||||||
if (!(code in this.subscriptions)) {
|
return this.socket.subscribe({
|
||||||
this.subscriptions[code] = []
|
next: (message: any) => {
|
||||||
}
|
if (message.op == code)
|
||||||
this.subscriptions[code].push(action);
|
next(message.d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribeToRequests(requestType: string, action: (data: any) => void) {
|
||||||
|
return this.subscribe(4, (data) => {
|
||||||
|
const type = data.request.type;
|
||||||
|
if (type == requestType)
|
||||||
|
action(data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) {
|
public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) {
|
||||||
@ -186,6 +228,18 @@ export class HermesClientService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
||||||
|
if (!this.logged_in)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.send(3, {
|
||||||
|
request_id: null,
|
||||||
|
type: "update_redeemable_action",
|
||||||
|
data: { name, type, data: d },
|
||||||
|
nounce: this.session_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public updateTTSFilter(id: string, search: string, replace: string) {
|
public updateTTSFilter(id: string, search: string, replace: string) {
|
||||||
if (!this.logged_in)
|
if (!this.logged_in)
|
||||||
return;
|
return;
|
||||||
@ -207,18 +261,11 @@ export class HermesClientService {
|
|||||||
console.log("Heartbeat received. Potential connection problem?");
|
console.log("Heartbeat received. Potential connection problem?");
|
||||||
break;
|
break;
|
||||||
case 2: // Login Ack
|
case 2: // Login Ack
|
||||||
console.log("Login successful.", message.d.session_id);
|
console.log("Login successful.");
|
||||||
this.logged_in = true;
|
this.logged_in = true;
|
||||||
this.session_id = message.d.session_id;
|
this.session_id = message.d.session_id;
|
||||||
this.events.emit('tts_login_ack', null);
|
this.events.emit('tts_login_ack', null);
|
||||||
break;
|
break;
|
||||||
case 4: // Request Ack
|
|
||||||
console.log("Request ack received.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (message.op in this.subscriptions) {
|
|
||||||
for (let action of this.subscriptions[message.op])
|
|
||||||
action(message.d);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err: any) => {
|
error: (err: any) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { OnInit, Injectable } from '@angular/core';
|
import { OnInit, Injectable } from '@angular/core';
|
||||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||||
import { catchError, filter, first, timeout } from 'rxjs/operators';
|
import { catchError, first, timeout } from 'rxjs/operators';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<impersonation />
|
<user-card class="card" />
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a routerLink="/login" routerLinkActive="active" *ngIf="!isLoggedIn()">
|
<a routerLink="/login" routerLinkActive="active" *ngIf="!isLoggedIn()">
|
||||||
|
@ -4,7 +4,6 @@ $primary_font_color: #111111;
|
|||||||
$secondary_background_color: #DDDDDD;
|
$secondary_background_color: #DDDDDD;
|
||||||
$secondary_font_color: #333333;
|
$secondary_font_color: #333333;
|
||||||
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ import { HermesClientService } from '../hermes-client.service';
|
|||||||
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
|
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { ImpersonationComponent } from "../auth/impersonation/impersonation.component";
|
import { UserCardComponent } from "../auth/user-card/user-card.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'navigation',
|
selector: 'navigation',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterModule, MatCardModule, AuthModule, ImpersonationComponent],
|
imports: [CommonModule, RouterModule, MatCardModule, AuthModule, UserCardComponent],
|
||||||
templateUrl: './navigation.component.html',
|
templateUrl: './navigation.component.html',
|
||||||
styleUrl: './navigation.component.scss'
|
styleUrl: './navigation.component.scss'
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<form
|
<form
|
||||||
standalone>
|
standalone>
|
||||||
<mat-form-field class="example-full-width">
|
<mat-form-field>
|
||||||
<input
|
<input
|
||||||
name="path"
|
name="path"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -8,176 +8,168 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'policy-table',
|
selector: 'policy-table',
|
||||||
imports: [FormsModule, MatTableModule, MatIconModule],
|
imports: [FormsModule, MatTableModule, MatIconModule],
|
||||||
templateUrl: './policy-table.component.html',
|
templateUrl: './policy-table.component.html',
|
||||||
styleUrl: './policy-table.component.scss'
|
styleUrl: './policy-table.component.scss'
|
||||||
})
|
})
|
||||||
export class PolicyTableComponent implements OnInit, OnDestroy {
|
export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||||
@Input() policies: Policy[] = []
|
@Input() policies: Policy[] = []
|
||||||
displayedColumns = ['path', 'group', 'usage', 'span', 'actions']
|
displayedColumns = ['path', 'group', 'usage', 'span', 'actions']
|
||||||
groups: { [id: string]: { id: string, name: string, priority: number } }
|
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, private hermes: HermesClientService) {
|
constructor(private events: EventService, private hermes: HermesClientService) {
|
||||||
this.table = {} as MatTable<Policy>;
|
this.table = {} as MatTable<Policy>;
|
||||||
this.groups = {};
|
this.groups = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.subscription = this.events.listen('addPolicy', (payload) => {
|
this.subscription = this.events.listen('addPolicy', (payload) => {
|
||||||
if (!payload)
|
if (!payload)
|
||||||
return;
|
return;
|
||||||
if (this.policies.map(p => p.path).includes(payload)) {
|
if (this.policies.map(p => p.path).includes(payload)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.policies.push(new Policy("", "", payload, 1, 5000, "", true, true));
|
this.policies.push(new Policy("", "", payload, 1, 5000, "", true, true));
|
||||||
this.table.renderRows();
|
this.table.renderRows();
|
||||||
});
|
});
|
||||||
this.hermes.subscribe(4, (response: any) => {
|
this.hermes.subscribe(4, (response: any) => {
|
||||||
console.log('request received: ', response);
|
if (response.request.type == "get_policies") {
|
||||||
if (response.request.type == "get_policies") {
|
for (let policy of response.data) {
|
||||||
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.policies.push(new Policy(policy.id, policy.group_id, policy.path, policy.usage, policy.span, "", false, false));
|
}
|
||||||
}
|
this.table.renderRows();
|
||||||
if (isDevMode())
|
} else if (response.request.type == "create_policy") {
|
||||||
console.log('policies', this.policies);
|
const policy = this.policies.find(p => this.groups[response.data.group_id].name == p.temp_group_name && p.path == response.data.path);
|
||||||
this.table.renderRows();
|
if (policy == null) {
|
||||||
} else if (response.request.type == "create_policy") {
|
this.policies.push(new Policy(response.data.id, response.data.group_id, response.data.path, response.data.usage, response.data.span));
|
||||||
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('groups', response.data)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.hermes.fetchPolicies();
|
|
||||||
this.hermes.fetchPermissionsAndGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (this.subscription)
|
|
||||||
this.subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(policy: Policy) {
|
|
||||||
if (!policy.editing)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (policy.isNew) {
|
|
||||||
const index = this.policies.indexOf(policy);
|
|
||||||
if (index >= 0) {
|
|
||||||
this.policies.splice(index, 1);
|
|
||||||
this.table.renderRows();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
policy.path = policy.old_path ?? '';
|
policy.id = response.data.id;
|
||||||
policy.usage = policy.old_usage ?? 1;
|
policy.group_id = response.data.group_id;
|
||||||
policy.span = policy.old_span ?? 5000;
|
policy.editing = false;
|
||||||
policy.old_path = undefined;
|
policy.isNew = false;
|
||||||
policy.old_span = undefined;
|
|
||||||
policy.old_usage = undefined;
|
|
||||||
policy.editing = false;
|
|
||||||
}
|
}
|
||||||
}
|
this.table.renderRows();
|
||||||
|
} else if (response.request.type == "update_policy") {
|
||||||
delete(policy: Policy) {
|
const policy = this.policies.find(p => p.id == response.data.id);
|
||||||
this.hermes.deletePolicy(policy.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));
|
||||||
|
|
||||||
edit(policy: Policy) {
|
|
||||||
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) {
|
|
||||||
if (!policy.temp_group_name) {
|
|
||||||
console.log('group must be valid.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const group = Object.values(this.groups).find(g => g.name == policy.temp_group_name);
|
|
||||||
if (group == null) {
|
|
||||||
console.log('group does not exist.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policy.isNew) {
|
|
||||||
const match = this.policies.find(p => p.group_id == group.id && p.path == policy.path);
|
|
||||||
if (match) {
|
|
||||||
console.log('policy already exists');
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policy.isNew) {
|
|
||||||
this.hermes.createPolicy(group.id, policy.path, policy.usage, policy.span);
|
|
||||||
} else {
|
} else {
|
||||||
this.hermes.updatePolicy(policy.id, group.id, policy.path, policy.usage, policy.span);
|
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") {
|
||||||
|
const policy = this.policies.find(p => p.id == response.request.data.id);
|
||||||
|
if (!policy) {
|
||||||
|
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 })));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.hermes.fetchPolicies();
|
||||||
|
this.hermes.fetchPermissionsAndGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.subscription)
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(policy: Policy) {
|
||||||
|
if (!policy.editing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (policy.isNew) {
|
||||||
|
const index = this.policies.indexOf(policy);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.policies.splice(index, 1);
|
||||||
|
this.table.renderRows();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(policy: Policy) {
|
||||||
|
this.hermes.deletePolicy(policy.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
edit(policy: Policy) {
|
||||||
|
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) {
|
||||||
|
if (!policy.temp_group_name) {
|
||||||
|
console.log('group must be valid.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = Object.values(this.groups).find(g => g.name == policy.temp_group_name);
|
||||||
|
if (group == null) {
|
||||||
|
console.log('group does not exist.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (policy.isNew) {
|
||||||
|
const match = this.policies.find(p => p.group_id == group.id && p.path == policy.path);
|
||||||
|
if (match) {
|
||||||
|
console.log('policy already exists');
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
export enum FilterFlag {
|
export enum FilterFlag {
|
||||||
None = 0,
|
None = 0,
|
||||||
IgnoreCase = 1,
|
IgnoreCase = 1,
|
||||||
|
ExplicitCapture = 4,
|
||||||
|
CultureInvariant = 512,
|
||||||
|
NonBacktracking = 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
|
6
src/app/shared/models/redeemable_action.ts
Normal file
6
src/app/shared/models/redeemable_action.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default interface RedeemableAction {
|
||||||
|
user_id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
data: any
|
||||||
|
}
|
@ -32,6 +32,11 @@ export class ApiAuthenticationService {
|
|||||||
return this.user?.name;
|
return this.user?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
localStorage.removeItem('jwt');
|
||||||
|
this.updateAuthenticated(false, null);
|
||||||
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
const jwt = localStorage.getItem('jwt');
|
const jwt = localStorage.getItem('jwt');
|
||||||
if (!jwt) {
|
if (!jwt) {
|
||||||
|
14
src/app/shared/validators/item-exists-in-array.ts
Normal file
14
src/app/shared/validators/item-exists-in-array.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||||
|
|
||||||
|
|
||||||
|
export function createItemExistsInArrayValidator(items: any[], getter: (value: any) => any): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value;
|
||||||
|
|
||||||
|
if (!value)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const matches = items.some(i => getter(i) == value);
|
||||||
|
return matches ? { itemExistsInArray: true } : null;
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,11 @@ export class FilterItemEditComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
onSaveClick(): Filter {
|
onSaveClick(): Filter|undefined {
|
||||||
|
if (this.forms.invalid) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.search = this.forms.value.search ?? '';
|
this.data.search = this.forms.value.search ?? '';
|
||||||
this.data.replace = this.forms.value.replace ?? '';
|
this.data.replace = this.forms.value.replace ?? '';
|
||||||
this.data.flag = this.forms.value.flag ?? 0;
|
this.data.flag = this.forms.value.flag ?? 0;
|
||||||
|
@ -37,7 +37,6 @@ export class FilterItemComponent implements OnInit {
|
|||||||
|
|
||||||
dialogRef.afterClosed().subscribe((result: Filter) => {
|
dialogRef.afterClosed().subscribe((result: Filter) => {
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
console.log('update filter', result);
|
|
||||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_tts_filter' && d.d.data.id == this.item.id)
|
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_tts_filter' && d.d.data.id == this.item.id)
|
||||||
?.subscribe(_ => {
|
?.subscribe(_ => {
|
||||||
this.item.search = result.search;
|
this.item.search = result.search;
|
||||||
|
@ -28,24 +28,19 @@ export class FiltersComponent implements OnInit, OnDestroy {
|
|||||||
this.items = []
|
this.items = []
|
||||||
this.client.subscribe(4, d => {
|
this.client.subscribe(4, d => {
|
||||||
const type = d.request.type;
|
const type = d.request.type;
|
||||||
console.log('filters', type, d.data);
|
|
||||||
|
|
||||||
if (type == 'get_tts_word_filters') {
|
if (type == 'get_tts_word_filters') {
|
||||||
this.items = d.data;
|
this.items = d.data;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (d.request.nounce == client.session_id) {
|
if (d.request.nounce == client.session_id) {
|
||||||
console.log('from us. ignore.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type == 'create_tts_filter') {
|
if (type == 'create_tts_filter') {
|
||||||
console.log('create filter', d.data);
|
|
||||||
this.items = [d.data, ...this.items];
|
this.items = [d.data, ...this.items];
|
||||||
} else if (type == 'delete_tts_filter') {
|
} else if (type == 'delete_tts_filter') {
|
||||||
console.log('delete filter', d.data);
|
|
||||||
this.items = this.items.filter(i => i.id != d.data.id);
|
this.items = this.items.filter(i => i.id != d.data.id);
|
||||||
} else if (type == 'update_tts_filter') {
|
} else if (type == 'update_tts_filter') {
|
||||||
console.log('update filter', d.data);
|
|
||||||
const filter = this.items.find(f => f.id == d.data.id);
|
const filter = this.items.find(f => f.id == d.data.id);
|
||||||
if (filter == null)
|
if (filter == null)
|
||||||
return;
|
return;
|
||||||
@ -78,10 +73,8 @@ export class FiltersComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
dialogRef.afterClosed().subscribe((result: any) => {
|
dialogRef.afterClosed().subscribe((result: any) => {
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
console.log('filters create result', result);
|
|
||||||
this.client.first(d => d.op == 4 && d.d.request.type == 'create_tts_filter' && d.d.data.search == result.search && d.d.data.replace == result.replace)
|
this.client.first(d => d.op == 4 && d.d.request.type == 'create_tts_filter' && d.d.data.search == result.search && d.d.data.replace == result.replace)
|
||||||
?.subscribe(d => {
|
?.subscribe(d => {
|
||||||
console.log('adding filter', d.d.data);
|
|
||||||
this.items = [d.d.data, ...this.items];
|
this.items = [d.d.data, ...this.items];
|
||||||
});
|
});
|
||||||
this.client.createTTSFilter(result.search, result.replace);
|
this.client.createTTSFilter(result.search, result.replace);
|
||||||
|
@ -6,43 +6,41 @@ import { ApiAuthenticationService } from '../shared/services/api/api-authenticat
|
|||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-twitch-auth-callback',
|
selector: 'app-twitch-auth-callback',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [],
|
imports: [],
|
||||||
templateUrl: './twitch-auth-callback.component.html',
|
templateUrl: './twitch-auth-callback.component.html',
|
||||||
styleUrl: './twitch-auth-callback.component.scss'
|
styleUrl: './twitch-auth-callback.component.scss'
|
||||||
})
|
})
|
||||||
export class TwitchAuthCallbackComponent implements OnInit {
|
export class TwitchAuthCallbackComponent implements OnInit {
|
||||||
private isBrowser: boolean;
|
private isBrowser: boolean;
|
||||||
|
|
||||||
constructor(private http: HttpClient, private auth: ApiAuthenticationService, private route: ActivatedRoute, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) {
|
constructor(private http: HttpClient, private auth: ApiAuthenticationService, private route: ActivatedRoute, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||||
this.isBrowser = isPlatformBrowser(this.platformId)
|
this.isBrowser = isPlatformBrowser(this.platformId)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (!this.isBrowser) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
const code = this.route.snapshot.queryParamMap.get('code');
|
||||||
if (!this.isBrowser) {
|
const scope = this.route.snapshot.queryParamMap.get('scope');
|
||||||
return;
|
const state = this.route.snapshot.queryParamMap.get('state');
|
||||||
|
|
||||||
|
if (!code || !scope || !state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.http.post(environment.API_HOST + '/auth/twitch/callback', { code, scope, state })
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
if (!data?.authenticated) {
|
||||||
|
this.router.navigate(['/login?error=callback_error']);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = this.route.snapshot.queryParamMap.get('code');
|
localStorage.setItem('jwt', data.token);
|
||||||
const scope = this.route.snapshot.queryParamMap.get('scope');
|
this.auth.update();
|
||||||
const state = this.route.snapshot.queryParamMap.get('state');
|
this.router.navigate(['/tts-login']);
|
||||||
|
});
|
||||||
console.log('twitch callback', code, scope, state);
|
}
|
||||||
if (!code || !scope || !state)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.http.post(environment.API_HOST + '/auth/twitch/callback', { code, scope, state })
|
|
||||||
.subscribe((data: any) => {
|
|
||||||
console.log('twitch callback response', code, scope, state, data);
|
|
||||||
if (!data?.authenticated) {
|
|
||||||
this.router.navigate(['/login?error=callback_error']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('jwt', data.token);
|
|
||||||
this.auth.update();
|
|
||||||
this.router.navigate(['/tts-login']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user