Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16,620 changes: 16,620 additions & 0 deletions src/package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';

import {StandardBannerComponent} from './standard-banner.component';
import {provideHttpClientTesting} from '@angular/common/http/testing';
import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';

describe('StandardBannerComponent', () => {
let component: StandardBannerComponent;
let fixture: ComponentFixture<StandardBannerComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [StandardBannerComponent]
imports: [StandardBannerComponent],
providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()]
})
.compileComponents();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {Component, OnInit} from '@angular/core';
import {Component, inject, OnInit} from '@angular/core';
import {LogoComponent} from '../logo/logo.component';
import {environment} from '../../../../environments/environment';
import {MatButton} from '@angular/material/button';
import {AuthService} from "../../../service/auth.service";

@Component({
selector: 'app-standard-banner',
Expand All @@ -13,17 +14,18 @@ import {MatButton} from '@angular/material/button';
styleUrl: './standard-banner.component.scss'
})
export class StandardBannerComponent implements OnInit {
private auth = inject(AuthService);
public userIsLoggedIn: boolean = false;

constructor() {
}

ngOnInit(): void {
this.userIsLoggedIn = null !== localStorage.getItem('auth-token');
this.userIsLoggedIn = null !== this.auth.getToken();
}

onLogout(): void {
localStorage.removeItem('auth-token');
this.auth.clearToken();

// Need to use window.location here instead of the router because if you're already on the home page and you
// router.navigate to it, it doesn't refresh the page and update the state.
Expand Down
6 changes: 3 additions & 3 deletions src/src/app/common/interface/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface OAuth {
bearer: string,
refresh: string,
expiresUtc: string
AccessToken: string,
RefreshToken: string,
ExpiresUtc: string
}
6 changes: 3 additions & 3 deletions src/src/app/middleware/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import {CanActivateFn} from '@angular/router';
import {environment} from "../../environments/environment";
import {inject} from "@angular/core";
import {NullinsideService} from "../service/nullinside.service";
import {of, tap} from "rxjs";
import {AuthService} from "../service/auth.service";

export const authGuard: CanActivateFn = (_, __) => {
// TODO: Hook up the "returnUrl" in the rest of the application. GitHub issue #
const token = localStorage.getItem('auth-token');
const token = inject(AuthService).getToken();
if (!token) {
window.location.href = `${environment.siteUrl}`;
return of(false);
}

return inject(NullinsideService).validateToken(token).pipe(
return inject(AuthService).validateToken(token).pipe(
tap({
next: success => {
if (!success) {
Expand Down
34 changes: 28 additions & 6 deletions src/src/app/middleware/bearer-token-interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
import {Injectable} from '@angular/core';
import {inject, Injectable} from '@angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';

import {Observable} from 'rxjs';
import {environment} from "../../environments/environment";
import {AuthService} from "../service/auth.service";

@Injectable()
export class BearerTokenInterceptor implements HttpInterceptor {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const url = req.url.toLowerCase().replace('https://www.', 'https://');
if (!url.startsWith(`${environment.apiUrl}/`) && !url.startsWith(`${environment.nullApiUrl}/`) &&
!url.startsWith(`${environment.twitchBotApiUrl}/`)) {

if (
// Prevent infinite loop
url.endsWith(`/user/token/refresh`) ||
(
// Our services, no need to send our bearer tokens to other apis
!url.startsWith(`${environment.apiUrl}/`) &&
!url.startsWith(`${environment.nullApiUrl}/`) &&
!url.startsWith(`${environment.twitchBotApiUrl}/`)
)
) {
return next.handle(req);
}

const token = localStorage.getItem('auth-token');
if (!token) {
const oAuth = inject(AuthService).getOAuth();
if (!oAuth) {
return next.handle(req);
}

if (new Date(oAuth.ExpiresUtc) < new Date()) {
inject(AuthService).clearToken();
inject(AuthService).refreshToken(oAuth.RefreshToken).subscribe({
next: oAuth => {
inject(AuthService).setToken(oAuth);
},
error: err => {
console.error(err);
}
});
}

req = req.clone({
setHeaders: {Authorization: `Bearer ${token}`}
setHeaders: {Authorization: `Bearer ${inject(AuthService).getToken()}`}
});

return next.handle(req);
Expand Down
18 changes: 18 additions & 0 deletions src/src/app/service/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {TestBed} from '@angular/core/testing';

import {AuthService} from './auth.service';
import {provideHttpClientTesting} from '@angular/common/http/testing';
import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';

describe('AuthService', () => {
let service: AuthService;

beforeEach(() => {
TestBed.configureTestingModule({providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()]});
service = TestBed.inject(AuthService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
69 changes: 69 additions & 0 deletions src/src/app/service/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {OAuth} from "../common/interface/oauth";
import {environment} from "../../environments/environment";
import {UserRolesResponse} from "../common/interface/user-roles-response";
import {FeatureToggleResponse} from "../common/interface/feature-toggle-response";

@Injectable({
providedIn: 'root'
})
export class AuthService {
private httpClient = inject(HttpClient);
private oauth: OAuth | null = null;

constructor() {
this.oauth = {
AccessToken: localStorage.getItem('auth-AccessToken') ?? '',
RefreshToken: localStorage.getItem('auth-RefreshToken') ?? '',
ExpiresUtc: localStorage.getItem('auth-ExpiresUtc') ?? ''
};

if (!this.oauth.AccessToken) {
this.oauth = null;
}
}

setToken(token: OAuth): void {
console.log('Setting token', token);
this.oauth = token;
localStorage.setItem('auth-AccessToken', token.AccessToken);
localStorage.setItem('auth-RefreshToken', token.RefreshToken);
localStorage.setItem('auth-ExpiresUtc', token.ExpiresUtc);
}

getToken(): string | null {
console.log('Getting token', this.oauth);
console.log('Getting token', this.oauth?.AccessToken);
return this.oauth?.AccessToken ?? null;
}

clearToken(): void {
console.log('Clearing token');
this.oauth = null;
localStorage.removeItem('auth-AccessToken');
localStorage.removeItem('auth-RefreshToken');
localStorage.removeItem('auth-ExpiresUtc');
}

getOAuth(): OAuth | null {
return this.oauth;
}

refreshToken(token: string): Observable<OAuth> {
return this.httpClient.post<OAuth>(`${environment.apiUrl}/user/token/refresh`, {token: token});
}

validateToken(token: string): Observable<boolean> {
return this.httpClient.post<boolean>(`${environment.apiUrl}/user/token/validate`, {token: token});
}

getUserRoles(): Observable<UserRolesResponse> {
return this.httpClient.get<UserRolesResponse>(`${environment.apiUrl}/user/roles`);
}

getFeatureToggles(): Observable<FeatureToggleResponse[]> {
return this.httpClient.get<FeatureToggleResponse[]>(`${environment.apiUrl}/featureToggle`);
}
}
15 changes: 0 additions & 15 deletions src/src/app/service/nullinside.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,19 @@ import {inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {environment} from "../../environments/environment";
import {UserRolesResponse} from "../common/interface/user-roles-response";
import {DockerResource} from '../common/interface/docker-resource';
import {FeatureToggleResponse} from "../common/interface/feature-toggle-response";

@Injectable({
providedIn: 'root'
})
export class NullinsideService {
private httpClient = inject(HttpClient);


validateToken(token: string): Observable<boolean> {
return this.httpClient.post<boolean>(`${environment.apiUrl}/user/token/validate`, {token: token});
}

getUserRoles(): Observable<UserRolesResponse> {
return this.httpClient.get<UserRolesResponse>(`${environment.apiUrl}/user/roles`);
}

getVirtualMachines(): Observable<DockerResource[]> {
return this.httpClient.get<DockerResource[]>(`${environment.apiUrl}/docker`);
}

setVirtualMachinePowerState(id: number, turnOn: boolean): Observable<boolean> {
return this.httpClient.post<boolean>(`${environment.apiUrl}/docker/${id}`, {turnOn: turnOn});
}

getFeatureToggles(): Observable<FeatureToggleResponse[]> {
return this.httpClient.get<FeatureToggleResponse[]>(`${environment.apiUrl}/featureToggle`);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';

import {BackgroundWebglExampleComponent} from './background-webgl-example.component';
import {provideHttpClientTesting} from '@angular/common/http/testing';
import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';

describe('BackgroundWebglExampleComponent', () => {
let component: BackgroundWebglExampleComponent;
let fixture: ComponentFixture<BackgroundWebglExampleComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [BackgroundWebglExampleComponent]
imports: [BackgroundWebglExampleComponent],
providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()]
})
.compileComponents();

Expand Down
10 changes: 5 additions & 5 deletions src/src/app/view/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {Component, inject, OnInit} from '@angular/core';
import {NullinsideService} from "../../service/nullinside.service";
import {VM_ADMIN} from "../../common/constants";
import {WebsiteApp} from "../../common/interface/website-app";
import {Router} from '@angular/router';
import {StandardBannerComponent} from '../../common/components/standard-banner/standard-banner.component';
import {LoadingIconComponent} from "../../common/components/loading-icon/loading-icon.component";
import {catchError, forkJoin, Observable, of} from "rxjs";
import {UserRolesResponse} from "../../common/interface/user-roles-response";
import {AuthService} from "../../service/auth.service";

@Component({
selector: 'app-home',
Expand All @@ -18,7 +18,7 @@ import {UserRolesResponse} from "../../common/interface/user-roles-response";
styleUrl: './home.component.scss'
})
export class HomeComponent implements OnInit {
private api = inject(NullinsideService);
private auth = inject(AuthService);
private router = inject(Router);

public roles: string[] | null = null;
Expand All @@ -42,12 +42,12 @@ export class HomeComponent implements OnInit {
];

ngOnInit(): void {
this.userIsLoggedIn = null !== localStorage.getItem('auth-token');
this.userIsLoggedIn = null !== this.auth.getToken();

forkJoin({
// We don't care if the roles don't exist. This is only for authed users. So catch the error if there is one.
user: this.api.getUserRoles().pipe(catchError(_ => of({roles: []}))) as Observable<UserRolesResponse>,
featureToggles: this.api.getFeatureToggles()
user: this.auth.getUserRoles().pipe(catchError(_ => of({roles: []}))) as Observable<UserRolesResponse>,
featureToggles: this.auth.getFeatureToggles()
})
.subscribe({
next: response => {
Expand Down
9 changes: 6 additions & 3 deletions src/src/app/view/login-landing/login-landing.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ActivatedRoute, ParamMap, Router} from "@angular/router";
import {environment} from "../../../environments/environment";
import {HttpErrorResponse} from "@angular/common/http";
import {Errors} from "./errors";
import {AuthService} from "../../service/auth.service";

@Component({
selector: 'app-login-landing',
Expand All @@ -17,6 +18,7 @@ import {Errors} from "./errors";
styleUrl: './login-landing.component.scss'
})
export class LoginLandingComponent implements OnInit, OnDestroy {
private auth = inject(AuthService);
private api = inject(NullinsideService);
private route = inject(ActivatedRoute);
private router = inject(Router);
Expand Down Expand Up @@ -53,9 +55,10 @@ export class LoginLandingComponent implements OnInit, OnDestroy {
return;
}

this.api.validateToken(token).subscribe({
const oauth = JSON.parse(atob(token));
this.auth.validateToken(oauth.AccessToken).subscribe({
next: _ => {
localStorage.setItem('auth-token', token);
this.auth.setToken(oauth);
this.router.navigate(['/home']);
},
error: (_: HttpErrorResponse) => {
Expand All @@ -70,7 +73,7 @@ export class LoginLandingComponent implements OnInit, OnDestroy {
}

onLoginFailed(message = ':( Failed to login, please try again', redirect = true): void {
localStorage.removeItem('auth-token');
this.auth.clearToken();
this.error = message;

if (redirect) {
Expand Down
6 changes: 4 additions & 2 deletions src/src/app/view/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ActivatedRoute, ParamMap, Router} from "@angular/router";
import {LoadingIconComponent} from "../../common/components/loading-icon/loading-icon.component";
import {HttpErrorResponse} from '@angular/common/http';
import {TwitchLoginComponent} from '../../common/components/twitch-login/twitch-login.component';
import {AuthService} from "../../service/auth.service";

@Component({
selector: 'app-login',
Expand All @@ -18,6 +19,7 @@ import {TwitchLoginComponent} from '../../common/components/twitch-login/twitch-
styleUrl: './login.component.scss'
})
export class LoginComponent implements OnInit {
private auth = inject(AuthService);
private api = inject(NullinsideService);
private router = inject(Router);
private route = inject(ActivatedRoute);
Expand All @@ -44,13 +46,13 @@ export class LoginComponent implements OnInit {
}
});

const token = localStorage.getItem('auth-token');
const token = this.auth.getToken();
if (!token) {
return;
}

this.checkingLogin = true;
this.api.validateToken(token || '')
this.auth.validateToken(token || '')
.subscribe({
next: _ => {
this.router.navigate([this.pageDestinations[0]]);
Expand Down
Loading