Add refresh logic to auth interceptor and service. Misc. prettier.
This commit is contained in:
parent
28222e0655
commit
80752f7513
@ -1,15 +1,48 @@
|
|||||||
import { HttpInterceptorFn } from '@angular/common/http';
|
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { AuthService } from '../service/auth.service';
|
import { AuthService } from '../service/auth.service';
|
||||||
|
import { catchError, from, switchMap, throwError } from 'rxjs';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
|
const router = inject(Router);
|
||||||
const token = authService.accessToken();
|
const token = authService.accessToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
|
// first we try with the current token
|
||||||
return next(
|
return next(
|
||||||
req.clone({
|
req.clone({
|
||||||
headers: req.headers.set('Authorization', `Bearer ${token}`),
|
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 {
|
} else {
|
||||||
return next(req);
|
return next(req);
|
||||||
|
|||||||
@ -4,13 +4,13 @@
|
|||||||
<h1>{{ recipe.title }}</h1>
|
<h1>{{ recipe.title }}</h1>
|
||||||
@if (isLoggedIn()) {
|
@if (isLoggedIn()) {
|
||||||
<button id="star" (click)="starMutation.mutate()">
|
<button id="star" (click)="starMutation.mutate()">
|
||||||
<fa-icon [icon]="faStar"/>
|
<fa-icon [icon]="faStar" />
|
||||||
<span>Star</span>
|
<span>Star</span>
|
||||||
<span id="star-count">{{ recipe.starCount }}</span>
|
<span id="star-count">{{ recipe.starCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<div>
|
<div>
|
||||||
<fa-icon [icon]="faStar"/>
|
<fa-icon [icon]="faStar" />
|
||||||
<span id="star-count">{{ recipe.starCount }}</span>
|
<span id="star-count">{{ recipe.starCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,21 +2,18 @@ import { Component, computed, inject, input, Input } from '@angular/core';
|
|||||||
import { RecipeView } from '../../model/Recipe.model';
|
import { RecipeView } from '../../model/Recipe.model';
|
||||||
import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
import { ImageService } from '../../service/image.service';
|
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 { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { RecipeService } from '../../service/recipe.service';
|
import { RecipeService } from '../../service/recipe.service';
|
||||||
import { AuthService } from '../../service/auth.service';
|
import { AuthService } from '../../service/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recipe-page-content',
|
selector: 'app-recipe-page-content',
|
||||||
imports: [
|
imports: [FaIconComponent],
|
||||||
FaIconComponent
|
|
||||||
],
|
|
||||||
templateUrl: './recipe-page-content.html',
|
templateUrl: './recipe-page-content.html',
|
||||||
styleUrl: './recipe-page-content.css',
|
styleUrl: './recipe-page-content.css',
|
||||||
})
|
})
|
||||||
export class RecipePageContent {
|
export class RecipePageContent {
|
||||||
|
|
||||||
public recipeView = input.required<RecipeView>();
|
public recipeView = input.required<RecipeView>();
|
||||||
|
|
||||||
private readonly imageService = inject(ImageService);
|
private readonly imageService = inject(ImageService);
|
||||||
@ -30,7 +27,7 @@ export class RecipePageContent {
|
|||||||
return {
|
return {
|
||||||
queryKey: ['images', recipe.mainImage.owner.username, recipe.mainImage.filename],
|
queryKey: ['images', recipe.mainImage.owner.username, recipe.mainImage.filename],
|
||||||
queryFn: () => this.imageService.getImage(recipe.mainImage.url),
|
queryFn: () => this.imageService.getImage(recipe.mainImage.url),
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
protected readonly starMutation = injectMutation(() => ({
|
protected readonly starMutation = injectMutation(() => ({
|
||||||
@ -38,5 +35,4 @@ export class RecipePageContent {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
protected readonly faStar = faStar;
|
protected readonly faStar = faStar;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { inject, Injectable, Signal, signal } from '@angular/core';
|
import { inject, Injectable, Signal, signal } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { LoginView } from '../model/LoginView.model';
|
import { LoginView } from '../model/LoginView.model';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom, Observable, tap } from 'rxjs';
|
||||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@ -21,7 +21,13 @@ export class AuthService {
|
|||||||
|
|
||||||
public async login(username: string, password: string): Promise<LoginView> {
|
public async login(username: string, password: string): Promise<LoginView> {
|
||||||
const loginView = await firstValueFrom(
|
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._accessToken.set(loginView.accessToken);
|
||||||
this._username.set(loginView.username);
|
this._username.set(loginView.username);
|
||||||
@ -36,4 +42,19 @@ export class AuthService {
|
|||||||
await this.router.navigate(['/']);
|
await this.router.navigate(['/']);
|
||||||
await this.queryClient.invalidateQueries();
|
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);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class RecipeService {
|
|||||||
|
|
||||||
public async getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
public async getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
||||||
return firstValueFrom(
|
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({
|
await this.queryClient.invalidateQueries({
|
||||||
queryKey: ['recipe', recipeView.recipe.owner.username, recipeView.recipe.slug],
|
queryKey: ['recipe', recipeView.recipe.owner.username, recipeView.recipe.slug],
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Cannot star a recipe when not logged in.')
|
throw new Error('Cannot star a recipe when not logged in.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user