Added connections. Added url redirect for login.
This commit is contained in:
parent
56deb3384c
commit
6e5efab5ec
@ -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)
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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],
|
||||
}
|
||||
];
|
||||
|
3
src/app/connections/callback/callback.component.html
Normal file
3
src/app/connections/callback/callback.component.html
Normal file
@ -0,0 +1,3 @@
|
||||
@if (success || failure) {
|
||||
<p>Automatically going back to the connections page soon...</p>
|
||||
}
|
23
src/app/connections/callback/callback.component.spec.ts
Normal file
23
src/app/connections/callback/callback.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
48
src/app/connections/callback/callback.component.ts
Normal file
48
src/app/connections/callback/callback.component.ts
Normal 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)
|
||||
});
|
||||
;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -0,0 +1,11 @@
|
||||
section {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 0.5em;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
12
src/app/connections/connections.module.ts
Normal file
12
src/app/connections/connections.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class ConnectionsModule { }
|
@ -0,0 +1,3 @@
|
||||
<h3>Connections</h3>
|
||||
|
||||
<connection-list [connections]="connections"/>
|
@ -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();
|
||||
});
|
||||
});
|
41
src/app/connections/connections/connections.component.ts
Normal file
41
src/app/connections/connections/connections.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -42,14 +42,19 @@
|
||||
Redemptions
|
||||
</a>
|
||||
</li>
|
||||
@if (isAdmin()) {
|
||||
<li>
|
||||
<a routerLink="/groups"
|
||||
routerLinkActive="active">
|
||||
Groups
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/connections"
|
||||
routerLinkActive="active">
|
||||
Connections
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
</ul>
|
||||
</nav>
|
@ -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 {
|
||||
|
7
src/app/shared/models/connection-state.ts
Normal file
7
src/app/shared/models/connection-state.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface ConnectionState {
|
||||
user_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
client_id: string;
|
||||
grant_type: string;
|
||||
}
|
11
src/app/shared/models/connection.ts
Normal file
11
src/app/shared/models/connection.ts
Normal 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;
|
||||
}
|
14
src/app/shared/resolvers/connection-resolver.ts
Normal file
14
src/app/shared/resolvers/connection-resolver.ts
Normal 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();
|
||||
}
|
||||
}
|
14
src/app/shared/resolvers/connection-state-resolver.ts
Normal file
14
src/app/shared/resolvers/connection-state-resolver.ts
Normal 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();
|
||||
}
|
||||
}
|
16
src/app/shared/services/connection-state.service.spec.ts
Normal file
16
src/app/shared/services/connection-state.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
74
src/app/shared/services/connection-state.service.ts
Normal file
74
src/app/shared/services/connection-state.service.ts
Normal 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 $;
|
||||
}
|
||||
}
|
16
src/app/shared/services/connection.service.spec.ts
Normal file
16
src/app/shared/services/connection.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
75
src/app/shared/services/connection.service.ts
Normal file
75
src/app/shared/services/connection.service.ts
Normal 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 $;
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user