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:
Tom 2025-01-08 21:50:18 +00:00
parent 11dfde9a03
commit d595c3500e
41 changed files with 1228 additions and 321 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View 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);
}
}

View 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>

View 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;
}
}

View 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();
});
});

View 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);
});
}
}

View 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 { }

View 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>&nbsp;{{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>

View 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;
}
}

View 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();
});
});

View 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];
}
}

View File

@ -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);
}]) }])
), ),

View File

@ -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],
}, },
{ {

View File

@ -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 { }

View File

@ -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>
} }

View File

@ -0,0 +1,6 @@
main {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1em;
}

View File

@ -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: {

View File

@ -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>

View File

@ -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%;
} }

View File

@ -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);
} }
} }

View 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>
}

View 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;
}

View 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();
});
});

View 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();
}
}

View File

@ -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) => {

View File

@ -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';

View File

@ -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()">

View File

@ -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;
} }

View File

@ -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'
}) })

View File

@ -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"

View File

@ -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);
}
}
} }

View File

@ -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 {

View File

@ -0,0 +1,6 @@
export default interface RedeemableAction {
user_id: string
name: string
type: string
data: any
}

View File

@ -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) {

View 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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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']);
});
}
} }