diff --git a/src/app/app.component.html b/src/app/app.component.html index be8999a..f50582a 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,10 @@ -
- - +
+ +
+ @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) { + + + } + \ 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 $; + } +}