Fixed issues with impersonation. Show warning or error depending on connection's remaining time before expiry. Replaced use of /api/... in urls.

This commit is contained in:
Tom 2025-04-05 02:06:31 +00:00
parent 70e0e9bf71
commit 3e9a9f9dc5
18 changed files with 129 additions and 71 deletions

View File

@ -47,8 +47,8 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "1024kB", "maximumWarning": "3MB",
"maximumError": "1MB" "maximumError": "5MB"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
@ -93,7 +93,7 @@
}, },
"defaultConfiguration": "development", "defaultConfiguration": "development",
"options": { "options": {
"allowedHosts": ["*"] "allowedHosts": ["beta.tomtospeech.com"]
} }
}, },
"extract-i18n": { "extract-i18n": {

10
package-lock.json generated
View File

@ -22,6 +22,7 @@
"@angular/ssr": "^19.2.5", "@angular/ssr": "^19.2.5",
"angular-oauth2-oidc": "^17.0.2", "angular-oauth2-oidc": "^17.0.2",
"express": "^4.18.2", "express": "^4.18.2",
"moment": "^2.30.1",
"ngx-socket-io": "^4.7.0", "ngx-socket-io": "^4.7.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"rxjs-websockets": "^9.0.0", "rxjs-websockets": "^9.0.0",
@ -10344,6 +10345,15 @@
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
} }
}, },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",

View File

@ -2,8 +2,7 @@
"name": "hermes-web-angular", "name": "hermes-web-angular",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "start": "ng serve -c development --host 0.0.0.0 --watch false",
"start": "ng serve -c production --host 0.0.0.0 --watch false",
"build": "ng build", "build": "ng build",
"watch": "ng serve -c development --host 0.0.0.0 --disable-host-check", "watch": "ng serve -c development --host 0.0.0.0 --disable-host-check",
"test": "ng test", "test": "ng test",
@ -25,6 +24,7 @@
"@angular/ssr": "^19.2.5", "@angular/ssr": "^19.2.5",
"angular-oauth2-oidc": "^17.0.2", "angular-oauth2-oidc": "^17.0.2",
"express": "^4.18.2", "express": "^4.18.2",
"moment": "^2.30.1",
"ngx-socket-io": "^4.7.0", "ngx-socket-io": "^4.7.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"rxjs-websockets": "^9.0.0", "rxjs-websockets": "^9.0.0",

View File

