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 @@ +
    +
  • + + Name Filter + + + + + Type Filter + + All + Nightbot + Twitch + + + + +
  • + @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',