Add refresh logic to auth interceptor and service. Misc. prettier.

This commit is contained in:
Jesse Brault 2025-12-15 21:25:28 -06:00
parent 28222e0655
commit 80752f7513
5 changed files with 65 additions and 16 deletions

View File

@ -1,15 +1,48 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../service/auth.service';
import { catchError, from, switchMap, throwError } from 'rxjs';
import { Router } from '@angular/router';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const router = inject(Router);
const token = authService.accessToken();
if (token) {
// first we try with the current token
return next(
req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`),
}),
).pipe(
catchError((error: HttpErrorResponse) => {
// if the request with the current token returned 401,
// try refreshing, then retry the request with the new token
// for this first scenario, do not retry if we are getting 401 from login or refreshing
if (
error.status === 401 &&
!(error.url?.endsWith('auth/login') || error.url?.endsWith('auth/refresh'))
) {
return authService.refresh().pipe(
switchMap((loginView) => {
const newToken = loginView.accessToken;
return next(
req.clone({
headers: req.headers.set('Authorization', `Bearer ${newToken}`),
}),
);
}),
);
} else if (error.status === 401 && error.url?.endsWith('auth/refresh')) {
// our refresh token is expired
// redirect to login page
return from(router.navigate(['/'])).pipe(
switchMap(() => throwError(() => error)),
);
} else {
return throwError(() => error);
}
}),
);
} else {
return next(req);

View File

@ -4,13 +4,13 @@
<h1>{{ recipe.title }}</h1>
@if (isLoggedIn()) {
<button id="star" (click)="starMutation.mutate()">
<fa-icon [icon]="faStar"/>
<fa-icon [icon]="faStar" />
<span>Star</span>
<span id="star-count">{{ recipe.starCount }}</span>
</button>
} @else {
<div>
<fa-icon [icon]="faStar"/>
<fa-icon [icon]="faStar" />
<span id="star-count">{{ recipe.starCount }}</span>
</div>
}

View File

@ -2,21 +2,18 @@ import { Component, computed, inject, input, Input } from '@angular/core';
import { RecipeView } from '../../model/Recipe.model';
import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
import { ImageService } from '../../service/image.service';
import { faStar } from "@fortawesome/free-solid-svg-icons";
import { faStar } from '@fortawesome/free-solid-svg-icons';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { RecipeService } from '../../service/recipe.service';
import { AuthService } from '../../service/auth.service';
@Component({
selector: 'app-recipe-page-content',
imports: [
FaIconComponent
],
imports: [FaIconComponent],
templateUrl: './recipe-page-content.html',
styleUrl: './recipe-page-content.css',
})
export class RecipePageContent {
public recipeView = input.required<RecipeView>();
private readonly imageService = inject(ImageService);
@ -30,7 +27,7 @@ export class RecipePageContent {
return {
queryKey: ['images', recipe.mainImage.owner.username, recipe.mainImage.filename],
queryFn: () => this.imageService.getImage(recipe.mainImage.url),
}
};
});
protected readonly starMutation = injectMutation(() => ({
@ -38,5 +35,4 @@ export class RecipePageContent {
}));
protected readonly faStar = faStar;
}

View File

@ -1,7 +1,7 @@
import { inject, Injectable, Signal, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { LoginView } from '../model/LoginView.model';
import { firstValueFrom } from 'rxjs';
import { firstValueFrom, Observable, tap } from 'rxjs';
import { QueryClient } from '@tanstack/angular-query-experimental';
import { Router } from '@angular/router';
@ -21,7 +21,13 @@ export class AuthService {
public async login(username: string, password: string): Promise<LoginView> {
const loginView = await firstValueFrom(
this.http.post<LoginView>('http://localhost:8080/auth/login', { username, password }),
this.http.post<LoginView>(
'http://localhost:8080/auth/login',
{ username, password },
{
withCredentials: true,
},
),
);
this._accessToken.set(loginView.accessToken);
this._username.set(loginView.username);
@ -36,4 +42,19 @@ export class AuthService {
await this.router.navigate(['/']);
await this.queryClient.invalidateQueries();
}
public refresh(): Observable<LoginView> {
this._accessToken.set(null);
this._username.set(null);
return this.http
.post<LoginView>('http://localhost:8080/auth/refresh', null, {
withCredentials: true,
})
.pipe(
tap((loginView) => {
this._username.set(loginView.username);
this._accessToken.set(loginView.accessToken);
}),
);
}
}

View File

@ -23,7 +23,7 @@ export class RecipeService {
public async getRecipeView(username: string, slug: string): Promise<RecipeView> {
return firstValueFrom(
this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`)
this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`),
);
}
@ -40,10 +40,9 @@ export class RecipeService {
}
await this.queryClient.invalidateQueries({
queryKey: ['recipe', recipeView.recipe.owner.username, recipeView.recipe.slug],
})
});
} else {
throw new Error('Cannot star a recipe when not logged in.')
throw new Error('Cannot star a recipe when not logged in.');
}
}
}