@ -8,7 +8,6 @@ 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';
import { ApiKeyService } from './shared/services/api/api-key.service'; import { ApiKeyService } from './shared/services/api/api-key.service';
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 { MatIconModule } from '@angular/material/icon';
@ -16,6 +15,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { SidebarComponent } from "./navigation/sidebar/sidebar.component"; import { SidebarComponent } from "./navigation/sidebar/sidebar.component";
import { Topbar as TopbarComponent } from "./navigation/topbar/topbar.component"; import { Topbar as TopbarComponent } from "./navigation/topbar/topbar.component";
import ApiKey from './shared/models/api-key';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -38,7 +38,6 @@ export class AppComponent implements OnInit, OnDestroy {
private readonly overlayContainer = inject(OverlayContainer); private readonly overlayContainer = inject(OverlayContainer);
private readonly themeService = inject(ThemeService); private readonly themeService = inject(ThemeService);
private isBrowser: boolean;
private ngZone: NgZone; private ngZone: NgZone;
private subscriptions: Subscription[]; private subscriptions: Subscription[];
@ -57,7 +56,6 @@ export class AppComponent implements OnInit, OnDestroy {
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.subscriptions = []; this.subscriptions = [];
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => { this.subscriptions.push(this.events.listen('tts_login_ack', async _ => {
@ -72,16 +70,27 @@ export class AppComponent implements OnInit, OnDestroy {
} }
})); }));
this.addSubscription(this.events.listen('login', () => {
this.keyService.fetch()
.pipe(timeout(3000), first())
.subscribe(async (d: ApiKey[]) => {
if (d.length > 0)
this.client.login(d[0].id);
});
}));
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'])));
this.subscriptions.push(this.events.listen('toggle_sidebar', () => this.isSidebarOpen = !this.isSidebarOpen)) this.subscriptions.push(this.events.listen('toggle_sidebar', () => this.isSidebarOpen = !this.isSidebarOpen))
} }
ngOnInit(): void { ngOnInit(): void {
if (!this.isBrowser) if (!isPlatformBrowser(this.platformId))
return; return;
this.auth.update(); this.auth.update();
this.subscriptions.push(this.events.listen('login', async () => await this.router.navigate(['tts-login'])));
this.addSubscription(this.events.listen('logoff', async (message) => { this.addSubscription(this.events.listen('logoff', async (message) => {
localStorage.removeItem('jwt'); localStorage.removeItem('jwt');
if (!document.location.href.includes('/login')) { if (!document.location.href.includes('/login')) {
@ -94,17 +103,6 @@ export class AppComponent implements OnInit, OnDestroy {
} }
})); }));
this.addSubscription(this.events.listen('login', () => {
this.keyService.fetch()
.pipe(timeout(3000), first())
.subscribe(async (d: ApiKey[]) => {
if (d.length > 0)
this.client.login(d[0].id);
else if (['/login', '/auth'].some(partial => document.location.href.includes(partial)))
await this.router.navigate(['tts-login']);
});
}));
let currentTheme = localStorage.getItem('ui-theme') ?? this.themeService.theme; let currentTheme = localStorage.getItem('ui-theme') ?? this.themeService.theme;
if (currentTheme == 'light' || currentTheme == 'dark') { if (currentTheme == 'light' || currentTheme == 'dark') {
this.themeService.theme = currentTheme; this.themeService.theme = currentTheme;

View File

@ -60,9 +60,8 @@ export class ImpersonationComponent implements OnInit {
impersonation: impersonationId impersonation: impersonationId
} }
}).subscribe(async (data: any) => { }).subscribe(async (data: any) => {
this.impersonationControl.setValue(undefined);
this.client.disconnect(true); this.client.disconnect(true);
this.events.emit('impersonation', undefined); this.events.emit('impersonation', impersonationId);
}); });
} else { } else {
this.http.put(environment.API_HOST + '/admin/impersonate', { this.http.put(environment.API_HOST + '/admin/impersonate', {
@ -72,7 +71,6 @@ export class ImpersonationComponent implements OnInit {
'Authorization': 'Bearer ' + localStorage.getItem('jwt') 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
} }
}).subscribe(async (data: any) => { }).subscribe(async (data: any) => {
this.impersonationControl.setValue(impersonationId);
this.client.disconnect(true); this.client.disconnect(true);
this.events.emit('impersonation', impersonationId); this.events.emit('impersonation', impersonationId);
await this.router.navigate(['tts-login']); await this.router.navigate(['tts-login']);

View File

@ -18,6 +18,7 @@
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button mat-raised-button <button mat-raised-button
[disabled]="disabled"
(click)="login()">Log In</button> (click)="login()">Log In</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>

View File

@ -32,13 +32,17 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
keyControl = new FormControl<string | null>(''); keyControl = new FormControl<string | null>('');
api_keys: { id: string, label: string }[] = []; api_keys: { id: string, label: string }[] = [];
subscriptions: (Subscription | null)[] = []; subscriptions: (Subscription | null)[] = [];
disabled: boolean = false;
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.eventService.listen('impersonation', _ => this.reset())); this.subscriptions.push(this.eventService.listen('impersonation', _ => this.reset()));
this.subscriptions.push(this.eventService.listen('logoff', _ => this.reset())); this.subscriptions.push(this.eventService.listen('logoff', impersonation => {
if (!impersonation)
this.reset();
}));
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -57,7 +61,11 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
} }
private reset() { private reset() {
this.disabled = true;
this.api_keys = []; this.api_keys = [];
this.keyService.fetch().subscribe(keys => this.api_keys = keys); this.keyService.fetch().subscribe(keys => {
this.api_keys = keys;
this.disabled = false;
});
} }
} }

View File

