Skip to content

Commit c841a08

Browse files
feat: oauth
Adding oauth. This is barely working. The refresh token code in particular doesn't work correctly as it allows the current request to fail while it sends a request for a new token. But it's a start.
1 parent 7e2e050 commit c841a08

File tree

16 files changed

+16782
-47
lines changed

16 files changed

+16782
-47
lines changed

src/package-lock.json

Lines changed: 16620 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/src/app/common/components/standard-banner/standard-banner.component.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import {ComponentFixture, TestBed} from '@angular/core/testing';
22

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

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

911
beforeEach(async () => {
1012
await TestBed.configureTestingModule({
11-
imports: [StandardBannerComponent]
13+
imports: [StandardBannerComponent],
14+
providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()]
1215
})
1316
.compileComponents();
1417

src/src/app/common/components/standard-banner/standard-banner.component.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {Component, OnInit} from '@angular/core';
1+
import {Component, inject, OnInit} from '@angular/core';
22
import {LogoComponent} from '../logo/logo.component';
33
import {environment} from '../../../../environments/environment';
44
import {MatButton} from '@angular/material/button';
5+
import {AuthService} from "../../../service/auth.service";
56

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

1820
constructor() {
1921
}
2022

2123
ngOnInit(): void {
22-
this.userIsLoggedIn = null !== localStorage.getItem('auth-token');
24+
this.userIsLoggedIn = null !== this.auth.getToken();
2325
}
2426

2527
onLogout(): void {
26-
localStorage.removeItem('auth-token');
28+
this.auth.clearToken();
2729

2830
// Need to use window.location here instead of the router because if you're already on the home page and you
2931
// router.navigate to it, it doesn't refresh the page and update the state.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface OAuth {
2-
bearer: string,
3-
refresh: string,
4-
expiresUtc: string
2+
AccessToken: string,
3+
RefreshToken: string,
4+
ExpiresUtc: string
55
}

src/src/app/middleware/auth.guard.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import {CanActivateFn} from '@angular/router';
22
import {environment} from "../../environments/environment";
33
import {inject} from "@angular/core";
4-
import {NullinsideService} from "../service/nullinside.service";
54
import {of, tap} from "rxjs";
5+
import {AuthService} from "../service/auth.service";
66

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

15-
return inject(NullinsideService).validateToken(token).pipe(
15+
return inject(AuthService).validateToken(token).pipe(
1616
tap({
1717
next: success => {
1818
if (!success) {

src/src/app/middleware/bearer-token-interceptor.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,48 @@
1-
import {Injectable} from '@angular/core';
1+
import {inject, Injectable} from '@angular/core';
22
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
33

44
import {Observable} from 'rxjs';
55
import {environment} from "../../environments/environment";
6+
import {AuthService} from "../service/auth.service";
67

78
@Injectable()
89
export class BearerTokenInterceptor implements HttpInterceptor {
910
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1011
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
1112
const url = req.url.toLowerCase().replace('https://www.', 'https://');
12-
if (!url.startsWith(`${environment.apiUrl}/`) && !url.startsWith(`${environment.nullApiUrl}/`) &&
13-
!url.startsWith(`${environment.twitchBotApiUrl}/`)) {
13+
14+
if (
15+
// Prevent infinite loop
16+
url.endsWith(`/user/token/refresh`) ||
17+
(
18+
// Our services, no need to send our bearer tokens to other apis
19+
!url.startsWith(`${environment.apiUrl}/`) &&
20+
!url.startsWith(`${environment.nullApiUrl}/`) &&
21+
!url.startsWith(`${environment.twitchBotApiUrl}/`)
22+
)
23+
) {
1424
return next.handle(req);
1525
}
1626

17-
const token = localStorage.getItem('auth-token');
18-
if (!token) {
27+
const oAuth = inject(AuthService).getOAuth();
28+
if (!oAuth) {
1929
return next.handle(req);
2030
}
2131

32+
if (new Date(oAuth.ExpiresUtc) < new Date()) {
33+
inject(AuthService).clearToken();
34+
inject(AuthService).refreshToken(oAuth.RefreshToken).subscribe({
35+
next: oAuth => {
36+
inject(AuthService).setToken(oAuth);
37+
},
38+
error: err => {
39+
console.error(err);
40+
}
41+
});
42+
}
43+
2244
req = req.clone({
23-
setHeaders: {Authorization: `Bearer ${token}`}
45+
setHeaders: {Authorization: `Bearer ${inject(AuthService).getToken()}`}
2446
});
2547

2648
return next.handle(req);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {TestBed} from '@angular/core/testing';
2+
3+
import {AuthService} from './auth.service';
4+
import {provideHttpClientTesting} from '@angular/common/http/testing';
5+
import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
6+
7+
describe('AuthService', () => {
8+
let service: AuthService;
9+
10+
beforeEach(() => {
11+
TestBed.configureTestingModule({providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()]});
12+
service = TestBed.inject(AuthService);
13+
});
14+
15+
it('should be created', () => {
16+
expect(service).toBeTruthy();
17+
});
18+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {inject, Injectable} from '@angular/core';
2+
import {HttpClient} from "@angular/common/http";
3+
import {Observable} from "rxjs";
4+
import {OAuth} from "../common/interface/oauth";
5+
import {environment} from "../../environments/environment";
6+
import {UserRolesResponse} from "../common/interface/user-roles-response";
7+
import {FeatureToggleResponse} from "../common/interface/feature-toggle-response";
8+
9+
@Injectable({
10+
providedIn: 'root'
11+
})
12+
export class AuthService {
13+
private httpClient = inject(HttpClient);
14+
private oauth: OAuth | null = null;
15+
16+
constructor() {
17+
this.oauth = {
18+
AccessToken: localStorage.getItem('auth-AccessToken') ?? '',
19+
RefreshToken: localStorage.getItem('auth-RefreshToken') ?? '',
20+
ExpiresUtc: localStorage.getItem('auth-ExpiresUtc') ?? ''
21+
};
22+
23+
if (!this.oauth.AccessToken) {
24+
this.oauth = null;
25+
}
26+
}
27+
28+
setToken(token: OAuth): void {
29+
console.log('Setting token', token);
30+
this.oauth = token;
31+
localStorage.setItem('auth-AccessToken', token.AccessToken);
32+
localStorage.setItem('auth-RefreshToken', token.RefreshToken);
33+
localStorage.setItem('auth-ExpiresUtc', token.ExpiresUtc);
34+
}
35+
36+
getToken(): string | null {
37+
console.log('Getting token', this.oauth);
38+
console.log('Getting token', this.oauth?.AccessToken);
39+
return this.oauth?.AccessToken ?? null;
40+
}
41+
42+
clearToken(): void {
43+
console.log('Clearing token');
44+
this.oauth = null;
45+
localStorage.removeItem('auth-AccessToken');
46+
localStorage.removeItem('auth-RefreshToken');
47+
localStorage.removeItem('auth-ExpiresUtc');
48+
}
49+
50+
getOAuth(): OAuth | null {
51+
return this.oauth;
52+
}
53+
54+
refreshToken(token: string): Observable<OAuth> {
55+
return this.httpClient.post<OAuth>(`${environment.apiUrl}/user/token/refresh`, {token: token});
56+
}
57+
58+
validateToken(token: string): Observable<boolean> {
59+
return this.httpClient.post<boolean>(`${environment.apiUrl}/user/token/validate`, {token: token});
60+
}
61+
62+
getUserRoles(): Observable<UserRolesResponse> {
63+
return this.httpClient.get<UserRolesResponse>(`${environment.apiUrl}/user/roles`);
64+
}
65+
66+
getFeatureToggles(): Observable<FeatureToggleResponse[]> {
67+
return this.httpClient.get<FeatureToggleResponse[]>(`${environment.apiUrl}/featureToggle`);
68+
}
69+
}

src/src/app/service/nullinside.service.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,19 @@ import {inject, Injectable} from '@angular/core';
22
import {HttpClient} from "@angular/common/http";
33
import {Observable} from "rxjs";
44
import {environment} from "../../environments/environment";
5-
import {UserRolesResponse} from "../common/interface/user-roles-response";
65
import {DockerResource} from '../common/interface/docker-resource';
7-
import {FeatureToggleResponse} from "../common/interface/feature-toggle-response";
86

97
@Injectable({
108
providedIn: 'root'
119
})
1210
export class NullinsideService {
1311
private httpClient = inject(HttpClient);
1412

15-
16-
validateToken(token: string): Observable<boolean> {
17-
return this.httpClient.post<boolean>(`${environment.apiUrl}/user/token/validate`, {token: token});
18-
}
19-
20-
getUserRoles(): Observable<UserRolesResponse> {
21-
return this.httpClient.get<UserRolesResponse>(`${environment.apiUrl}/user/roles`);
22-
}
23-
2413
getVirtualMachines(): Observable<DockerResource[]> {
2514
return this.httpClient.get<DockerResource[]>(`${environment.apiUrl}/docker`);
2615
}
2716

2817
setVirtualMachinePowerState(id: number, turnOn: boolean): Observable<boolean> {
2918
return this.httpClient.post<boolean>(`${environment.apiUrl}/docker/${id}`, {turnOn: turnOn});
3019
}
31-
32-
getFeatureToggles(): Observable<FeatureToggleResponse[]> {
33-
return this.httpClient.get<FeatureToggleResponse[]>(`${environment.apiUrl}/featureToggle`);
34-
}
3520
}

src/src/app/view/background-webgl-example/background-webgl-example.component.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import {ComponentFixture, TestBed} from '@angular/core/testing';
22

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

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

911
beforeEach(async () => {
1012
await TestBed.configureTestingModule({
11-
imports: [BackgroundWebglExampleComponent]
13+
imports: [BackgroundWebglExampleComponent],
14+
providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()]
1215
})
1316
.compileComponents();
1417

0 commit comments

Comments
 (0)