generated from nhcarrigan/template
feat: another security sweep
This commit is contained in:
@@ -4,36 +4,78 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpHandler,
|
||||
HttpEvent,
|
||||
HttpInterceptor,
|
||||
HttpErrorResponse
|
||||
HttpErrorResponse,
|
||||
HttpClient
|
||||
} from '@angular/common/http';
|
||||
import { Observable, catchError, throwError } from 'rxjs';
|
||||
import { Observable, catchError, throwError, switchMap, BehaviorSubject, filter, take } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
constructor(private router: Router) {}
|
||||
private isRefreshing = false;
|
||||
private refreshTokenSubject = new BehaviorSubject<boolean | null>(null);
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
// Clone the request to add withCredentials
|
||||
// This ensures cookies are sent with every request
|
||||
const authReq = request.clone({
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
return next.handle(authReq).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (error.status === 401) {
|
||||
// Unauthorized - redirect to home
|
||||
this.router.navigate(['/']);
|
||||
if (error.status === 401 && !request.url.includes('/auth/refresh') && !request.url.includes('/auth/logout')) {
|
||||
return this.handle401Error(authReq, next);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handle401Error(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
if (!this.isRefreshing) {
|
||||
this.isRefreshing = true;
|
||||
this.refreshTokenSubject.next(null);
|
||||
|
||||
return this.http.post(
|
||||
`${environment.apiUrl}/auth/refresh`,
|
||||
{},
|
||||
{ withCredentials: true }
|
||||
).pipe(
|
||||
switchMap(() => {
|
||||
this.isRefreshing = false;
|
||||
this.refreshTokenSubject.next(true);
|
||||
return next.handle(request);
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.isRefreshing = false;
|
||||
this.refreshTokenSubject.next(false);
|
||||
this.router.navigate(['/']);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return this.refreshTokenSubject.pipe(
|
||||
filter(result => result !== null),
|
||||
take(1),
|
||||
switchMap(success => {
|
||||
if (success) {
|
||||
return next.handle(request);
|
||||
}
|
||||
return throwError(() => new Error('Token refresh failed'));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { Observable, tap, catchError, switchMap, throwError, of } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { AuthResponse, User } from '@library/shared-types';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -17,10 +18,12 @@ import { environment } from '../../environments/environment';
|
||||
export class AuthService {
|
||||
private currentUser = signal<User | null>(null);
|
||||
public readonly user = this.currentUser.asReadonly();
|
||||
private refreshing = false;
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
login(): void {
|
||||
@@ -32,6 +35,44 @@ export class AuthService {
|
||||
return this.api.get<AuthResponse>('/auth/me').pipe(
|
||||
tap(response => {
|
||||
this.currentUser.set(response.user);
|
||||
}),
|
||||
catchError(error => {
|
||||
if (error.status === 401) {
|
||||
return this.refreshToken().pipe(
|
||||
switchMap(() => this.api.get<AuthResponse>('/auth/me')),
|
||||
tap(response => {
|
||||
this.currentUser.set(response.user);
|
||||
}),
|
||||
catchError(() => {
|
||||
this.currentUser.set(null);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
refreshToken(): Observable<AuthResponse> {
|
||||
if (this.refreshing) {
|
||||
return of({ user: this.currentUser()!, accessToken: '' });
|
||||
}
|
||||
|
||||
this.refreshing = true;
|
||||
return this.http.post<AuthResponse>(
|
||||
`${environment.apiUrl}/auth/refresh`,
|
||||
{},
|
||||
{ withCredentials: true }
|
||||
).pipe(
|
||||
tap(response => {
|
||||
this.currentUser.set(response.user);
|
||||
this.refreshing = false;
|
||||
}),
|
||||
catchError(error => {
|
||||
this.refreshing = false;
|
||||
this.currentUser.set(null);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -46,6 +87,10 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
clearUser(): void {
|
||||
this.currentUser.set(null);
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.user() !== null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user