@ -9,6 +9,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'connection-item-edit', selector: 'connection-item-edit',
@ -54,7 +55,7 @@ export class ConnectionItemEditComponent {
return; return;
} }
this.http.post('/api/auth/connections', { this.http.post(environment.API_HOST + '/auth/connections', {
name: this.nameControl.value, name: this.nameControl.value,
type: this.typeControl.value, type: this.typeControl.value,
client_id: this.clientIdControl.value, client_id: this.clientIdControl.value,

View File

@ -2,6 +2,14 @@
[class.nightbot]="connection().type == 'nightbot'"> [class.nightbot]="connection().type == 'nightbot'">
{{connection().name}} {{connection().name}}
@if (isExpired) {
<mat-icon matTooltip="Connection has expired."
class="danger">error</mat-icon>
} @else if (isExpiringSoon) {
<mat-icon matTooltip="Connection is soon going to expire."
class="warning">warning</mat-icon>
}
<article class="right"> <article class="right">
<button mat-button <button mat-button
class="neutral" class="neutral"

View File

@ -3,10 +3,13 @@ import { Connection } from '../../shared/models/connection';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import {MatTooltipModule} from '@angular/material/tooltip';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { HermesClientService } from '../../hermes-client.service'; import { HermesClientService } from '../../hermes-client.service';
import { environment } from '../../../environments/environment';
import moment from 'moment';
@Component({ @Component({
selector: 'connection-item', selector: 'connection-item',
@ -14,6 +17,7 @@ import { HermesClientService } from '../../hermes-client.service';
MatButtonModule, MatButtonModule,
MatIconModule, MatIconModule,
MatFormFieldModule, MatFormFieldModule,
MatTooltipModule,
ReactiveFormsModule, ReactiveFormsModule,
], ],
templateUrl: './connection-item.component.html', templateUrl: './connection-item.component.html',
@ -27,13 +31,25 @@ export class ConnectionItemComponent {
constructor(@Inject(DOCUMENT) private document: Document) { } constructor(@Inject(DOCUMENT) private document: Document) { }
ngOnInit() {
console.log('coonnn', this.connection())
}
delete() { delete() {
this.client.deleteConnection(this.connection().name); this.client.deleteConnection(this.connection().name);
} }
get isExpired() {
return moment(this.connection().expires_at).toDate().getTime() < new Date().getTime();
}
get isExpiringSoon() {
return moment(this.connection().expires_at).toDate().getTime() < moment.now() + moment.duration(7, 'd').asMilliseconds();
}
renew() { renew() {
const conn = this.connection(); const conn = this.connection();
this.http.post('/api/auth/connections', { this.http.post(environment.API_HOST + '/auth/connections', {
name: conn.name, name: conn.name,
type: conn.type, type: conn.type,
client_id: conn.client_id, client_id: conn.client_id,

View File

@ -1,4 +1,4 @@
import { OnInit, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { catchError, first, timeout } from 'rxjs/operators'; import { catchError, first, timeout } from 'rxjs/operators';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
@ -8,13 +8,9 @@ import { EMPTY, Observable, Observer, throwError } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class HermesSocketService implements OnInit { export class HermesSocketService {
private socket: WebSocketSubject<any> | undefined = undefined private socket: WebSocketSubject<any> | undefined = undefined
constructor() { }
ngOnInit(): void {
}
public connect(): void { public connect(): void {
if (!this.socket || this.socket.closed) { if (!this.socket || this.socket.closed) {
@ -22,9 +18,10 @@ export class HermesSocketService implements OnInit {
} }
} }
public first(predicate: (data: any) => boolean): Observable<any> { public first<T>(predicate: (data: T) => boolean): Observable<T> {
if (!this.socket || this.socket.closed) if (!this.socket || this.socket.closed) {
return new Observable().pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.'))); throw new Error('Socket is ' + (this.socket ? 'closed' : 'null') + '.');
}
return this.socket.pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')), first(predicate)); return this.socket.pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')), first(predicate));
} }
@ -43,7 +40,11 @@ export class HermesSocketService implements OnInit {
} }
public get$(): Observable<any> | undefined { public get$(): Observable<any> | undefined {
return this.socket?.asObservable().pipe(catchError(_ => EMPTY)); if (!this.socket || this.socket.closed) {
throw new Error('Socket is ' + (this.socket ? 'closed' : 'null') + '.');
}
return this.socket.asObservable().pipe(catchError(_ => EMPTY));
} }
public subscribe(subscriptions: Partial<Observer<any>> | ((value: any) => void)) { public subscribe(subscriptions: Partial<Observer<any>> | ((value: any) => void)) {

View File

@ -9,6 +9,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import EventService from '../../shared/services/EventService'; import EventService from '../../shared/services/EventService';
import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'key-item-edit', selector: 'key-item-edit',
@ -50,7 +51,7 @@ export class KeyItemEditComponent {
} }
const label = this.labelControl.value; const label = this.labelControl.value;
this.http.post('/api/keys', { label }, this.http.post(environment.API_HOST + '/keys', { label },
{ {
headers: { headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt') 'Authorization': 'Bearer ' + localStorage.getItem('jwt')

View File

@ -6,6 +6,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import ApiKey from '../../shared/models/api-key'; import ApiKey from '../../shared/models/api-key';
import EventService from '../../shared/services/EventService'; import EventService from '../../shared/services/EventService';
import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'key-item', selector: 'key-item',
@ -32,7 +33,7 @@ export class KeyItemComponent {
} }
const key_id = this.key().id; const key_id = this.key().id;
this.http.delete('/api/keys', this.http.delete(environment.API_HOST + '/keys',
{ {
body: { body: {
key: key_id, key: key_id,

View File

@ -5,8 +5,8 @@
</button> </button>
<span>Tom-to-Speech</span> <span>Tom-to-Speech</span>
@if (isLoggedIn) {
<span class="spacer"></span> <span class="spacer"></span>
@if (isLoggedIn) {
<div class="links"> <div class="links">
@if (showImpersonation) { @if (showImpersonation) {
<impersonation /> <impersonation />

View File

@ -51,7 +51,7 @@ export class Topbar implements OnDestroy {
} }
get isAdminLoggedIn() { get isAdminLoggedIn() {
return this.auth.isAuthenticated() && this.auth.isAdmin(); return this.auth.isAdmin();
} }
get username() { get username() {

View File

@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import EventService from '../EventService'; import EventService from '../EventService';
import { environment } from '../../../../environments/environment';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -50,26 +51,28 @@ export class ApiAuthenticationService {
return; return;
} }
// /api/auth/validate this.http.get(environment.API_HOST + '/auth/validate', {
this.http.get('/api/auth/validate', {
headers: { headers: {
'Authorization': 'Bearer ' + jwt 'Authorization': 'Bearer ' + jwt
} },
}).subscribe((data: any) => { withCredentials: true
this.updateAuthenticated(data?.authenticated, data?.user); }).subscribe({
next: (data: any) => this.updateAuthenticated(data?.authenticated, data?.user),
error: () => this.updateAuthenticated(false, null)
}); });
} }
private updateAuthenticated(authenticated: boolean, user: any) { private updateAuthenticated(authenticated: boolean, user: any) {
const previous = this.authenticated; const previous = this.authenticated;
this.authenticated = authenticated;
this.user = user; this.user = user;
this.authenticated = authenticated;
this.lastCheck = new Date(); this.lastCheck = new Date();
if (previous != authenticated) { if (previous != authenticated) {
if (authenticated) { if (authenticated) {
this.events.emit('login', null); this.events.emit('login', null);
} else { } else {
localStorage.removeItem('jwt');
this.events.emit('logoff', null); this.events.emit('logoff', null);
} }
} }

View File

@ -1,9 +1,10 @@
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core'; import { Component, inject, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service'; import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-twitch-auth-callback', selector: 'app-twitch-auth-callback',
@ -12,20 +13,26 @@ import { environment } from '../../environments/environment';
templateUrl: './twitch-auth-callback.component.html', templateUrl: './twitch-auth-callback.component.html',
styleUrl: './twitch-auth-callback.component.scss' styleUrl: './twitch-auth-callback.component.scss'
}) })
export class TwitchAuthCallbackComponent implements OnInit { export class TwitchAuthCallbackComponent implements OnInit, OnDestroy {
private isBrowser: boolean; private readonly auth = inject(ApiAuthenticationService);
private readonly http = inject(HttpClient);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly subscriptions: (Subscription | null)[] = [];
constructor(private http: HttpClient, private auth: ApiAuthenticationService, private route: ActivatedRoute, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) { constructor(@Inject(PLATFORM_ID) private platformId: Object) { }
this.isBrowser = isPlatformBrowser(this.platformId)
}
async ngOnInit(): Promise<any> { async ngOnInit(): Promise<any> {
if (!this.isBrowser) { if (!isPlatformBrowser(this.platformId)) {
return; return;
} }
if (this.auth.isAuthenticated()) { if (!this.auth.isAuthenticated() && localStorage.getItem('jwt')) {
await this.router.navigate(['tts-login']); localStorage.removeItem('jwt');
}
if (this.auth.isAuthenticated() || localStorage.getItem('jwt')) {
await this.router.navigate(['policies']);
return; return;
} }
@ -43,20 +50,24 @@ export class TwitchAuthCallbackComponent implements OnInit {
} }
this.http.post(environment.API_HOST + '/auth/twitch/callback', { code, scope, state }) this.http.post(environment.API_HOST + '/auth/twitch/callback', { code, scope, state })
.subscribe(async (response: any) => { .subscribe({
if (!response?.authenticated) { next: async (response: any) => {
await this.router.navigate(['login'], { console.log('twitch api callback response:', response);
queryParams: {
error: 'callback_issue'
}
});
return;
}
localStorage.setItem('jwt', response.token); localStorage.setItem('jwt', response.token);
this.auth.update(); this.auth.update();
},
await this.router.navigate(['tts-login']); error: async () => await this.router.navigate(['login'], {
queryParams: {
error: 'callback_issue_twitch'
}
}),
}); });
} }
ngOnDestroy(): void {
for (let subscription of this.subscriptions) {
if (subscription)
subscription.unsubscribe();
}
}
} }

View File

@ -10,6 +10,7 @@ import { Group } from '../../shared/models/group';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'app-twitch-user-item-add', selector: 'app-twitch-user-item-add',
@ -49,7 +50,7 @@ export class TwitchUserItemAddComponent implements OnInit {
this.responseError = undefined; this.responseError = undefined;
const username = this.usernameControl.value!.toLowerCase(); const username = this.usernameControl.value!.toLowerCase();
this.http.get('/api/auth/twitch/users?login=' + username, { this.http.get(environment.API_HOST + '/auth/twitch/users?login=' + username, {
headers: { headers: {
'x-api-key': this.client.api_key, 'x-api-key': this.client.api_key,
} }