Added connections. Added url redirect for login.

This commit is contained in:
Tom 2025-03-27 01:25:56 +00:00
parent 56deb3384c
commit 6e5efab5ec
36 changed files with 948 additions and 39 deletions

View File

@ -33,10 +33,21 @@ export class AppComponent implements OnInit, OnDestroy {
this.subscriptions = []; this.subscriptions = [];
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => { 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 _ => { 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)
}
});
})); }));
} }

View File

@ -20,32 +20,40 @@ import { GroupsComponent } from './groups/groups/groups.component';
import { GroupPageComponent } from './groups/group-page/group-page.component'; import { GroupPageComponent } from './groups/group-page/group-page.component';
import GroupChatterResolver from './shared/resolvers/group-chatter-resolver'; import GroupChatterResolver from './shared/resolvers/group-chatter-resolver';
import PermissionResolver from './shared/resolvers/permission-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 = [ export const routes: Routes = [
{ {
path: 'policies', path: 'actions',
component: PolicyComponent, component: ActionsComponent,
canActivate: [AuthUserGuard], canActivate: [AuthUserGuard],
resolve: { resolve: {
groups: GroupResolver, redeemableActions: RedeemableActionResolver,
policies: PolicyResolver,
} }
}, },
{
path: 'auth',
component: TwitchAuthCallbackComponent,
canActivate: [AuthVisitorGuard],
},
{
path: 'connections',
component: ConnectionsComponent,
canActivate: [AuthUserGuard],
resolve: {
connections: ConnectionResolver,
}
},
{
path: 'connections/callback',
component: ConnectionCallbackComponent,
},
{ {
path: 'groups', path: 'groups',
component: GroupsComponent, component: GroupsComponent,
canActivate: [AuthAdminGuard], canActivate: [AuthUserGuard],
resolve: {
groups: GroupResolver,
chatters: GroupChatterResolver,
policies: PolicyResolver,
permissions: PermissionResolver,
}
},
{
path: 'groups/:id',
component: GroupPageComponent,
canActivate: [AuthAdminGuard],
resolve: { resolve: {
groups: GroupResolver, groups: GroupResolver,
chatters: GroupChatterResolver, chatters: GroupChatterResolver,
@ -62,11 +70,28 @@ export const routes: Routes = [
} }
}, },
{ {
path: 'actions', path: 'groups/:id',
component: ActionsComponent, component: GroupPageComponent,
canActivate: [AuthUserGuard], canActivate: [AuthUserGuard],
resolve: { 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, twitchRedemptions: TwitchRedemptionResolver,
} }
}, },
{
path: 'login',
component: LoginComponent,
canActivate: [AuthVisitorGuard],
},
{ {
path: 'tts-login', path: 'tts-login',
component: TtsLoginComponent, component: TtsLoginComponent,
@ -92,9 +112,4 @@ export const routes: Routes = [
keys: ApiKeyResolver, keys: ApiKeyResolver,
} }
}, },
{
path: 'auth',
component: TwitchAuthCallbackComponent,
canActivate: [AuthVisitorGuard],
}
]; ];

View File

@ -0,0 +1,3 @@
@if (success || failure) {
<p>Automatically going back to the connections page soon...</p>
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CallbackComponent } from './callback.component';
describe('CallbackComponent', () => {
let component: CallbackComponent;
let fixture: ComponentFixture<CallbackComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CallbackComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CallbackComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@ -0,0 +1,60 @@
<mat-card>
<mat-card-header>
<mat-card-title-group>
<mat-card-title>Add Connection</mat-card-title>
<mat-card-subtitle></mat-card-subtitle>
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<mat-form-field>
<mat-label>Connection Name</mat-label>
<input matInput
[formControl]="nameControl" />
@if (nameControl.invalid && (nameControl.dirty || nameControl.touched)) {
@if (nameControl.hasError('required')) {
<small class="error">This field is required.</small>
}
}
</mat-form-field>
<mat-form-field>
<mat-label>Client Type</mat-label>
<mat-select [formControl]="typeControl">
<mat-option value="nightbot">Nightbot</mat-option>
<mat-option value="twitch">Twitch</mat-option>
</mat-select>
@if (typeControl.invalid && (typeControl.dirty || typeControl.touched)) {
@if (typeControl.hasError('required')) {
<small class="error">This field is required.</small>
}
}
</mat-form-field>
<mat-form-field>
<mat-label>Client Id</mat-label>
<input matInput
[formControl]="clientIdControl" />
@if (clientIdControl.invalid && (clientIdControl.dirty || clientIdControl.touched)) {
@if (clientIdControl.hasError('required')) {
<small class="error">This field is required.</small>
}
}
</mat-form-field>
</mat-card-content>
<mat-card-actions class="actions"
align="end">
<button mat-raised-button
class="warning"
(click)="dialogRef.close()">Cancel</button>
<button mat-raised-button
class="confirm"
disabled="{{form.invalid || waitForResponse}}"
(click)="submit()">Add</button>
</mat-card-actions>
@if (responseError) {
<mat-card-footer>
<small class="error below">{{responseError}}</small>
</mat-card-footer>
}
</mat-card>

View File

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

View File

@ -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<ConnectionItemEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ConnectionItemEditComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ConnectionItemEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<string>('', [Validators.required]);
readonly clientIdControl = new FormControl<string>('', [Validators.required]);
readonly typeControl = new FormControl<string>('', [Validators.required]);
readonly form = new FormGroup({
name: this.nameControl,
clientId: this.clientIdControl,
type: this.typeControl,
});
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
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);
}
}

