Added top bar on all pages. Simplified TTS login component. Fixed some issues. Removed redirects for now.

This commit is contained in:
Tom 2025-04-01 21:12:01 +00:00
parent d44ec50a6a
commit 055885837c
30 changed files with 402 additions and 186 deletions

View File

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

View File

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

View File

@ -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 {

View File

@ -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 = [
{ {

View File

@ -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,
] ]

View File

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

View File

@ -2,5 +2,4 @@ main {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-top: 1em;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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() {

View 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>

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

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

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

View File

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

View File

@ -0,0 +1,4 @@
export interface User {
id: string;
name: string;
}

View File

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

View File

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

View File

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

View File

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

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

View 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 $;
}
}