Added top bar on all pages. Simplified TTS login component. Fixed some issues. Removed redirects for now.
This commit is contained in:
parent
d44ec50a6a
commit
055885837c
@ -1,4 +1,10 @@
|
|||||||
<main class="main">
|
<main>
|
||||||
<navigation class="navigation" />
|
<topbar class="top" />
|
||||||
<router-outlet class="content" />
|
<div [class.container]="isSidebarOpen"
|
||||||
|
[class.full]="!isSidebarOpen">
|
||||||
|
@if (isSidebarOpen) {
|
||||||
|
<sidebar class="navigation" />
|
||||||
|
}
|
||||||
|
<router-outlet class="content" />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
@ -1,4 +1,9 @@
|
|||||||
.main {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20em 0px 1fr;
|
grid-template-columns: 20em 0px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
@ -1,10 +1,9 @@
|
|||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy, inject, HostBinding } from '@angular/core';
|
import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy, inject, HostBinding } from '@angular/core';
|
||||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
import { Router, RouterOutlet } from '@angular/router';
|
||||||
import { HermesClientService } from './hermes-client.service';
|
import { HermesClientService } from './hermes-client.service';
|
||||||
import { AuthUserGuard } from './shared/auth/auth.user.guard'
|
import { AuthUserGuard } from './shared/auth/auth.user.guard'
|
||||||
import { first, Subscription, timeout } from 'rxjs';
|
import { first, Subscription, timeout } from 'rxjs';
|
||||||
import { NavigationComponent } from "./navigation/navigation.component";
|
|
||||||
import EventService from './shared/services/EventService';
|
import EventService from './shared/services/EventService';
|
||||||
import { ApiAuthenticationService } from './shared/services/api/api-authentication.service';
|
import { ApiAuthenticationService } from './shared/services/api/api-authentication.service';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
@ -12,14 +11,23 @@ import { ApiKeyService } from './shared/services/api/api-key.service';
|
|||||||
import ApiKey from './shared/models/api-key';
|
import ApiKey from './shared/models/api-key';
|
||||||
import { ThemeService } from './shared/services/theme.service';
|
import { ThemeService } from './shared/services/theme.service';
|
||||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { SidebarComponent } from "./navigation/sidebar/sidebar.component";
|
||||||
|
import { Topbar as TopbarComponent } from "./navigation/topbar/topbar.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
RouterOutlet,
|
|
||||||
AuthModule,
|
AuthModule,
|
||||||
NavigationComponent
|
RouterOutlet,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
SidebarComponent,
|
||||||
|
TopbarComponent,
|
||||||
],
|
],
|
||||||
providers: [AuthUserGuard],
|
providers: [AuthUserGuard],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
@ -34,6 +42,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private ngZone: NgZone;
|
private ngZone: NgZone;
|
||||||
private subscriptions: Subscription[];
|
private subscriptions: Subscription[];
|
||||||
|
|
||||||
|
authentication = inject(ApiAuthenticationService);
|
||||||
|
isSidebarOpen: boolean = true
|
||||||
|
|
||||||
@HostBinding('class.dark-theme')
|
@HostBinding('class.dark-theme')
|
||||||
get isDarkTheme() {
|
get isDarkTheme() {
|
||||||
@ -45,7 +55,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
return this.themeService.isLightTheme();
|
return this.themeService.isLightTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {
|
constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||||
this.ngZone = ngZone;
|
this.ngZone = ngZone;
|
||||||
this.isBrowser = isPlatformBrowser(this.platformId);
|
this.isBrowser = isPlatformBrowser(this.platformId);
|
||||||
@ -54,20 +63,17 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => {
|
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => {
|
||||||
const url = router.url;
|
const url = router.url;
|
||||||
const params = router.parseUrl(url).queryParams;
|
const params = router.parseUrl(url).queryParams;
|
||||||
|
const redirect = params['rd'];
|
||||||
|
|
||||||
if (params && 'rd' in params) {
|
if (redirect && !(url.startsWith(redirect) || redirect.startsWith(url))) {
|
||||||
await this.router.navigate([params['rd']]);
|
await this.router.navigate([redirect]);
|
||||||
} else if (url == '/' || url.startsWith('/login') || url.startsWith('/tts-login')) {
|
} else if (url == '/' || url.startsWith('/login') || url.startsWith('/tts-login')) {
|
||||||
await this.router.navigate(['policies']);
|
await this.router.navigate(['policies']);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
this.subscriptions.push(this.events.listen('tts_logoff', async _ => {
|
|
||||||
await this.router.navigate(['tts-login'], {
|
this.subscriptions.push(this.events.listen('tts_logoff', async _ => await this.router.navigate(['tts-login'])));
|
||||||
queryParams: {
|
this.subscriptions.push(this.events.listen('toggle_sidebar', () => this.isSidebarOpen = !this.isSidebarOpen))
|
||||||
rd: this.router.url.substring(1)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
|||||||
import { PolicyComponent } from './policies/policy/policy.component';
|
import { PolicyComponent } from './policies/policy/policy.component';
|
||||||
import { AuthUserGuard } from './shared/auth/auth.user.guard';
|
import { AuthUserGuard } from './shared/auth/auth.user.guard';
|
||||||
import { LoginComponent } from './auth/login/login.component';
|
import { LoginComponent } from './auth/login/login.component';
|
||||||
import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
|
|
||||||
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
|
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
|
||||||
import { FiltersComponent } from './tts-filters/filters/filters.component';
|
import { FiltersComponent } from './tts-filters/filters/filters.component';
|
||||||
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
||||||
@ -24,6 +23,7 @@ import { ConnectionsComponent } from './connections/connections/connections.comp
|
|||||||
import ConnectionResolver from './shared/resolvers/connection-resolver';
|
import ConnectionResolver from './shared/resolvers/connection-resolver';
|
||||||
import { ConnectionCallbackComponent } from './connections/callback/callback.component';
|
import { ConnectionCallbackComponent } from './connections/callback/callback.component';
|
||||||
import { KeysComponent } from './keys/keys/keys.component';
|
import { KeysComponent } from './keys/keys/keys.component';
|
||||||
|
import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { LoginComponent } from './login/login.component';
|
import { LoginComponent } from './login/login.component';
|
||||||
import { TtsLoginComponent } from './tts-login/tts-login.component';
|
|
||||||
import { ImpersonationComponent } from './impersonation/impersonation.component';
|
import { ImpersonationComponent } from './impersonation/impersonation.component';
|
||||||
import { UserCardComponent } from './user-card/user-card.component';
|
import { UserCardComponent } from './user-card/user-card.component';
|
||||||
|
|
||||||
@ -8,7 +7,6 @@ import { UserCardComponent } from './user-card/user-card.component';
|
|||||||
declarations: [],
|
declarations: [],
|
||||||
imports: [
|
imports: [
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
TtsLoginComponent,
|
|
||||||
ImpersonationComponent,
|
ImpersonationComponent,
|
||||||
UserCardComponent,
|
UserCardComponent,
|
||||||
]
|
]
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
@if (isAdmin()) {
|
@if (isAdmin()) {
|
||||||
<main>
|
<main>
|
||||||
<mat-form-field>
|
<mat-form-field class="mat-small"
|
||||||
|
subscriptSizing="dynamic">
|
||||||
<mat-label>User to impersonate</mat-label>
|
<mat-label>User to impersonate</mat-label>
|
||||||
<mat-select (selectionChange)="onChange($event)"
|
<mat-select [formControl]="impersonationControl">
|
||||||
[(value)]="impersonated">
|
|
||||||
<mat-option>{{getUsername()}}</mat-option>
|
<mat-option>{{getUsername()}}</mat-option>
|
||||||
@for (user of users; track user.id) {
|
@for (user of users; track user.id) {
|
||||||
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
||||||
|
@ -2,5 +2,4 @@ main {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 1em;
|
|
||||||
}
|
}
|
@ -8,56 +8,76 @@ import { environment } from '../../../environments/environment';
|
|||||||
import EventService from '../../shared/services/EventService';
|
import EventService from '../../shared/services/EventService';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { timeout, first } from 'rxjs';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import ApiKey from '../../shared/models/api-key';
|
import { User } from '../../shared/models/user';
|
||||||
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
import { UserService } from '../../shared/services/user.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'impersonation',
|
selector: 'impersonation',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatCardModule, MatSelectModule],
|
imports: [
|
||||||
|
MatCardModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
templateUrl: './impersonation.component.html',
|
templateUrl: './impersonation.component.html',
|
||||||
styleUrl: './impersonation.component.scss'
|
styleUrl: './impersonation.component.scss'
|
||||||
})
|
})
|
||||||
export class ImpersonationComponent implements OnInit {
|
export class ImpersonationComponent implements OnInit {
|
||||||
private readonly keyService = inject(ApiKeyService);
|
private readonly events = inject(EventService);
|
||||||
|
private readonly userService = inject(UserService);
|
||||||
|
|
||||||
impersonated: string | undefined;
|
impersonationControl = new FormControl<string | undefined>(undefined);
|
||||||
users: { id: string, name: string }[];
|
users: User[];
|
||||||
|
|
||||||
constructor(private client: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private events: EventService, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
|
constructor(private client: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||||
this.users = []
|
this.users = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!isPlatformBrowser(this.platformId) || !this.auth.isAdmin()) {
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http.get(environment.API_HOST + '/admin/users', {
|
this.userService.fetch().subscribe(users => {
|
||||||
headers: {
|
this.users = users.filter((d: any) => d.name != this.auth.getUsername());
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
|
||||||
}
|
|
||||||
}).subscribe((data: any) => {
|
|
||||||
this.users = data.filter((d: any) => d.name != this.auth.getUsername());
|
|
||||||
const id = this.auth.getImpersonatedId();
|
const id = this.auth.getImpersonatedId();
|
||||||
if (id && this.users.find(u => u.id == id)) {
|
if (id && this.users.find(u => u.id == id)) {
|
||||||
this.impersonated = id;
|
this.impersonationControl.setValue(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.events.listen('impersonation', (userId) => {
|
this.impersonationControl.valueChanges.subscribe((impersonationId) => {
|
||||||
const url = this.router.url;
|
if (!this.auth.isAdmin() || impersonationId == this.auth.getImpersonatedId())
|
||||||
this.client.first(d => d.op == 2 && !d.d.another_client)
|
return;
|
||||||
.subscribe(async _ =>
|
|
||||||
await setTimeout(async () =>
|
if (!impersonationId) {
|
||||||
await this.router.navigate([url.substring(1)]), 500));
|
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||||
this.keyService.fetch()
|
headers: {
|
||||||
.pipe(timeout(3000), first())
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
.subscribe(async (d: ApiKey[]) => {
|
},
|
||||||
if (d.length > 0)
|
body: {
|
||||||
this.client.login(d[0].id);
|
impersonation: impersonationId
|
||||||
|
}
|
||||||
|
}).subscribe(async (data: any) => {
|
||||||
|
this.impersonationControl.setValue(undefined);
|
||||||
|
this.client.disconnect(true);
|
||||||
|
this.events.emit('impersonation', undefined);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
||||||
|
impersonation: impersonationId
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
|
}
|
||||||
|
}).subscribe(async (data: any) => {
|
||||||
|
this.impersonationControl.setValue(impersonationId);
|
||||||
|
this.client.disconnect(true);
|
||||||
|
this.events.emit('impersonation', impersonationId);
|
||||||
|
await this.router.navigate(['tts-login']);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,35 +88,4 @@ export class ImpersonationComponent implements OnInit {
|
|||||||
public getUsername() {
|
public getUsername() {
|
||||||
return this.auth.getUsername();
|
return this.auth.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChange(e: any) {
|
|
||||||
if (!this.auth.isAdmin())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!e.value) {
|
|
||||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
impersonation: e.value
|
|
||||||
}
|
|
||||||
}).subscribe(async (data: any) => {
|
|
||||||
this.client.disconnect();
|
|
||||||
this.events.emit('impersonation', e.value);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
|
||||||
impersonation: e.value
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
|
||||||
}
|
|
||||||
}).subscribe(async (data: any) => {
|
|
||||||
this.client.disconnect();
|
|
||||||
this.events.emit('impersonation', e.value);
|
|
||||||
await this.router.navigate(['tts-login']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,32 +1,15 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { Router, RouterModule } from '@angular/router';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'login',
|
selector: 'login',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatCardModule, RouterModule],
|
imports: [MatCardModule],
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrl: './login.component.scss'
|
styleUrl: './login.component.scss'
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent {
|
||||||
subscription: Subscription | null;
|
|
||||||
|
|
||||||
constructor(private router: Router) {
|
|
||||||
this.subscription = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (this.subscription)
|
|
||||||
this.subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
document.location.replace(environment.API_HOST + '/auth');
|
document.location.replace(environment.API_HOST + '/auth');
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<mat-card-content class="content">
|
<mat-card-content class="content">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>API Key</mat-label>
|
<mat-label>API Key</mat-label>
|
||||||
<mat-select [(value)]="selected_api_key">
|
<mat-select [formControl]="keyControl">
|
||||||
@for (key of api_keys; track key.id) {
|
@for (key of api_keys; track key.id) {
|
||||||
<mat-option [value]="key.id">{{key.label}}</mat-option>
|
<mat-option [value]="key.id">{{key.label}}</mat-option>
|
||||||
}
|
}
|
||||||
|
@ -1,58 +1,41 @@
|
|||||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import EventService from '../../shared/services/EventService';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { first, Subscription, timeout } from 'rxjs';
|
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tts-login',
|
selector: 'tts-login',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatButtonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
templateUrl: './tts-login.component.html',
|
templateUrl: './tts-login.component.html',
|
||||||
styleUrl: './tts-login.component.scss'
|
styleUrl: './tts-login.component.scss'
|
||||||
})
|
})
|
||||||
export class TtsLoginComponent implements OnInit, OnDestroy {
|
export class TtsLoginComponent implements OnInit {
|
||||||
private readonly client = inject(HermesClientService);
|
private readonly client = inject(HermesClientService);
|
||||||
private readonly keyService = inject(ApiKeyService);
|
|
||||||
private readonly events = inject(EventService);
|
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
|
||||||
|
keyControl = new FormControl<string | null>('');
|
||||||
api_keys: { id: string, label: string }[] = [];
|
api_keys: { id: string, label: string }[] = [];
|
||||||
selected_api_key: string | undefined;
|
|
||||||
|
|
||||||
private subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.data.subscribe(d => this.api_keys = d['keys']);
|
this.route.data.subscribe(d => this.api_keys = d['keys']);
|
||||||
|
|
||||||
this.subscriptions.push(this.events.listen('tts_logoff', async _ => {
|
|
||||||
this.selected_api_key = undefined;
|
|
||||||
}));
|
|
||||||
this.subscriptions.push(this.events.listen('impersonation', _ => {
|
|
||||||
this.selected_api_key = undefined;
|
|
||||||
|
|
||||||
this.keyService.fetch()
|
|
||||||
.pipe(timeout(3000), first())
|
|
||||||
.subscribe(d => this.api_keys = d);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subscriptions.forEach(s => s.unsubscribe());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
login(): void {
|
login(): void {
|
||||||
if (!this.selected_api_key)
|
if (!this.keyControl.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.client.login(this.selected_api_key);
|
this.client.login(this.keyControl.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,14 +35,20 @@ export class ConnectionCallbackComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http.get(`${environment.API_HOST}/auth/connections?token=${params['access_token']}&state=${params['state']}&expires_in=${params['expires_in']}`).subscribe(async (d: any) => {
|
console.log(params);
|
||||||
const data = d.data;
|
this.http.get(`${environment.API_HOST}/auth/connections?token=${params['access_token']}&state=${params['state']}&expires_in=${params['expires_in']}`).subscribe({
|
||||||
this.success = true;
|
next: async (d: any) => {
|
||||||
|
const data = d.data;
|
||||||
|
this.success = true;
|
||||||
|
|
||||||
await setTimeout(async () => {
|
console.log('about to wait for 2 seconds')
|
||||||
this.client.createConnection(data.connection.name, data.connection.type, data.connection.clientId, params['access_token'], data.connection.grantType, params['scope'], data.expires_at);
|
await setTimeout(async () => {
|
||||||
await this.router.navigate(['connections']);
|
console.log('create connection')
|
||||||
}, 2000)
|
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)
|
||||||
|
},
|
||||||
|
error: async () => await this.router.navigate(['connections'])
|
||||||
});
|
});
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export class HermesClientService {
|
|||||||
return this.listen();
|
return this.listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect() {
|
public disconnect(impersonated: boolean = false) {
|
||||||
if (!this.connected)
|
if (!this.connected)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ export class HermesClientService {
|
|||||||
this.session_id = undefined;
|
this.session_id = undefined;
|
||||||
this.api_key = undefined;
|
this.api_key = undefined;
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
this.events.emit('tts_logoff', null);
|
this.events.emit('tts_logoff', impersonated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public filter(predicate: (data: any) => boolean): Observable<any> | undefined {
|
public filter(predicate: (data: any) => boolean): Observable<any> | undefined {
|
||||||
|
12
src/app/navigation/navigation.module.ts
Normal file
12
src/app/navigation/navigation.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class NavigationModule { }
|
@ -1,24 +1,7 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<user-card class="card" />
|
<ul>
|
||||||
<ul class="">
|
@if (isLoggedIn()) {
|
||||||
@if (!isLoggedIn()) {
|
@if (isTTSLoggedIn()) {
|
||||||
<li>
|
|
||||||
<a routerLink="/login"
|
|
||||||
routerLinkActive="active">
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if (isLoggedIn() && !isTTSLoggedIn()) {
|
|
||||||
<li>
|
|
||||||
<a mat-raised-button
|
|
||||||
routerLink="/tts-login"
|
|
||||||
routerLinkActive="active">
|
|
||||||
TTS Login
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if (isLoggedIn() && isTTSLoggedIn()) {
|
|
||||||
<li>
|
<li>
|
||||||
<a mat-raised-button
|
<a mat-raised-button
|
||||||
routerLink="/policies"
|
routerLink="/policies"
|
||||||
@ -61,8 +44,15 @@
|
|||||||
Connections
|
Connections
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
} @else {
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/tts-login"
|
||||||
|
routerLinkActive="active">
|
||||||
|
TTS Login
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
@if (isLoggedIn()) {
|
|
||||||
<li>
|
<li>
|
||||||
<a mat-raised-button
|
<a mat-raised-button
|
||||||
routerLink="/keys"
|
routerLink="/keys"
|
||||||
@ -70,6 +60,13 @@
|
|||||||
API Keys
|
API Keys
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
} @else {
|
||||||
|
<li>
|
||||||
|
<a routerLink="/login"
|
||||||
|
routerLinkActive="active">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
@ -1,18 +1,18 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { NavigationComponent } from './navigation.component';
|
import { SidebarComponent } from './sidebar.component';
|
||||||
|
|
||||||
describe('NavigationComponent', () => {
|
describe('NavigationComponent', () => {
|
||||||
let component: NavigationComponent;
|
let component: SidebarComponent;
|
||||||
let fixture: ComponentFixture<NavigationComponent>;
|
let fixture: ComponentFixture<SidebarComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NavigationComponent]
|
imports: [SidebarComponent]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NavigationComponent);
|
fixture = TestBed.createComponent(SidebarComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@ -1,26 +1,30 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { HermesClientService } from '../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
|
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../../auth/auth.module';
|
||||||
import { UserCardComponent } from "../auth/user-card/user-card.component";
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'navigation',
|
selector: 'sidebar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
AuthModule,
|
AuthModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatSidenavModule,
|
||||||
|
MatToolbarModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
UserCardComponent,
|
|
||||||
],
|
],
|
||||||
templateUrl: './navigation.component.html',
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrl: './navigation.component.scss'
|
styleUrl: './sidebar.component.scss'
|
||||||
})
|
})
|
||||||
export class NavigationComponent {
|
export class SidebarComponent {
|
||||||
constructor(private auth: ApiAuthenticationService, private hermes: HermesClientService) { }
|
constructor(private auth: ApiAuthenticationService, private hermes: HermesClientService) { }
|
||||||
|
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
28
src/app/navigation/topbar/topbar.component.html
Normal file
28
src/app/navigation/topbar/topbar.component.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<mat-toolbar class="top">
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="toggleSidebar()">
|
||||||
|
<mat-icon>menu</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span>Tom-to-Speech</span>
|
||||||
|
@if (isTTSLoggedIn) {
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<div class="links">
|
||||||
|
@if (showImpersonation) {
|
||||||
|
<impersonation />
|
||||||
|
} @else {
|
||||||
|
<div class="userInfo">
|
||||||
|
<span class="username">{{impersonatedName ?? username}}</span>
|
||||||
|
@if (impersonatedId) {
|
||||||
|
<br />
|
||||||
|
<span class="impersonated">Impersonating from {{username}}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="showImpersonation = !showImpersonation">
|
||||||
|
<mat-icon>supervisor_account</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</mat-toolbar>
|
22
src/app/navigation/topbar/topbar.component.scss
Normal file
22
src/app/navigation/topbar/topbar.component.scss
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links > * {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfo {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 10px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impersonated {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
impersonation {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
23
src/app/navigation/topbar/topbar.component.spec.ts
Normal file
23
src/app/navigation/topbar/topbar.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TopbarComponent } from './topbar.component';
|
||||||
|
|
||||||
|
describe('TopbarComponent', () => {
|
||||||
|
let component: TopbarComponent;
|
||||||
|
let fixture: ComponentFixture<TopbarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TopbarComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TopbarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
76
src/app/navigation/topbar/topbar.component.ts
Normal file
76
src/app/navigation/topbar/topbar.component.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Component, inject, OnDestroy } from '@angular/core';
|
||||||
|
import { AuthVisitorGuard } from '../../shared/auth/auth.visitor.guard';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { AuthModule } from '../../auth/auth.module';
|
||||||
|
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||||
|
import { ImpersonationComponent } from '../../auth/impersonation/impersonation.component';
|
||||||
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
import EventService from '../../shared/services/EventService';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'topbar',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AuthModule,
|
||||||
|
ImpersonationComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
],
|
||||||
|
providers: [AuthVisitorGuard],
|
||||||
|
templateUrl: './topbar.component.html',
|
||||||
|
styleUrl: './topbar.component.scss'
|
||||||
|
})
|
||||||
|
export class Topbar implements OnDestroy {
|
||||||
|
private readonly auth = inject(ApiAuthenticationService);
|
||||||
|
private readonly client = inject(HermesClientService);
|
||||||
|
private readonly events = inject(EventService);
|
||||||
|
|
||||||
|
private subscriptions: (Subscription | null)[] = [];
|
||||||
|
private _showImpersonation: boolean = false
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.subscriptions.push(this.events.listen('impersonation', () => this.showImpersonation = false));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
for (let subscription of this.subscriptions) {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTTSLoggedIn() {
|
||||||
|
return this.client.logged_in;
|
||||||
|
}
|
||||||
|
|
||||||
|
get username() {
|
||||||
|
return this.auth.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
get impersonatedId() {
|
||||||
|
return this.auth.getImpersonatedId();
|
||||||
|
}
|
||||||
|
|
||||||
|
get impersonatedName() {
|
||||||
|
return this.auth.getImpersonatedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
get showImpersonation() {
|
||||||
|
return this._showImpersonation;
|
||||||
|
}
|
||||||
|
|
||||||
|
set showImpersonation(value: any) {
|
||||||
|
if (this.auth.isAdmin()) {
|
||||||
|
this._showImpersonation = !!value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
this.events.emit('toggle_sidebar', undefined);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
import { Component, inject, OnDestroy, signal } from '@angular/core';
|
||||||
import RedemptionService from '../../shared/services/redemption.service';
|
import RedemptionService from '../../shared/services/redemption.service';
|
||||||
import Redemption from '../../shared/models/redemption';
|
import Redemption from '../../shared/models/redemption';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
4
src/app/shared/models/user.ts
Normal file
4
src/app/shared/models/user.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
@ -14,6 +14,8 @@ export class ApiAuthenticationService {
|
|||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.lastCheck = new Date();
|
this.lastCheck = new Date();
|
||||||
|
|
||||||
|
this.events.listen('impersonation', _ => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
isAuthenticated() {
|
||||||
@ -28,6 +30,10 @@ export class ApiAuthenticationService {
|
|||||||
return this.user?.impersonation?.id;
|
return this.user?.impersonation?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getImpersonatedName() {
|
||||||
|
return this.user?.impersonation?.name;
|
||||||
|
}
|
||||||
|
|
||||||
getUsername() {
|
getUsername() {
|
||||||
return this.user?.name;
|
return this.user?.name;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,16 @@ export class ApiKeyService {
|
|||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.events.listen('tts_logoff', (impersonation) => {
|
||||||
|
console.log('tts_logoff triggered:', impersonation);
|
||||||
|
if (impersonation) {
|
||||||
|
this.keys = [];
|
||||||
|
this.loaded = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.events.listen('logoff', () => {
|
this.events.listen('logoff', () => {
|
||||||
|
console.log('logoff triggered');
|
||||||
this.keys = [];
|
this.keys = [];
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
});
|
});
|
||||||
|
@ -41,7 +41,7 @@ export default class TtsFilterService {
|
|||||||
|
|
||||||
fetch() {
|
fetch() {
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
return of(this.data).pipe(first());
|
return of(this.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $ = this.client.first(d => d.op == 4 && d.d.request.type == 'get_tts_word_filters')!.pipe(map(d => d.d.data));
|
const $ = this.client.first(d => d.op == 4 && d.d.request.type == 'get_tts_word_filters')!.pipe(map(d => d.d.data));
|
||||||
|
@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import TwitchRedemption from '../models/twitch-redemption';
|
import TwitchRedemption from '../models/twitch-redemption';
|
||||||
import { of } from 'rxjs';
|
import { catchError, EMPTY, Observable, of } from 'rxjs';
|
||||||
import EventService from './EventService';
|
import EventService from './EventService';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -31,10 +31,12 @@ export default class TwitchRedemptionService {
|
|||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt'),
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt'),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$.subscribe(d => {
|
$.pipe(catchError(() => EMPTY))
|
||||||
this.twitchRedemptions = d;
|
.subscribe({
|
||||||
this.loaded = true;
|
next: d => this.twitchRedemptions = d,
|
||||||
});
|
error: d => console.log('Twitch API redemptions:', d.error),
|
||||||
|
complete: () => this.loaded = true,
|
||||||
|
});
|
||||||
return $;
|
return $;
|
||||||
}
|
}
|
||||||
}
|
}
|
16
src/app/shared/services/user.service.spec.ts
Normal file
16
src/app/shared/services/user.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
|
describe('UsersService', () => {
|
||||||
|
let service: UserService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(UserService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
42
src/app/shared/services/user.service.ts
Normal file
42
src/app/shared/services/user.service.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { of, catchError, EMPTY } from 'rxjs';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import EventService from './EventService';
|
||||||
|
import { User } from '../models/user';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UserService {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly events = inject(EventService);
|
||||||
|
private users: User[] = [];
|
||||||
|
private loaded = false;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.events.listen('logoff', () => {
|
||||||
|
this.users = [];
|
||||||
|
this.loaded = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
if (this.loaded)
|
||||||
|
return of(this.users);
|
||||||
|
|
||||||
|
const $ = this.http.get<User[]>(environment.API_HOST + '/admin/users', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt'),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.pipe(catchError(() => EMPTY))
|
||||||
|
.subscribe({
|
||||||
|
next: d => this.users = d,
|
||||||
|
error: d => console.log('user service error:', d.error),
|
||||||
|
complete: () => this.loaded = true,
|
||||||
|
});
|
||||||
|
return $;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user