diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index a655f62..1acd13a 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -33,10 +33,21 @@ export class AppComponent implements OnInit, OnDestroy {
this.subscriptions = [];
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => {
- await this.router.navigate(['policies'])
+ const url = router.url;
+ const params = router.parseUrl(url).queryParams;
+
+ if (params && 'rd' in params) {
+ await this.router.navigate([params['rd']]);
+ } else if (url == '/' || url.startsWith('/login') || url.startsWith('/tts-login')) {
+ await this.router.navigate(['policies']);
+ }
}));
this.subscriptions.push(this.events.listen('tts_logoff', async _ => {
- await this.router.navigate(['tts-login'])
+ await this.router.navigate(['tts-login'], {
+ queryParams: {
+ rd: this.router.url.substring(1)
+ }
+ });
}));
}
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index d2f8d3a..68b2822 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -20,32 +20,40 @@ import { GroupsComponent } from './groups/groups/groups.component';
import { GroupPageComponent } from './groups/group-page/group-page.component';
import GroupChatterResolver from './shared/resolvers/group-chatter-resolver';
import PermissionResolver from './shared/resolvers/permission-resolver';
+import { ConnectionsComponent } from './connections/connections/connections.component';
+import ConnectionResolver from './shared/resolvers/connection-resolver';
+import { ConnectionCallbackComponent } from './connections/callback/callback.component';
export const routes: Routes = [
{
- path: 'policies',
- component: PolicyComponent,
+ path: 'actions',
+ component: ActionsComponent,
canActivate: [AuthUserGuard],
resolve: {
- groups: GroupResolver,
- policies: PolicyResolver,
+ redeemableActions: RedeemableActionResolver,
}
},
+ {
+ path: 'auth',
+ component: TwitchAuthCallbackComponent,
+ canActivate: [AuthVisitorGuard],
+ },
+ {
+ path: 'connections',
+ component: ConnectionsComponent,
+ canActivate: [AuthUserGuard],
+ resolve: {
+ connections: ConnectionResolver,
+ }
+ },
+ {
+ path: 'connections/callback',
+ component: ConnectionCallbackComponent,
+ },
{
path: 'groups',
component: GroupsComponent,
- canActivate: [AuthAdminGuard],
- resolve: {
- groups: GroupResolver,
- chatters: GroupChatterResolver,
- policies: PolicyResolver,
- permissions: PermissionResolver,
- }
- },
- {
- path: 'groups/:id',
- component: GroupPageComponent,
- canActivate: [AuthAdminGuard],
+ canActivate: [AuthUserGuard],
resolve: {
groups: GroupResolver,
chatters: GroupChatterResolver,
@@ -62,11 +70,28 @@ export const routes: Routes = [
}
},
{
- path: 'actions',
- component: ActionsComponent,
+ path: 'groups/:id',
+ component: GroupPageComponent,
canActivate: [AuthUserGuard],
resolve: {
- redeemableActions: RedeemableActionResolver,
+ groups: GroupResolver,
+ chatters: GroupChatterResolver,
+ policies: PolicyResolver,
+ permissions: PermissionResolver,
+ }
+ },
+ {
+ path: 'login',
+ component: LoginComponent,
+ canActivate: [AuthVisitorGuard],
+ },
+ {
+ path: 'policies',
+ component: PolicyComponent,
+ canActivate: [AuthUserGuard],
+ resolve: {
+ groups: GroupResolver,
+ policies: PolicyResolver,
}
},
{
@@ -79,11 +104,6 @@ export const routes: Routes = [
twitchRedemptions: TwitchRedemptionResolver,
}
},
- {
- path: 'login',
- component: LoginComponent,
- canActivate: [AuthVisitorGuard],
- },
{
path: 'tts-login',
component: TtsLoginComponent,
@@ -92,9 +112,4 @@ export const routes: Routes = [
keys: ApiKeyResolver,
}
},
- {
- path: 'auth',
- component: TwitchAuthCallbackComponent,
- canActivate: [AuthVisitorGuard],
- }
];
diff --git a/src/app/connections/callback/callback.component.html b/src/app/connections/callback/callback.component.html
new file mode 100644
index 0000000..6a22b12
--- /dev/null
+++ b/src/app/connections/callback/callback.component.html
@@ -0,0 +1,3 @@
+@if (success || failure) {
+
Automatically going back to the connections page soon...
+}
\ No newline at end of file
diff --git a/src/app/connections/callback/callback.component.scss b/src/app/connections/callback/callback.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/connections/callback/callback.component.spec.ts b/src/app/connections/callback/callback.component.spec.ts
new file mode 100644
index 0000000..6764d58
--- /dev/null
+++ b/src/app/connections/callback/callback.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CallbackComponent } from './callback.component';
+
+describe('CallbackComponent', () => {
+ let component: CallbackComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [CallbackComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CallbackComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/connections/callback/callback.component.ts b/src/app/connections/callback/callback.component.ts
new file mode 100644
index 0000000..155695c
--- /dev/null
+++ b/src/app/connections/callback/callback.component.ts
@@ -0,0 +1,48 @@
+import { Component, inject, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { HermesClientService } from '../../hermes-client.service';
+import { HttpClient } from '@angular/common/http';
+
+@Component({
+ selector: 'connection-callback',
+ imports: [],
+ templateUrl: './callback.component.html',
+ styleUrl: './callback.component.scss'
+})
+export class ConnectionCallbackComponent implements OnInit {
+ private readonly client = inject(HermesClientService);
+ private readonly http = inject(HttpClient);
+ private readonly router = inject(Router);
+
+ success: boolean = false;
+ failure: boolean = false;
+
+ async ngOnInit() {
+ const url = this.router.parseUrl(this.router.url);
+ if (!url.fragment) {
+ this.failure = true;
+ await this.router.navigate(['connections']);
+ return;
+ }
+
+ const paramsParts = url.fragment.split('&');
+ const params = Object.assign({}, ...paramsParts.map((p: string) => ({ [p.split('=')[0]]: p.split('=')[1] })));
+
+ if (!params.access_token || !params.scope || !params.state || !params.token_type) {
+ this.failure = true;
+ await this.router.navigate(['connections']);
+ return;
+ }
+
+ this.http.get(`https://beta.tomtospeech.com/api/auth/connections?token=${params['access_token']}&state=${params['state']}&expires_in=${params['expires_in']}`).subscribe(async (d: any) => {
+ const data = d.data;
+ this.success = true;
+
+ await setTimeout(async () => {
+ this.client.createConnection(data.connection.name, data.connection.type, data.connection.clientId, params['access_token'], data.connection.grantType, params['scope'], data.expires_at);
+ await this.router.navigate(['connections']);
+ }, 2000)
+ });
+ ;
+ }
+}
diff --git a/src/app/connections/connection-item-edit/connection-item-edit.component.html b/src/app/connections/connection-item-edit/connection-item-edit.component.html
new file mode 100644
index 0000000..d503b9d
--- /dev/null
+++ b/src/app/connections/connection-item-edit/connection-item-edit.component.html
@@ -0,0 +1,60 @@
+
+
+
+ Add Connection
+
+
+
+
+
+
+ Connection Name
+
+ @if (nameControl.invalid && (nameControl.dirty || nameControl.touched)) {
+ @if (nameControl.hasError('required')) {
+ This field is required.
+ }
+ }
+
+
+ Client Type
+
+ Nightbot
+ Twitch
+
+ @if (typeControl.invalid && (typeControl.dirty || typeControl.touched)) {
+ @if (typeControl.hasError('required')) {
+ This field is required.
+ }
+ }
+
+
+ Client Id
+
+ @if (clientIdControl.invalid && (clientIdControl.dirty || clientIdControl.touched)) {
+ @if (clientIdControl.hasError('required')) {
+ This field is required.
+ }
+ }
+
+
+
+
+
+
+
+
+ @if (responseError) {
+
+ {{responseError}}
+
+ }
+
\ No newline at end of file
diff --git a/src/app/connections/connection-item-edit/connection-item-edit.component.scss b/src/app/connections/connection-item-edit/connection-item-edit.component.scss
new file mode 100644
index 0000000..7334674
--- /dev/null
+++ b/src/app/connections/connection-item-edit/connection-item-edit.component.scss
@@ -0,0 +1,12 @@
+.mat-mdc-form-field {
+ display: block;
+ margin: 1em;
+}
+
+.mat-mdc-card-actions {
+ align-self: center;
+}
+
+.mat-mdc-card-actions > button {
+ margin: 1em;
+}
\ No newline at end of file
diff --git a/src/app/connections/connection-item-edit/connection-item-edit.component.spec.ts b/src/app/connections/connection-item-edit/connection-item-edit.component.spec.ts
new file mode 100644
index 0000000..d93238c
--- /dev/null
+++ b/src/app/connections/connection-item-edit/connection-item-edit.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConnectionItemEditComponent } from './connection-item-edit.component';
+
+describe('ConnectionItemEditComponent', () => {
+ let component: ConnectionItemEditComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ConnectionItemEditComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ConnectionItemEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/connections/connection-item-edit/connection-item-edit.component.ts b/src/app/connections/connection-item-edit/connection-item-edit.component.ts
new file mode 100644
index 0000000..cd7931e
--- /dev/null
+++ b/src/app/connections/connection-item-edit/connection-item-edit.component.ts
@@ -0,0 +1,72 @@
+import { HttpClient } from '@angular/common/http';
+import { Component, Inject, inject } from '@angular/core';
+import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
+import { MatInputModule } from '@angular/material/input';
+import { ActionItemEditComponent } from '../../actions/action-item-edit/action-item-edit.component';
+import { HermesClientService } from '../../hermes-client.service';
+import { MatSelectModule } from '@angular/material/select';
+import { DOCUMENT } from '@angular/common';
+
+@Component({
+ selector: 'connection-item-edit',
+ imports: [
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatSelectModule,
+ ReactiveFormsModule,
+ ],
+ templateUrl: './connection-item-edit.component.html',
+ styleUrl: './connection-item-edit.component.scss'
+})
+export class ConnectionItemEditComponent {
+ private readonly client = inject(HermesClientService);
+ private readonly http = inject(HttpClient);
+
+ readonly data = inject<{ name: string }>(MAT_DIALOG_DATA);
+ readonly nameControl = new FormControl('', [Validators.required]);
+ readonly clientIdControl = new FormControl('', [Validators.required]);
+ readonly typeControl = new FormControl('', [Validators.required]);
+ readonly form = new FormGroup({
+ name: this.nameControl,
+ clientId: this.clientIdControl,
+ type: this.typeControl,
+ });
+
+ readonly dialogRef = inject(MatDialogRef);
+
+ responseError: string | undefined;
+ waitForResponse = false;
+
+ constructor(@Inject(DOCUMENT) private document: Document) { }
+
+
+ ngOnInit(): void {
+ this.nameControl.setValue(this.data.name);
+ }
+
+ submit(): void {
+ if (this.form.invalid || this.waitForResponse) {
+ return;
+ }
+
+ this.http.post('/api/auth/connections', {
+ name: this.nameControl.value,
+ type: this.typeControl.value,
+ client_id: this.clientIdControl.value,
+ grant_type: 'bearer',
+ },
+ {
+ headers: {
+ 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
+ }
+ }).subscribe(async (d: any) => this.document.location.href = d.data);
+ }
+}
diff --git a/src/app/connections/connection-item/connection-item.component.html b/src/app/connections/connection-item/connection-item.component.html
new file mode 100644
index 0000000..1faf2b6
--- /dev/null
+++ b/src/app/connections/connection-item/connection-item.component.html
@@ -0,0 +1,19 @@
+
+ {{connection().name}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/connections/connection-item/connection-item.component.scss b/src/app/connections/connection-item/connection-item.component.scss
new file mode 100644
index 0000000..22a0489
--- /dev/null
+++ b/src/app/connections/connection-item/connection-item.component.scss
@@ -0,0 +1,11 @@
+section {
+ padding: 1em;
+}
+
+.right {
+ float: right;
+}
+
+button {
+ margin: 0 0.5em;
+}
\ No newline at end of file
diff --git a/src/app/connections/connection-item/connection-item.component.spec.ts b/src/app/connections/connection-item/connection-item.component.spec.ts
new file mode 100644
index 0000000..645887b
--- /dev/null
+++ b/src/app/connections/connection-item/connection-item.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConnectionItemComponent } from './connection-item.component';
+
+describe('ConnectionItemComponent', () => {
+ let component: ConnectionItemComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ConnectionItemComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ConnectionItemComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/connections/connection-item/connection-item.component.ts b/src/app/connections/connection-item/connection-item.component.ts
new file mode 100644
index 0000000..de4c47e
--- /dev/null
+++ b/src/app/connections/connection-item/connection-item.component.ts
@@ -0,0 +1,49 @@
+import { Component, Inject, inject, input } from '@angular/core';
+import { Connection } from '../../shared/models/connection';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClient } from '@angular/common/http';
+import { Router } from '@angular/router';
+import { DOCUMENT } from '@angular/common';
+import { HermesClientService } from '../../hermes-client.service';
+
+@Component({
+ selector: 'connection-item',
+ imports: [
+ MatButtonModule,
+ MatIconModule,
+ MatFormFieldModule,
+ ReactiveFormsModule,
+ ],
+ templateUrl: './connection-item.component.html',
+ styleUrl: './connection-item.component.scss'
+})
+export class ConnectionItemComponent {
+ router = inject(Router);
+ http = inject(HttpClient);
+ client = inject(HermesClientService);
+
+ connection = input.required();
+
+ constructor(@Inject(DOCUMENT) private document: Document) { }
+
+ delete(conn: Connection) {
+ this.client.deleteConnection(conn.name);
+ }
+
+ renew(conn: Connection) {
+ this.http.post('/api/auth/connections', {
+ name: conn.name,
+ type: conn.type,
+ client_id: conn.client_id,
+ grant_type: conn.grant_type,
+ },
+ {
+ headers: {
+ 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
+ }
+ }).subscribe(async (d: any) => this.document.location.href = d.data);
+ }
+}
diff --git a/src/app/connections/connection-list/connection-list.component.html b/src/app/connections/connection-list/connection-list.component.html
new file mode 100644
index 0000000..a3ef1e3
--- /dev/null
+++ b/src/app/connections/connection-list/connection-list.component.html
@@ -0,0 +1,38 @@
+
+
+ @for (connection of connections; track $index) {
+ -
+
+
+ }
+ @if (!connections.length) {
+ @if (searchControl.value) {
+ No connections matches the filter.
+ } @else {
+ No connections available.
+ }
+ }
+
\ No newline at end of file
diff --git a/src/app/connections/connection-list/connection-list.component.scss b/src/app/connections/connection-list/connection-list.component.scss
new file mode 100644
index 0000000..e3e4013
--- /dev/null
+++ b/src/app/connections/connection-list/connection-list.component.scss
@@ -0,0 +1,38 @@
+@use '@angular/material' as mat;
+
+ul {
+ @include mat.all-component-densities(-5);
+
+ @include mat.form-field-overrides((
+ outlined-outline-color: rgb(167, 88, 199),
+ outlined-focus-label-text-color: rgb(155, 57, 194),
+ outlined-focus-outline-color: rgb(155, 57, 194),
+ ));
+
+ background-color: rgb(202, 68, 255);
+ border-radius: 15px;
+ margin: 0 0;
+ padding: 0;
+ max-width: 500px;
+ overflow: hidden;
+}
+
+ul li {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ background-color: rgb(240, 165, 255);
+}
+
+ul li.header {
+ background-color: rgb(215, 115, 255);
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ flex-direction: row;
+ padding: 8px;
+}
+
+ul .notice {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/src/app/connections/connection-list/connection-list.component.spec.ts b/src/app/connections/connection-list/connection-list.component.spec.ts
new file mode 100644
index 0000000..44376a9
--- /dev/null
+++ b/src/app/connections/connection-list/connection-list.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConnectionListComponent } from './connection-list.component';
+
+describe('ConnectionListComponent', () => {
+ let component: ConnectionListComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ConnectionListComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ConnectionListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/connections/connection-list/connection-list.component.ts b/src/app/connections/connection-list/connection-list.component.ts
new file mode 100644
index 0000000..1a03423
--- /dev/null
+++ b/src/app/connections/connection-list/connection-list.component.ts
@@ -0,0 +1,62 @@
+import { Component, inject, Input } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
+import { Connection } from '../../shared/models/connection';
+import { ConnectionItemComponent } from "../connection-item/connection-item.component";
+import { MatInputModule } from '@angular/material/input';
+import { MatDialog } from '@angular/material/dialog';
+import { ConnectionItemEditComponent } from '../connection-item-edit/connection-item-edit.component';
+import { MatSelectModule } from '@angular/material/select';
+import { containsLettersInOrder } from '../../shared/utils/string-compare';
+
+@Component({
+ selector: 'connection-list',
+ imports: [
+ MatButtonModule,
+ MatCardModule,
+ MatFormFieldModule,
+ MatIconModule,
+ MatInputModule,
+ MatSelectModule,
+ ReactiveFormsModule,
+ ConnectionItemComponent,
+ ],
+ templateUrl: './connection-list.component.html',
+ styleUrl: './connection-list.component.scss'
+})
+export class ConnectionListComponent {
+ private readonly _dialog = inject(MatDialog);
+
+ private _connections: Connection[] = [];
+
+ readonly searchControl = new FormControl('');
+ readonly typeControl = new FormControl('');
+
+ opened = false;
+
+
+ get connections() {
+ return this._connections.filter(c => containsLettersInOrder(c.name, this.searchControl.value) && (!this.typeControl.value || c.type == this.typeControl.value));
+ }
+
+ @Input({ required: true })
+ set connections(value: Connection[]) {
+ this._connections = value;
+ }
+
+ add() {
+ if (this.opened)
+ return;
+
+ this.opened = true;
+
+ const dialogRef = this._dialog.open(ConnectionItemEditComponent, {
+ data: { name: this.searchControl.value },
+ });
+
+ dialogRef.afterClosed().subscribe((_: any) => this.opened = false);
+ }
+}
diff --git a/src/app/connections/connections.module.ts b/src/app/connections/connections.module.ts
new file mode 100644
index 0000000..0544fab
--- /dev/null
+++ b/src/app/connections/connections.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CommonModule
+ ]
+})
+export class ConnectionsModule { }
diff --git a/src/app/connections/connections/connections.component.html b/src/app/connections/connections/connections.component.html
new file mode 100644
index 0000000..2033919
--- /dev/null
+++ b/src/app/connections/connections/connections.component.html
@@ -0,0 +1,3 @@
+Connections
+
+
\ No newline at end of file
diff --git a/src/app/connections/connections/connections.component.scss b/src/app/connections/connections/connections.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/connections/connections/connections.component.spec.ts b/src/app/connections/connections/connections.component.spec.ts
new file mode 100644
index 0000000..68000b0
--- /dev/null
+++ b/src/app/connections/connections/connections.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConnectionsComponent } from './connections.component';
+
+describe('ConnectionsComponent', () => {
+ let component: ConnectionsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ConnectionsComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ConnectionsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/connections/connections/connections.component.ts b/src/app/connections/connections/connections.component.ts
new file mode 100644
index 0000000..cfbbe41
--- /dev/null
+++ b/src/app/connections/connections/connections.component.ts
@@ -0,0 +1,41 @@
+import { Component, inject, OnDestroy } from '@angular/core';
+import { Connection } from '../../shared/models/connection';
+import { ActivatedRoute } from '@angular/router';
+import { ConnectionListComponent } from "../connection-list/connection-list.component";
+import { Subscription } from 'rxjs';
+import { ConnectionService } from '../../shared/services/connection.service';
+
+@Component({
+ selector: 'connections',
+ imports: [ConnectionListComponent],
+ templateUrl: './connections.component.html',
+ styleUrl: './connections.component.scss'
+})
+export class ConnectionsComponent implements OnDestroy {
+ private readonly route = inject(ActivatedRoute);
+ private readonly connectionService = inject(ConnectionService);
+ subscriptions: (Subscription | undefined)[] = [];
+ connections: Connection[] = [];
+
+
+ constructor() {
+ this.route.data.subscribe(payload => {
+ this.connections = payload['connections'] ?? [];
+ });
+
+ this.subscriptions.push(this.connectionService.delete$?.subscribe(d => {
+ if (d.error) {
+ return;
+ }
+
+ this.connectionService.fetch().subscribe(connections => this.connections = connections);
+ }));
+ }
+
+ ngOnDestroy(): void {
+ for (let subscription of this.subscriptions) {
+ if (subscription)
+ subscription.unsubscribe();
+ }
+ }
+}
diff --git a/src/app/groups/group-item/group-item.component.scss b/src/app/groups/group-item/group-item.component.scss
index 3d98a1d..19fe350 100644
--- a/src/app/groups/group-item/group-item.component.scss
+++ b/src/app/groups/group-item/group-item.component.scss
@@ -4,13 +4,17 @@ article {
flex-direction: row;
justify-content: space-between;
border-radius: 15px;
- padding: 1em;
+ padding: 0.5em 1em;
- & :first-child {
- min-width: 180px;
+ & > :first-child {
+ min-width: 200px;
}
- & :not(:first-child) {
+ & > :last-child {
+ min-width: 100px;
+ }
+
+ & > :not(:first-child) {
text-align: center;
align-self: center;
}
diff --git a/src/app/hermes-client.service.ts b/src/app/hermes-client.service.ts
index 72d9821..dce1380 100644
--- a/src/app/hermes-client.service.ts
+++ b/src/app/hermes-client.service.ts
@@ -90,6 +90,28 @@ export class HermesClientService {
});
}
+ public createConnection(name: string, type: string, client_id: string, access_token: string, grant_type: string, scope: string, expiration: Date) {
+ if (!this.logged_in)
+ return;
+
+ this.send(3, {
+ request_id: null,
+ type: "create_connection",
+ data: { name, type, client_id, access_token, grant_type, scope, expiration },
+ });
+ }
+
+ public createConnectionState(name: string, type: string, client_id: string, grant_type: string) {
+ if (!this.logged_in)
+ return;
+
+ this.send(3, {
+ request_id: null,
+ type: "create_connection_state",
+ data: { name, type, client_id, grant_type },
+ });
+ }
+
public createGroup(name: string, priority: number) {
if (!this.logged_in)
return;
@@ -170,6 +192,28 @@ export class HermesClientService {
});
}
+ public deleteConnection(name: string) {
+ if (!this.logged_in)
+ return;
+
+ this.send(3, {
+ request_id: null,
+ type: "delete_connection",
+ data: { name },
+ });
+ }
+
+ public deleteConnectionState(name: string) {
+ if (!this.logged_in)
+ return;
+
+ this.send(3, {
+ request_id: null,
+ type: "delete_connection_state",
+ data: { name },
+ });
+ }
+
public deleteGroup(id: string) {
if (!this.logged_in)
return;
@@ -250,6 +294,28 @@ export class HermesClientService {
});
}
+ public fetchConnections() {
+ if (!this.logged_in)
+ return;
+
+ this.send(3, {
+ request_id: null,
+ type: "get_connections",
+ data: null,
+ });
+ }
+
+ public fetchConnectionStates() {
+ if (!this.logged_in)
+ return;
+
+ this.send(3, {
+ request_id: null,
+ type: "get_connection_states",
+ data: null,
+ });
+ }
+
public fetchFilters() {
if (!this.logged_in)
return;
diff --git a/src/app/navigation/navigation.component.html b/src/app/navigation/navigation.component.html
index 464a72f..e0251ea 100644
--- a/src/app/navigation/navigation.component.html
+++ b/src/app/navigation/navigation.component.html
@@ -42,14 +42,19 @@
Redemptions
- @if (isAdmin()) {
Groups
+
+
+ Connections
+
+
}
- }
+
\ No newline at end of file
diff --git a/src/app/permissions/permission-item-edit/permission-item-edit.component.ts b/src/app/permissions/permission-item-edit/permission-item-edit.component.ts
index 5d8614e..07bb782 100644
--- a/src/app/permissions/permission-item-edit/permission-item-edit.component.ts
+++ b/src/app/permissions/permission-item-edit/permission-item-edit.component.ts
@@ -69,7 +69,6 @@ export class PermissionItemEditComponent implements OnInit {
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_group_permission' && d.d.request.data.path == this.pathControl.value)
.subscribe({
next: (d) => {
- console.log('sdifhsdiofs data', d);
if (d.d.error) {
this.responseError = d.d.error;
} else {
diff --git a/src/app/shared/models/connection-state.ts b/src/app/shared/models/connection-state.ts
new file mode 100644
index 0000000..b82c360
--- /dev/null
+++ b/src/app/shared/models/connection-state.ts
@@ -0,0 +1,7 @@
+export interface ConnectionState {
+ user_id: string;
+ name: string;
+ type: string;
+ client_id: string;
+ grant_type: string;
+}
\ No newline at end of file
diff --git a/src/app/shared/models/connection.ts b/src/app/shared/models/connection.ts
new file mode 100644
index 0000000..3e7a226
--- /dev/null
+++ b/src/app/shared/models/connection.ts
@@ -0,0 +1,11 @@
+export interface Connection {
+ user_id: string;
+ name: string;
+ type: string;
+ client_id: string;
+ access_token: string;
+ grant_type: string;
+ scope: string;
+ expires_at: Date;
+ default: boolean;
+}
\ No newline at end of file
diff --git a/src/app/shared/resolvers/connection-resolver.ts b/src/app/shared/resolvers/connection-resolver.ts
new file mode 100644
index 0000000..215d4f3
--- /dev/null
+++ b/src/app/shared/resolvers/connection-resolver.ts
@@ -0,0 +1,14 @@
+import { Injectable } from '@angular/core';
+import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+import { Observable } from 'rxjs';
+import { Connection } from '../models/connection';
+import { ConnectionService } from '../services/connection.service';
+
+@Injectable({ providedIn: 'root' })
+export default class ConnectionResolver implements Resolve {
+ constructor(private service: ConnectionService) { }
+
+ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return this.service.fetch();
+ }
+}
\ No newline at end of file
diff --git a/src/app/shared/resolvers/connection-state-resolver.ts b/src/app/shared/resolvers/connection-state-resolver.ts
new file mode 100644
index 0000000..3e18bb5
--- /dev/null
+++ b/src/app/shared/resolvers/connection-state-resolver.ts
@@ -0,0 +1,14 @@
+import { Injectable } from '@angular/core';
+import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+import { Observable } from 'rxjs';
+import { ConnectionService } from '../services/connection.service';
+import { ConnectionState } from '../models/connection-state';
+
+@Injectable({ providedIn: 'root' })
+export default class ConnectionResolver implements Resolve {
+ constructor(private service: ConnectionService) { }
+
+ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return this.service.fetch();
+ }
+}
\ No newline at end of file
diff --git a/src/app/shared/services/connection-state.service.spec.ts b/src/app/shared/services/connection-state.service.spec.ts
new file mode 100644
index 0000000..0a5a8a1
--- /dev/null
+++ b/src/app/shared/services/connection-state.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { ConnectionStateService } from './connection-state.service';
+
+describe('ConnectionStateService', () => {
+ let service: ConnectionStateService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(ConnectionStateService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/services/connection-state.service.ts b/src/app/shared/services/connection-state.service.ts
new file mode 100644
index 0000000..6830bda
--- /dev/null
+++ b/src/app/shared/services/connection-state.service.ts
@@ -0,0 +1,74 @@
+import { inject, Injectable } from '@angular/core';
+import { HermesClientService } from '../../hermes-client.service';
+import EventService from './EventService';
+import { map, Observable, of } from 'rxjs';
+import { Connection } from '../models/connection';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ConnectionStateService {
+ private readonly client = inject(HermesClientService);
+ private readonly events = inject(EventService);
+ private data: Connection[] = [];
+ private loaded = false;
+ create$: Observable | undefined;
+ update$: Observable | undefined;
+ delete$: Observable | undefined;
+
+ constructor() {
+ this.create$ = this.client.filterByRequestType('create_connection_state');
+ this.delete$ = this.client.filterByRequestType('delete_connection_state');
+
+ this.create$?.subscribe(d => {
+ if (d.error) {
+ return;
+ }
+
+ this.data.push(d.data);
+ });
+ this.update$?.subscribe(d => {
+ if (d.error) {
+ return;
+ }
+
+ const connection = this.data.find(p => p.name == d.data.name);
+ if (connection) {
+ connection.type = d.data.type;
+ connection.client_id = d.data.client_id;
+ connection.access_token = d.data.access_token;
+ connection.grant_type = d.data.grant_type;
+ connection.scope = d.data.scope;
+ connection.expires_at = d.data.expires_at;
+ connection.default = d.data.default;
+ }
+ });
+ this.delete$?.subscribe(d => {
+ if (d.error) {
+ return;
+ }
+
+ this.data = this.data.filter(r => r.name != d.request.data.name);
+ });
+
+ this.events.listen('tts_logoff', () => {
+ this.data = [];
+ this.loaded = false;
+ });
+ }
+
+
+ fetch() {
+ if (this.loaded) {
+ return of(this.data);
+ }
+
+ const $ = this.client.first(d => d.d.request.type == 'get_connection_states')!.pipe(map(d => d.d.data));
+ $.subscribe(d => {
+ this.data = d;
+ this.loaded = true;
+ });
+ this.client.fetchConnectionStates();
+ return $;
+ }
+}
diff --git a/src/app/shared/services/connection.service.spec.ts b/src/app/shared/services/connection.service.spec.ts
new file mode 100644
index 0000000..dd28134
--- /dev/null
+++ b/src/app/shared/services/connection.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { ConnectionService } from './connection.service';
+
+describe('ConnectionService', () => {
+ let service: ConnectionService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(ConnectionService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/services/connection.service.ts b/src/app/shared/services/connection.service.ts
new file mode 100644
index 0000000..7c863c6
--- /dev/null
+++ b/src/app/shared/services/connection.service.ts
@@ -0,0 +1,75 @@
+import { inject, Injectable } from '@angular/core';
+import { HermesClientService } from '../../hermes-client.service';
+import EventService from './EventService';
+import { map, Observable, of } from 'rxjs';
+import { Connection } from '../models/connection';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ConnectionService {
+ private readonly client = inject(HermesClientService);
+ private readonly events = inject(EventService);
+ private data: Connection[] = [];
+ private loaded = false;
+ create$: Observable | undefined;
+ update$: Observable | undefined;
+ delete$: Observable | undefined;
+
+ constructor() {
+ this.create$ = this.client.filterByRequestType('create_connection');
+ this.update$ = this.client.filterByRequestType('update_connection');
+ this.delete$ = this.client.filterByRequestType('delete_connection');
+
+ this.create$?.subscribe(d => {
+ if (d.error) {
+ return;
+ }
+
+ this.data.push(d.data);
+ });
+ this.update$?.subscribe(d => {
+ if (d.error) {
+ return;
+ }
+
+ const connection = this.data.find(p => p.name == d.data.name);
+ if (connection) {
+ connection.type = d.data.type;
+ connection.client_id = d.data.client_id;
+ connection.access_token = d.data.access_token;
+ connection.grant_type = d.data.grant_type;
+ connection.scope = d.data.scope;
+ connection.expires_at = d.data.expires_at;
+ connection.default = d.data.default;
+ }
+ });
+ this.delete$?.subscribe(d => {
+ if (d.error) {
+ return;
+ }
+
+ this.data = this.data.filter(r => r.name != d.request.data.name);
+ });
+
+ this.events.listen('tts_logoff', () => {
+ this.data = [];
+ this.loaded = false;
+ });
+ }
+
+
+ fetch() {
+ if (this.loaded) {
+ return of(this.data);
+ }
+
+ const $ = this.client.first(d => d.d.request.type == 'get_connections')!.pipe(map(d => d.d.data));
+ $.subscribe(d => {
+ this.data = d;
+ this.loaded = true;
+ });
+ this.client.fetchConnections();
+ return $;
+ }
+}
diff --git a/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts b/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts
index 589a56e..142fc42 100644
--- a/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts
+++ b/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts
@@ -10,7 +10,6 @@ import { Group } from '../../shared/models/group';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
-import { group } from 'console';
@Component({
selector: 'app-twitch-user-item-add',