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">
<navigation class="navigation" />
<main>
<topbar class="top" />
<div [class.container]="isSidebarOpen"
[class.full]="!isSidebarOpen">
@if (isSidebarOpen) {
<sidebar class="navigation" />
}
<router-outlet class="content" />
</div>
</main>

View File

@ -1,4 +1,9 @@
.main {
.container {
display: grid;
grid-template-columns: 20em 0px 1fr;
}
.full {
width: 80%;
margin: 0 auto;
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
@if (isAdmin()) {
<main>
<mat-form-field>
<mat-form-field class="mat-small"
subscriptSizing="dynamic">
<mat-label>User to impersonate</mat-label>
<mat-select (selectionChange)="onChange($event)"
[(value)]="impersonated">
<mat-select [formControl]="impersonationControl">
<mat-option>{{getUsername()}}</mat-option>
@for (user of users; track user.id) {
<mat-option [value]="user.id">{{ user.name }}</mat-option>

View File

@ -2,5 +2,4 @@ main {
display: flex;
justify-content: 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 { 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<string | undefined>(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']);
});
}
}
}

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

View File

@ -9,7 +9,7 @@
<mat-card-content class="content">
<mat-form-field>
<mat-label>API Key</mat-label>
<mat-select [(value)]="selected_api_key">
<mat-select [formControl]="keyControl">
@for (key of api_keys; track key.id) {
<mat-option [value]="key.id">{{key.label}}</mat-option>
}

View File

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

View File

@ -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) => {
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;
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']);
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();
}
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<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>
<user-card class="card" />
<ul class="">
@if (!isLoggedIn()) {
<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()) {
<ul>
@if (isLoggedIn()) {
@if (isTTSLoggedIn()) {
<li>
<a mat-raised-button
routerLink="/policies"
@ -61,8 +44,15 @@
Connections
</a>
</li>
} @else {
<li>
<a mat-raised-button
routerLink="/tts-login"
routerLinkActive="active">
TTS Login
</a>
</li>
}
@if (isLoggedIn()) {
<li>
<a mat-raised-button
routerLink="/keys"
@ -70,6 +60,13 @@
API Keys
</a>
</li>
} @else {
<li>
<a routerLink="/login"
routerLinkActive="active">
Login
</a>
</li>
}
</ul>
</nav>

View File

@ -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<NavigationComponent>;
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NavigationComponent]
imports: [SidebarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NavigationComponent);
fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

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

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 Redemption from '../../shared/models/redemption';
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.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;
}

View File

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

View File

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

View File

@ -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,9 +31,11 @@ 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 $;
}

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