-
-
+
+
+
+ @if (isSidebarOpen) {
+
+ }
+
+
\ No newline at end of file
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index a6e184d..4befec6 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -1,4 +1,9 @@
-.main {
+.container {
display: grid;
grid-template-columns: 20em 0px 1fr;
+}
+
+.full {
+ width: 80%;
+ margin: 0 auto;
}
\ No newline at end of file
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 0e36c9a..0b8bc10 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,10 +1,9 @@
import { isPlatformBrowser } from '@angular/common';
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 { AuthUserGuard } from './shared/auth/auth.user.guard'
import { first, Subscription, timeout } from 'rxjs';
-import { NavigationComponent } from "./navigation/navigation.component";
import EventService from './shared/services/EventService';
import { ApiAuthenticationService } from './shared/services/api/api-authentication.service';
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 { ThemeService } from './shared/services/theme.service';
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({
selector: 'app-root',
standalone: true,
imports: [
- RouterOutlet,
AuthModule,
- NavigationComponent
+ RouterOutlet,
+ MatButtonModule,
+ MatIconModule,
+ MatToolbarModule,
+ SidebarComponent,
+ TopbarComponent,
],
providers: [AuthUserGuard],
templateUrl: './app.component.html',
@@ -34,6 +42,8 @@ export class AppComponent implements OnInit, OnDestroy {
private ngZone: NgZone;
private subscriptions: Subscription[];
+ authentication = inject(ApiAuthenticationService);
+ isSidebarOpen: boolean = true
@HostBinding('class.dark-theme')
get isDarkTheme() {
@@ -45,7 +55,6 @@ export class AppComponent implements OnInit, OnDestroy {
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) {
this.ngZone = ngZone;
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 _ => {
const url = router.url;
const params = router.parseUrl(url).queryParams;
+ const redirect = params['rd'];
- if (params && 'rd' in params) {
- await this.router.navigate([params['rd']]);
+ if (redirect && !(url.startsWith(redirect) || redirect.startsWith(url))) {
+ await this.router.navigate([redirect]);
} 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'], {
- queryParams: {
- rd: this.router.url.substring(1)
- }
- });
- }));
+
+ this.subscriptions.push(this.events.listen('tts_logoff', async _ => await this.router.navigate(['tts-login'])));
+ this.subscriptions.push(this.events.listen('toggle_sidebar', () => this.isSidebarOpen = !this.isSidebarOpen))
}
ngOnInit(): void {
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 4608be4..b805436 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
import { PolicyComponent } from './policies/policy/policy.component';
import { AuthUserGuard } from './shared/auth/auth.user.guard';
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 { FiltersComponent } from './tts-filters/filters/filters.component';
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 { ConnectionCallbackComponent } from './connections/callback/callback.component';
import { KeysComponent } from './keys/keys/keys.component';
+import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
export const routes: Routes = [
{
diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts
index 86da670..0863a3a 100644
--- a/src/app/auth/auth.module.ts
+++ b/src/app/auth/auth.module.ts
@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { LoginComponent } from './login/login.component';
-import { TtsLoginComponent } from './tts-login/tts-login.component';
import { ImpersonationComponent } from './impersonation/impersonation.component';
import { UserCardComponent } from './user-card/user-card.component';
@@ -8,7 +7,6 @@ import { UserCardComponent } from './user-card/user-card.component';
declarations: [],
imports: [
LoginComponent,
- TtsLoginComponent,
ImpersonationComponent,
UserCardComponent,
]
diff --git a/src/app/auth/impersonation/impersonation.component.html b/src/app/auth/impersonation/impersonation.component.html
index ca64fc0..3afe00c 100644
--- a/src/app/auth/impersonation/impersonation.component.html
+++ b/src/app/auth/impersonation/impersonation.component.html
@@ -1,9 +1,9 @@
@if (isAdmin()) {
-
+
User to impersonate
-
+
{{getUsername()}}
@for (user of users; track user.id) {
{{ user.name }}
diff --git a/src/app/auth/impersonation/impersonation.component.scss b/src/app/auth/impersonation/impersonation.component.scss
index 7269665..737fb4f 100644
--- a/src/app/auth/impersonation/impersonation.component.scss
+++ b/src/app/auth/impersonation/impersonation.component.scss
@@ -2,5 +2,4 @@ main {
display: flex;
justify-content: center;
align-items: center;
- margin-top: 1em;
}
\ No newline at end of file
diff --git a/src/app/auth/impersonation/impersonation.component.ts b/src/app/auth/impersonation/impersonation.component.ts
index 7eed283..c069259 100644
--- a/src/app/auth/impersonation/impersonation.component.ts
+++ b/src/app/auth/impersonation/impersonation.component.ts
@@ -8,56 +8,76 @@ import { environment } from '../../../environments/environment';
import EventService from '../../shared/services/EventService';
import { HermesClientService } from '../../hermes-client.service';
import { Router } from '@angular/router';
-import { timeout, first } from 'rxjs';
-import ApiKey from '../../shared/models/api-key';
-import { ApiKeyService } from '../../shared/services/api/api-key.service';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { User } from '../../shared/models/user';
+import { UserService } from '../../shared/services/user.service';
@Component({
selector: 'impersonation',
standalone: true,
- imports: [MatCardModule, MatSelectModule],
+ imports: [
+ MatCardModule,
+ MatSelectModule,
+ ReactiveFormsModule,
+ ],
templateUrl: './impersonation.component.html',
styleUrl: './impersonation.component.scss'
})
export class ImpersonationComponent implements OnInit {
- private readonly keyService = inject(ApiKeyService);
+ private readonly events = inject(EventService);
+ private readonly userService = inject(UserService);
- impersonated: string | undefined;
- users: { id: string, name: string }[];
+ impersonationControl = new FormControl(undefined);
+ users: User[];
- constructor(private client: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private events: EventService, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
- this.users = []
+ constructor(private client: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
+ this.users = [];
}
ngOnInit(): void {
- if (!isPlatformBrowser(this.platformId) || !this.auth.isAdmin()) {
+ if (!isPlatformBrowser(this.platformId)) {
return;
}
- this.http.get(environment.API_HOST + '/admin/users', {
- headers: {
- 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
- }
- }).subscribe((data: any) => {
- this.users = data.filter((d: any) => d.name != this.auth.getUsername());
+ this.userService.fetch().subscribe(users => {
+ this.users = users.filter((d: any) => d.name != this.auth.getUsername());
const id = this.auth.getImpersonatedId();
if (id && this.users.find(u => u.id == id)) {
- this.impersonated = id;
+ this.impersonationControl.setValue(id);
}
});
- this.events.listen('impersonation', (userId) => {
- const url = this.router.url;
- this.client.first(d => d.op == 2 && !d.d.another_client)
- .subscribe(async _ =>
- await setTimeout(async () =>
- await this.router.navigate([url.substring(1)]), 500));
- this.keyService.fetch()
- .pipe(timeout(3000), first())
- .subscribe(async (d: ApiKey[]) => {
- if (d.length > 0)
- this.client.login(d[0].id);
+ this.impersonationControl.valueChanges.subscribe((impersonationId) => {
+ if (!this.auth.isAdmin() || impersonationId == this.auth.getImpersonatedId())
+ return;
+
+ if (!impersonationId) {
+ this.http.delete(environment.API_HOST + '/admin/impersonate', {
+ headers: {
+ 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
+ },
+ body: {
+ 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() {
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']);
- });
- }
- }
}
\ No newline at end of file
diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts
index 57c01ba..9276804 100644
--- a/src/app/auth/login/login.component.ts
+++ b/src/app/auth/login/login.component.ts
@@ -1,32 +1,15 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
-import { Router, RouterModule } from '@angular/router';
-import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment';
@Component({
selector: 'login',
standalone: true,
- imports: [MatCardModule, RouterModule],
+ imports: [MatCardModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
-export class LoginComponent implements OnInit, OnDestroy {
- subscription: Subscription | null;
-
- constructor(private router: Router) {
- this.subscription = null;
- }
-
- ngOnInit(): void {
-
- }
-
- ngOnDestroy(): void {
- if (this.subscription)
- this.subscription.unsubscribe()
- }
-
+export class LoginComponent {
login() {
document.location.replace(environment.API_HOST + '/auth');
}
diff --git a/src/app/auth/tts-login/tts-login.component.html b/src/app/auth/tts-login/tts-login.component.html
index 7beae39..19c2b0a 100644
--- a/src/app/auth/tts-login/tts-login.component.html
+++ b/src/app/auth/tts-login/tts-login.component.html
@@ -9,7 +9,7 @@
API Key
-
+
@for (key of api_keys; track key.id) {
{{key.label}}
}
diff --git a/src/app/auth/tts-login/tts-login.component.ts b/src/app/auth/tts-login/tts-login.component.ts
index 4ca87ab..e3b7aa1 100644
--- a/src/app/auth/tts-login/tts-login.component.ts
+++ b/src/app/auth/tts-login/tts-login.component.ts
@@ -1,58 +1,41 @@
-import { Component, inject, OnDestroy, OnInit } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { MatInputModule } from '@angular/material/input';
+import { Component, inject, OnInit } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
-import EventService from '../../shared/services/EventService';
import { ActivatedRoute } from '@angular/router';
-import { first, Subscription, timeout } from 'rxjs';
import { HermesClientService } from '../../hermes-client.service';
import { MatCardModule } from '@angular/material/card';
-import { ApiKeyService } from '../../shared/services/api/api-key.service';
@Component({
selector: 'tts-login',
standalone: true,
- imports: [MatButtonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
+ imports: [
+ MatButtonModule,
+ MatCardModule,
+ MatFormFieldModule,
+ MatSelectModule,
+ ReactiveFormsModule,
+ ],
templateUrl: './tts-login.component.html',
styleUrl: './tts-login.component.scss'
})
-export class TtsLoginComponent implements OnInit, OnDestroy {
+export class TtsLoginComponent implements OnInit {
private readonly client = inject(HermesClientService);
- private readonly keyService = inject(ApiKeyService);
- private readonly events = inject(EventService);
private readonly route = inject(ActivatedRoute);
+ keyControl = new FormControl('');
api_keys: { id: string, label: string }[] = [];
- selected_api_key: string | undefined;
-
- private subscriptions: Subscription[] = [];
ngOnInit(): void {
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 {
- if (!this.selected_api_key)
+ if (!this.keyControl.value)
return;
- this.client.login(this.selected_api_key);
+ this.client.login(this.keyControl.value);
}
}
diff --git a/src/app/connections/callback/callback.component.ts b/src/app/connections/callback/callback.component.ts
index bca3cb8..8913052 100644
--- a/src/app/connections/callback/callback.component.ts
+++ b/src/app/connections/callback/callback.component.ts
@@ -35,14 +35,20 @@ export class ConnectionCallbackComponent implements OnInit {
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) => {
- const data = d.data;
- this.success = true;
+ console.log(params);
+ this.http.get(`${environment.API_HOST}/auth/connections?token=${params['access_token']}&state=${params['state']}&expires_in=${params['expires_in']}`).subscribe({
+ next: 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)
+ console.log('about to wait for 2 seconds')
+ await setTimeout(async () => {
+ console.log('create connection')
+ 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'])
});
;
}
diff --git a/src/app/hermes-client.service.ts b/src/app/hermes-client.service.ts
index dce1380..d32be0c 100644
--- a/src/app/hermes-client.service.ts
+++ b/src/app/hermes-client.service.ts
@@ -34,7 +34,7 @@ export class HermesClientService {
return this.listen();
}
- public disconnect() {
+ public disconnect(impersonated: boolean = false) {
if (!this.connected)
return;
@@ -43,7 +43,7 @@ export class HermesClientService {
this.session_id = undefined;
this.api_key = undefined;
this.socket.close();
- this.events.emit('tts_logoff', null);
+ this.events.emit('tts_logoff', impersonated);
}
public filter(predicate: (data: any) => boolean): Observable | undefined {
diff --git a/src/app/navigation/navigation.module.ts b/src/app/navigation/navigation.module.ts
new file mode 100644
index 0000000..b32402f
--- /dev/null
+++ b/src/app/navigation/navigation.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CommonModule
+ ]
+})
+export class NavigationModule { }
diff --git a/src/app/navigation/navigation.component.html b/src/app/navigation/sidebar/sidebar.component.html
similarity index 88%
rename from src/app/navigation/navigation.component.html
rename to src/app/navigation/sidebar/sidebar.component.html
index 2167af9..9780d86 100644
--- a/src/app/navigation/navigation.component.html
+++ b/src/app/navigation/sidebar/sidebar.component.html
@@ -1,24 +1,7 @@
\ No newline at end of file
diff --git a/src/app/navigation/navigation.component.scss b/src/app/navigation/sidebar/sidebar.component.scss
similarity index 100%
rename from src/app/navigation/navigation.component.scss
rename to src/app/navigation/sidebar/sidebar.component.scss
diff --git a/src/app/navigation/navigation.component.spec.ts b/src/app/navigation/sidebar/sidebar.component.spec.ts
similarity index 59%
rename from src/app/navigation/navigation.component.spec.ts
rename to src/app/navigation/sidebar/sidebar.component.spec.ts
index aa04048..c8e3ae8 100644
--- a/src/app/navigation/navigation.component.spec.ts
+++ b/src/app/navigation/sidebar/sidebar.component.spec.ts
@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { NavigationComponent } from './navigation.component';
+import { SidebarComponent } from './sidebar.component';
describe('NavigationComponent', () => {
- let component: NavigationComponent;
- let fixture: ComponentFixture;
+ let component: SidebarComponent;
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [NavigationComponent]
+ imports: [SidebarComponent]
})
.compileComponents();
- fixture = TestBed.createComponent(NavigationComponent);
+ fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/src/app/navigation/navigation.component.ts b/src/app/navigation/sidebar/sidebar.component.ts
similarity index 51%
rename from src/app/navigation/navigation.component.ts
rename to src/app/navigation/sidebar/sidebar.component.ts
index c8c222c..95b37dd 100644
--- a/src/app/navigation/navigation.component.ts
+++ b/src/app/navigation/sidebar/sidebar.component.ts
@@ -1,26 +1,30 @@
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
-import { HermesClientService } from '../hermes-client.service';
-import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
+import { HermesClientService } from '../../hermes-client.service';
+import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
import { MatCardModule } from '@angular/material/card';
-import { AuthModule } from '../auth/auth.module';
-import { UserCardComponent } from "../auth/user-card/user-card.component";
+import { AuthModule } from '../../auth/auth.module';
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({
- selector: 'navigation',
+ selector: 'sidebar',
standalone: true,
imports: [
AuthModule,
MatButtonModule,
MatCardModule,
+ MatIconModule,
+ MatSidenavModule,
+ MatToolbarModule,
RouterModule,
- UserCardComponent,
],
- templateUrl: './navigation.component.html',
- styleUrl: './navigation.component.scss'
+ templateUrl: './sidebar.component.html',
+ styleUrl: './sidebar.component.scss'
})
-export class NavigationComponent {
+export class SidebarComponent {
constructor(private auth: ApiAuthenticationService, private hermes: HermesClientService) { }
isLoggedIn() {
diff --git a/src/app/navigation/topbar/topbar.component.html b/src/app/navigation/topbar/topbar.component.html
new file mode 100644
index 0000000..f43c817
--- /dev/null
+++ b/src/app/navigation/topbar/topbar.component.html
@@ -0,0 +1,28 @@
+
+
+
+ Tom-to-Speech
+ @if (isTTSLoggedIn) {
+
+
+ @if (showImpersonation) {
+
+ } @else {
+
+ {{impersonatedName ?? username}}
+ @if (impersonatedId) {
+
+ Impersonating from {{username}}
+ }
+
+ }
+
+
+ }
+
\ No newline at end of file
diff --git a/src/app/navigation/topbar/topbar.component.scss b/src/app/navigation/topbar/topbar.component.scss
new file mode 100644
index 0000000..43ebfdf
--- /dev/null
+++ b/src/app/navigation/topbar/topbar.component.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/src/app/navigation/topbar/topbar.component.spec.ts b/src/app/navigation/topbar/topbar.component.spec.ts
new file mode 100644
index 0000000..079d5d9
--- /dev/null
+++ b/src/app/navigation/topbar/topbar.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TopbarComponent } from './topbar.component';
+
+describe('TopbarComponent', () => {
+ let component: TopbarComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [TopbarComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(TopbarComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/navigation/topbar/topbar.component.ts b/src/app/navigation/topbar/topbar.component.ts
new file mode 100644
index 0000000..bd8088b
--- /dev/null
+++ b/src/app/navigation/topbar/topbar.component.ts
@@ -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);
+ }
+}
diff --git a/src/app/redemptions/redemption-list/redemption-list.component.ts b/src/app/redemptions/redemption-list/redemption-list.component.ts
index 3dfcfd7..6da1052 100644
--- a/src/app/redemptions/redemption-list/redemption-list.component.ts
+++ b/src/app/redemptions/redemption-list/redemption-list.component.ts
@@ -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 Redemption from '../../shared/models/redemption';
import { MatCardModule } from '@angular/material/card';
diff --git a/src/app/shared/models/user.ts b/src/app/shared/models/user.ts
new file mode 100644
index 0000000..3a0dc84
--- /dev/null
+++ b/src/app/shared/models/user.ts
@@ -0,0 +1,4 @@
+export interface User {
+ id: string;
+ name: string;
+}
\ No newline at end of file
diff --git a/src/app/shared/services/api/api-authentication.service.ts b/src/app/shared/services/api/api-authentication.service.ts
index c3f2b68..28dc3bb 100644
--- a/src/app/shared/services/api/api-authentication.service.ts
+++ b/src/app/shared/services/api/api-authentication.service.ts
@@ -14,6 +14,8 @@ export class ApiAuthenticationService {
this.authenticated = false;
this.user = null;
this.lastCheck = new Date();
+
+ this.events.listen('impersonation', _ => this.update());
}
isAuthenticated() {
@@ -28,6 +30,10 @@ export class ApiAuthenticationService {
return this.user?.impersonation?.id;
}
+ getImpersonatedName() {
+ return this.user?.impersonation?.name;
+ }
+
getUsername() {
return this.user?.name;
}
diff --git a/src/app/shared/services/api/api-key.service.ts b/src/app/shared/services/api/api-key.service.ts
index d87a07e..25efc1f 100644
--- a/src/app/shared/services/api/api-key.service.ts
+++ b/src/app/shared/services/api/api-key.service.ts
@@ -16,7 +16,16 @@ export class ApiKeyService {
constructor() {
+ this.events.listen('tts_logoff', (impersonation) => {
+ console.log('tts_logoff triggered:', impersonation);
+ if (impersonation) {
+ this.keys = [];
+ this.loaded = false;
+ }
+ });
+
this.events.listen('logoff', () => {
+ console.log('logoff triggered');
this.keys = [];
this.loaded = false;
});
diff --git a/src/app/shared/services/tts-filter.service.ts b/src/app/shared/services/tts-filter.service.ts
index 6687b3e..9593f3a 100644
--- a/src/app/shared/services/tts-filter.service.ts
+++ b/src/app/shared/services/tts-filter.service.ts
@@ -41,7 +41,7 @@ export default class TtsFilterService {
fetch() {
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));
diff --git a/src/app/shared/services/twitch-redemption.service.ts b/src/app/shared/services/twitch-redemption.service.ts
index 7e542c7..3301e7e 100644
--- a/src/app/shared/services/twitch-redemption.service.ts
+++ b/src/app/shared/services/twitch-redemption.service.ts
@@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { environment } from '../../../environments/environment';
import TwitchRedemption from '../models/twitch-redemption';
-import { of } from 'rxjs';
+import { catchError, EMPTY, Observable, of } from 'rxjs';
import EventService from './EventService';
@Injectable({
@@ -31,10 +31,12 @@ export default class TwitchRedemptionService {
'Authorization': 'Bearer ' + localStorage.getItem('jwt'),
}
});
- $.subscribe(d => {
- this.twitchRedemptions = d;
- this.loaded = true;
- });
+ $.pipe(catchError(() => EMPTY))
+ .subscribe({
+ next: d => this.twitchRedemptions = d,
+ error: d => console.log('Twitch API redemptions:', d.error),
+ complete: () => this.loaded = true,
+ });
return $;
}
}
\ No newline at end of file
diff --git a/src/app/shared/services/user.service.spec.ts b/src/app/shared/services/user.service.spec.ts
new file mode 100644
index 0000000..e57cf0e
--- /dev/null
+++ b/src/app/shared/services/user.service.spec.ts
@@ -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();
+ });
+});
diff --git a/src/app/shared/services/user.service.ts b/src/app/shared/services/user.service.ts
new file mode 100644
index 0000000..d21dc11
--- /dev/null
+++ b/src/app/shared/services/user.service.ts
@@ -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(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 $;
+ }
+}