diff --git a/src/app/actions/action-item-edit/action-item-edit.component.html b/src/app/actions/action-item-edit/action-item-edit.component.html
new file mode 100644
index 0000000..fda87e9
--- /dev/null
+++ b/src/app/actions/action-item-edit/action-item-edit.component.html
@@ -0,0 +1,94 @@
+
+
+
+
+ {{isNew ? "New Action" : previousName}}
+ {{isNew ? 'Creating a new action' : 'Modifying an existing action'}}
+
+
+
+
+
+
+ @if (actionEntries.hasOwnProperty(action.type)) {
+
+ }
+
+
+
+ @if (!isNew) {
+
+ }
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/actions/action-item-edit/action-item-edit.component.scss b/src/app/actions/action-item-edit/action-item-edit.component.scss
new file mode 100644
index 0000000..25b7ce5
--- /dev/null
+++ b/src/app/actions/action-item-edit/action-item-edit.component.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/src/app/actions/action-item-edit/action-item-edit.component.spec.ts b/src/app/actions/action-item-edit/action-item-edit.component.spec.ts
new file mode 100644
index 0000000..2ce4174
--- /dev/null
+++ b/src/app/actions/action-item-edit/action-item-edit.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ActionItemEditComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ActionItemEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/actions/action-item-edit/action-item-edit.component.ts b/src/app/actions/action-item-edit/action-item-edit.component.ts
new file mode 100644
index 0000000..5ba0feb
--- /dev/null
+++ b/src/app/actions/action-item-edit/action-item-edit.component.ts
@@ -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);
+ 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);
+ }
+}
diff --git a/src/app/actions/action-list/action-list.component.html b/src/app/actions/action-list/action-list.component.html
new file mode 100644
index 0000000..35f182a
--- /dev/null
+++ b/src/app/actions/action-list/action-list.component.html
@@ -0,0 +1,11 @@
+
+ @for (action of actions; track $index) {
+
+ }
+
+
\ No newline at end of file
diff --git a/src/app/actions/action-list/action-list.component.scss b/src/app/actions/action-list/action-list.component.scss
new file mode 100644
index 0000000..fe1d043
--- /dev/null
+++ b/src/app/actions/action-list/action-list.component.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/app/actions/action-list/action-list.component.spec.ts b/src/app/actions/action-list/action-list.component.spec.ts
new file mode 100644
index 0000000..79b2016
--- /dev/null
+++ b/src/app/actions/action-list/action-list.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ActionListComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ActionListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/actions/action-list/action-list.component.ts b/src/app/actions/action-list/action-list.component.ts
new file mode 100644
index 0000000..e81c425
--- /dev/null
+++ b/src/app/actions/action-list/action-list.component.ts
@@ -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();
+ 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);
+ });
+ }
+}
diff --git a/src/app/actions/actions.module.ts b/src/app/actions/actions.module.ts
new file mode 100644
index 0000000..3acfc2f
--- /dev/null
+++ b/src/app/actions/actions.module.ts
@@ -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 { }
diff --git a/src/app/actions/actions/actions.component.html b/src/app/actions/actions/actions.component.html
new file mode 100644
index 0000000..8d42f57
--- /dev/null
+++ b/src/app/actions/actions/actions.component.html
@@ -0,0 +1,31 @@
+
+ Redeemable Actions
+
+
+
+
\ No newline at end of file
diff --git a/src/app/actions/actions/actions.component.scss b/src/app/actions/actions/actions.component.scss
new file mode 100644
index 0000000..15bb90f
--- /dev/null
+++ b/src/app/actions/actions/actions.component.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/app/actions/actions/actions.component.spec.ts b/src/app/actions/actions/actions.component.spec.ts
new file mode 100644
index 0000000..b01eb17
--- /dev/null
+++ b/src/app/actions/actions/actions.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ActionsComponent } from './actions.component';
+
+describe('ActionsComponent', () => {
+ let component: ActionsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ActionsComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ActionsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/actions/actions/actions.component.ts b/src/app/actions/actions/actions.component.ts
new file mode 100644
index 0000000..895ec0b
--- /dev/null
+++ b/src/app/actions/actions/actions.component.ts
@@ -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];
+ }
+}
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index 02f66d3..f1116d2 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -13,7 +13,6 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes),
provideHttpClient(
withInterceptors([(req: HttpRequest, next: HttpHandlerFn) => {
- console.log(req.url);
return next(req);
}])
),
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 500d6d9..6172085 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -6,8 +6,8 @@ import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
import { FiltersComponent } from './tts-filters/filters/filters.component';
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
-import { ActionComponent } from './actions/action/action.component';
import { AuthVisitorGuard } from './shared/auth/auth.visitor.guard';
+import { ActionsComponent } from './actions/actions/actions.component';
export const routes: Routes = [
{
@@ -22,7 +22,7 @@ export const routes: Routes = [
},
{
path: 'actions',
- component: ActionComponent,
+ component: ActionsComponent,
canActivate: [AuthAdminGuard],
},
{
diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts
index ca1988e..86da670 100644
--- a/src/app/auth/auth.module.ts
+++ b/src/app/auth/auth.module.ts
@@ -2,15 +2,15 @@ import { NgModule } from '@angular/core';
import { LoginComponent } from './login/login.component';
import { TtsLoginComponent } from './tts-login/tts-login.component';
import { ImpersonationComponent } from './impersonation/impersonation.component';
-
-
+import { UserCardComponent } from './user-card/user-card.component';
@NgModule({
declarations: [],
imports: [
LoginComponent,
TtsLoginComponent,
- ImpersonationComponent
+ ImpersonationComponent,
+ UserCardComponent,
]
})
export class AuthModule { }
diff --git a/src/app/auth/impersonation/impersonation.component.html b/src/app/auth/impersonation/impersonation.component.html
index 284cbdd..89c39c1 100644
--- a/src/app/auth/impersonation/impersonation.component.html
+++ b/src/app/auth/impersonation/impersonation.component.html
@@ -1,19 +1,13 @@
@if (isAdmin()) {
-
-
- Impersonation
- Impersonate as another user
-
-
-
- User to impersonate
-
- {{getUsername()}}
- @for (user of users; track user.id) {
- {{ user.name }}
- }
-
-
-
-
+
+
+ User to impersonate
+
+ {{getUsername()}}
+ @for (user of users; track user.id) {
+ {{ user.name }}
+ }
+
+
+
}
\ No newline at end of file
diff --git a/src/app/auth/impersonation/impersonation.component.scss b/src/app/auth/impersonation/impersonation.component.scss
index e69de29..7269665 100644
--- a/src/app/auth/impersonation/impersonation.component.scss
+++ b/src/app/auth/impersonation/impersonation.component.scss
@@ -0,0 +1,6 @@
+main {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 1em;
+}
\ No newline at end of file
diff --git a/src/app/auth/impersonation/impersonation.component.ts b/src/app/auth/impersonation/impersonation.component.ts
index a420a41..f6eb2b1 100644
--- a/src/app/auth/impersonation/impersonation.component.ts
+++ b/src/app/auth/impersonation/impersonation.component.ts
@@ -51,7 +51,6 @@ export class ImpersonationComponent implements OnInit {
}
public onChange(e: any) {
- console.log('impersonate befre', e.value);
if (!e.value) {
this.http.delete(environment.API_HOST + '/admin/impersonate', {
headers: {
diff --git a/src/app/auth/tts-login/tts-login.component.html b/src/app/auth/tts-login/tts-login.component.html
index 4f3110a..fb8a2c0 100644
--- a/src/app/auth/tts-login/tts-login.component.html
+++ b/src/app/auth/tts-login/tts-login.component.html
@@ -1,14 +1,23 @@
-TTS Login
-
-
-
- API Key
-
- @for (key of api_keys; track key.id) {
- {{key.label}}
- }
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+ API Key
+
+ @for (key of api_keys; track key.id) {
+ {{key.label}}
+ }
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/auth/tts-login/tts-login.component.scss b/src/app/auth/tts-login/tts-login.component.scss
index f3681c1..4d22316 100644
--- a/src/app/auth/tts-login/tts-login.component.scss
+++ b/src/app/auth/tts-login/tts-login.component.scss
@@ -1,14 +1,7 @@
-.main-div {
- height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-h4 {
- text-align: center;
-}
-
-.main-card {
- width: 20%;
+main {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
}
\ No newline at end of file
diff --git a/src/app/auth/tts-login/tts-login.component.ts b/src/app/auth/tts-login/tts-login.component.ts
index 250cc60..76297db 100644
--- a/src/app/auth/tts-login/tts-login.component.ts
+++ b/src/app/auth/tts-login/tts-login.component.ts
@@ -10,61 +10,59 @@ import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment';
import { HermesClientService } from '../../hermes-client.service';
-import { MatCard } from '@angular/material/card';
+import { MatCard, MatCardModule } from '@angular/material/card';
@Component({
- selector: 'tts-login',
- standalone: true,
- imports: [MatButtonModule, MatCard, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
- templateUrl: './tts-login.component.html',
- styleUrl: './tts-login.component.scss'
+ selector: 'tts-login',
+ standalone: true,
+ imports: [MatButtonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
+ templateUrl: './tts-login.component.html',
+ styleUrl: './tts-login.component.scss'
})
export class TtsLoginComponent implements OnInit, OnDestroy {
- api_keys: { id: string, label: string }[];
- selected_api_key: string|undefined;
+ api_keys: { id: string, label: string }[];
+ 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) {
- this.api_keys = [];
- }
+ constructor(private hermes: HermesClientService, private events: EventService, private http: HttpClient, private router: Router) {
+ this.api_keys = [];
+ }
- ngOnInit(): void {
- this.http.get(environment.API_HOST + '/keys', {
- headers: {
- 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
- }
- }).subscribe((data: any) => this.api_keys = data);
+ ngOnInit(): void {
+ this.http.get(environment.API_HOST + '/keys', {
+ headers: {
+ 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
+ }
+ }).subscribe((data: any) => this.api_keys = data);
- this.subscription = this.events.listen('tts_login_ack', _ => {
- if (document.location.href.includes('/tts-login')) {
- this.router.navigate(['/policies'])
- }
- });
- this.events.listen('tts_logoff', _ => {
- this.selected_api_key = undefined;
- });
- this.events.listen('impersonation', _ => {
- this.selected_api_key = undefined;
+ this.subscription = this.events.listen('tts_login_ack', _ => {
+ this.router.navigate(['/policies'])
+ });
+ this.events.listen('tts_logoff', _ => {
+ this.selected_api_key = undefined;
+ this.router.navigate(['/tts-login'])
+ });
+ this.events.listen('impersonation', _ => {
+ this.selected_api_key = undefined;
- this.http.get(environment.API_HOST + '/keys', {
- headers: {
- 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
- }
- }).subscribe((data: any) => this.api_keys = data);
- });
- }
+ this.http.get(environment.API_HOST + '/keys', {
+ headers: {
+ 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
+ }
+ }).subscribe((data: any) => this.api_keys = data);
+ });
+ }
- ngOnDestroy(): void {
- if (this.subscription)
- this.subscription.unsubscribe();
- }
+ ngOnDestroy(): void {
+ if (this.subscription)
+ this.subscription.unsubscribe();
+ }
- login() {
- console.log('api key for login', this.selected_api_key)
- if (!this.selected_api_key)
- return;
+ login() {
+ if (!this.selected_api_key)
+ return;
- this.hermes.login(this.selected_api_key);
- }
+ this.hermes.login(this.selected_api_key);
+ }
}
diff --git a/src/app/auth/user-card/user-card.component.html b/src/app/auth/user-card/user-card.component.html
new file mode 100644
index 0000000..02d5fd9
--- /dev/null
+++ b/src/app/auth/user-card/user-card.component.html
@@ -0,0 +1,20 @@
+@if (auth.isAuthenticated()) {
+
+
+
+ {{username}}
+
+
+
+
+
+
+ @if (isTTSLoggedIn) {
+
+ }
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/app/auth/user-card/user-card.component.scss b/src/app/auth/user-card/user-card.component.scss
new file mode 100644
index 0000000..d0f18a8
--- /dev/null
+++ b/src/app/auth/user-card/user-card.component.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/src/app/auth/user-card/user-card.component.spec.ts b/src/app/auth/user-card/user-card.component.spec.ts
new file mode 100644
index 0000000..12a9b46
--- /dev/null
+++ b/src/app/auth/user-card/user-card.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [UserCardComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(UserCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/auth/user-card/user-card.component.ts b/src/app/auth/user-card/user-card.component.ts
new file mode 100644
index 0000000..6748698
--- /dev/null
+++ b/src/app/auth/user-card/user-card.component.ts
@@ -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();
+ }
+}
diff --git a/src/app/hermes-client.service.ts b/src/app/hermes-client.service.ts
index ce5c855..6a330fd 100644
--- a/src/app/hermes-client.service.ts
+++ b/src/app/hermes-client.service.ts
@@ -15,14 +15,11 @@ export interface Message {
})
export class HermesClientService {
pipe = new DatePipe('en-US');
- session_id: string|undefined;
+ session_id: string | undefined;
connected: boolean;
logged_in: boolean;
- private subscriptions: { [key: number]: ((data: any) => void)[] }
-
constructor(private socket: HermesSocketService, private events: EventService) {
- this.subscriptions = {};
this.connected = 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) {
if (!this.logged_in)
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) {
if (!this.logged_in)
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() {
const date = new Date()
this.send(0, {
@@ -166,11 +198,21 @@ export class HermesClientService {
});
}
- public subscribe(code: number, action: (data: any) => void) {
- if (!(code in this.subscriptions)) {
- this.subscriptions[code] = []
- }
- this.subscriptions[code].push(action);
+ public subscribe(code: number, next: (data: any) => void) {
+ return this.socket.subscribe({
+ next: (message: any) => {
+ if (message.op == code)
+ 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) {
@@ -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) {
if (!this.logged_in)
return;
@@ -207,18 +261,11 @@ export class HermesClientService {
console.log("Heartbeat received. Potential connection problem?");
break;
case 2: // Login Ack
- console.log("Login successful.", message.d.session_id);
+ console.log("Login successful.");
this.logged_in = true;
this.session_id = message.d.session_id;
this.events.emit('tts_login_ack', null);
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) => {
diff --git a/src/app/hermes-socket.service.ts b/src/app/hermes-socket.service.ts
index 5b066a1..9764974 100644
--- a/src/app/hermes-socket.service.ts
+++ b/src/app/hermes-socket.service.ts
@@ -1,6 +1,6 @@
import { OnInit, Injectable } from '@angular/core';
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 { Observable, throwError } from 'rxjs';
diff --git a/src/app/navigation/navigation.component.html b/src/app/navigation/navigation.component.html
index 9f488db..a3edf46 100644
--- a/src/app/navigation/navigation.component.html
+++ b/src/app/navigation/navigation.component.html
@@ -1,5 +1,5 @@