View File

@ -0,0 +1,19 @@
<section [class.twitch]="connection().type == 'twitch'"
[class.spotify]="connection().type == 'spotify'">
{{connection().name}}
<article class="right">
<button mat-button
class="neutral"
(click)="renew(connection())">
<mat-icon>refresh</mat-icon>
Renew
</button>
<button mat-button
class="danger"
(click)="delete(connection())">
<mat-icon>delete</mat-icon>
Delete
</button>
</article>
</section>

View File

@ -0,0 +1,11 @@
section {
padding: 1em;
}
.right {
float: right;
}
button {
margin: 0 0.5em;
}

View File

@ -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<ConnectionItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ConnectionItemComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ConnectionItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@ -0,0 +1,38 @@
<ul>
<li class="header">
<mat-form-field appearance="outline"
subscriptSizing="dynamic">
<mat-label>Name Filter</mat-label>
<input matInput
placeholder="Filter connections by name"
[formControl]="searchControl" />
</mat-form-field>
<mat-form-field appearance="outline"
subscriptSizing="dynamic">
<mat-label>Type Filter</mat-label>
<mat-select [formControl]="typeControl">
<mat-option value="">All</mat-option>
<mat-option value="nightbot">Nightbot</mat-option>
<mat-option value="twitch">Twitch</mat-option>
</mat-select>
</mat-form-field>
<button mat-icon-button
(click)="add()">
<mat-icon>add</mat-icon>
</button>
</li>
@for (connection of connections; track $index) {
<li>
<connection-item [connection]="connection" />
</li>
}
@if (!connections.length) {
@if (searchControl.value) {
<p class="notice">No connections matches the filter.</p>
} @else {
<p class="notice">No connections available.</p>
}
}
</ul>

View File

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

View File

@ -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<ConnectionListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ConnectionListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ConnectionListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<string>('');
readonly typeControl = new FormControl<string>('');
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);
}
}

View File

@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class ConnectionsModule { }

View File

@ -0,0 +1,3 @@
<h3>Connections</h3>
<connection-list [connections]="connections"/>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConnectionsComponent } from './connections.component';
describe('ConnectionsComponent', () => {
let component: ConnectionsComponent;
let fixture: ComponentFixture<ConnectionsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ConnectionsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ConnectionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@ -4,13 +4,17 @@ article {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border-radius: 15px; border-radius: 15px;
padding: 1em; padding: 0.5em 1em;
& :first-child { & > :first-child {
min-width: 180px; min-width: 200px;
} }
& :not(:first-child) { & > :last-child {
min-width: 100px;
}
& > :not(:first-child) {
text-align: center; text-align: center;
align-self: center; align-self: center;
} }

View File

@ -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) { public createGroup(name: string, priority: number) {
if (!this.logged_in) if (!this.logged_in)
return; 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) { public deleteGroup(id: string) {
if (!this.logged_in) if (!this.logged_in)
return; 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() { public fetchFilters() {
if (!this.logged_in) if (!this.logged_in)
return; return;

View File

@ -42,14 +42,19 @@
Redemptions Redemptions
</a> </a>
</li> </li>
@if (isAdmin()) {
<li> <li>
<a routerLink="/groups" <a routerLink="/groups"
routerLinkActive="active"> routerLinkActive="active">
Groups Groups
</a> </a>
</li> </li>
<li>
<a routerLink="/connections"
routerLinkActive="active">
Connections
</a>
</li>
} }
}
</ul> </ul>
</nav> </nav>

View File

@ -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) this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_group_permission' && d.d.request.data.path == this.pathControl.value)
.subscribe({ .subscribe({
next: (d) => { next: (d) => {
console.log('sdifhsdiofs data', d);
if (d.d.error) { if (d.d.error) {
this.responseError = d.d.error; this.responseError = d.d.error;
} else { } else {

View File

@ -0,0 +1,7 @@
export interface ConnectionState {
user_id: string;
name: string;
type: string;
client_id: string;
grant_type: string;
}

View File

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

View File

@ -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<Connection[]> {
constructor(private service: ConnectionService) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Connection[]> {
return this.service.fetch();
}
}

View File

@ -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<ConnectionState[]> {
constructor(private service: ConnectionService) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ConnectionState[]> {
return this.service.fetch();
}
}

View File

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

View File

@ -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<any> | undefined;
update$: Observable<any> | undefined;
delete$: Observable<any> | 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 $;
}
}

View File

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

View File

@ -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<any> | undefined;
update$: Observable<any> | undefined;
delete$: Observable<any> | 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 $;
}
}

View File

@ -10,7 +10,6 @@ import { Group } from '../../shared/models/group';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { group } from 'console';
@Component({ @Component({
selector: 'app-twitch-user-item-add', selector: 'app-twitch-user-item-add',