Skip to content

Commit 5d392a0

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 5d392a0

File tree

13 files changed

+16768
-44
lines changed

13 files changed

+16768
-44
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.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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {TestBed} from '@angular/core/testing';
2+
3+
import {AuthService} from './auth.service';
4+
5+
describe('AuthService', () => {
6+
let service: AuthService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(AuthService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});
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/home/home.component.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import {Component, inject, OnInit} from '@angular/core';
2-
import {NullinsideService} from "../../service/nullinside.service";
32
import {VM_ADMIN} from "../../common/constants";
43
import {WebsiteApp} from "../../common/interface/website-app";
54
import {Router} from '@angular/router';
65
import {StandardBannerComponent} from '../../common/components/standard-banner/standard-banner.component';
76
import {LoadingIconComponent} from "../../common/components/loading-icon/loading-icon.component";
87
import {catchError, forkJoin, Observable, of} from "rxjs";
98
import {UserRolesResponse} from "../../common/interface/user-roles-response";
9+
import {AuthService} from "../../service/auth.service";
1010

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

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

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

4747
forkJoin({
4848
// We don't care if the roles don't exist. This is only for authed users. So catch the error if there is one.
49-
user: this.api.getUserRoles().pipe(catchError(_ => of({roles: []}))) as Observable<UserRolesResponse>,
50-
featureToggles: this.api.getFeatureToggles()
49+
user: this.auth.getUserRoles().pipe(catchError(_ => of({roles: []}))) as Observable<UserRolesResponse>,
50+
featureToggles: this.auth.getFeatureToggles()
5151
})
5252
.subscribe({
5353
next: response => {

src/src/app/view/login-landing/login-landing.component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {ActivatedRoute, ParamMap, Router} from "@angular/router";
66
import {environment} from "../../../environments/environment";
77
import {HttpErrorResponse} from "@angular/common/http";
88
import {Errors} from "./errors";
9+
import {AuthService} from "../../service/auth.service";
910

1011
@Component({
1112
selector: 'app-login-landing',
@@ -17,6 +18,7 @@ import {Errors} from "./errors";
1718
styleUrl: './login-landing.component.scss'
1819
})
1920
export class LoginLandingComponent implements OnInit, OnDestroy {
21+
private auth = inject(AuthService);
2022
private api = inject(NullinsideService);
2123
private route = inject(ActivatedRoute);
2224
private router = inject(Router);
@@ -53,9 +55,10 @@ export class LoginLandingComponent implements OnInit, OnDestroy {
5355
return;
5456
}
5557

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

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

7679
if (redirect) {

0 commit comments

Comments
 (0)