diff --git a/admin/src/app/app.ts b/admin/src/app/app.ts index b0f929f..2a07864 100644 --- a/admin/src/app/app.ts +++ b/admin/src/app/app.ts @@ -3,6 +3,7 @@ import { Router, RouterOutlet } from '@angular/router'; import { MainMenu } from './components/main-menu/main-menu'; import { AuthService } from './auth/auth.service'; import { AdminLayout } from './layout/admin-layout/admin-layout'; +import { finalize } from 'rxjs/operators'; @Component({ selector: 'app-root', @@ -16,8 +17,12 @@ export class App { constructor(private authService: AuthService, private router: Router) {} logout(): void { - this.authService.logout().subscribe(() => { - this.router.navigate(['/login']); - }); + // Make a best-effort to log out on the server, but always + // clean up the client-side session in the `finalize` block. + this.authService.serverSideLogout().pipe( + finalize(() => { + this.authService.clientSideLogout(); + }) + ).subscribe(); } } diff --git a/admin/src/app/auth/auth.service.ts b/admin/src/app/auth/auth.service.ts index da22cd6..0475e0b 100644 --- a/admin/src/app/auth/auth.service.ts +++ b/admin/src/app/auth/auth.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { Router } from '@angular/router'; @Injectable({ providedIn: 'root', @@ -11,7 +12,7 @@ export class AuthService { private readonly REFRESH_TOKEN_KEY = 'refreshToken'; private apiUrl = 'http://localhost:4200/api/auth'; // Adjust if your server URL is different - constructor(private http: HttpClient) {} + constructor(private http: HttpClient, private router: Router) {} login(credentials: { username: string; password: string }): Observable { return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/login`, credentials).pipe( @@ -19,10 +20,20 @@ export class AuthService { ); } - logout(): Observable { - return this.http.post(`${this.apiUrl}/logout`, {}).pipe( - tap(() => this.removeTokens()) - ); + /** + * Makes a best-effort call to the server to invalidate the refresh token. + */ + serverSideLogout(): Observable { + return this.http.post(`${this.apiUrl}/logout`, {}); + } + + /** + * Performs the client-side cleanup, removing tokens and redirecting to login. + * This is the definitive logout action from the user's perspective. + */ + clientSideLogout(): void { + this.removeTokens(); + this.router.navigate(['/login']); } refreshToken(): Observable { diff --git a/admin/src/app/auth/jwt.interceptor.ts b/admin/src/app/auth/jwt.interceptor.ts index d14a214..31ac8d3 100644 --- a/admin/src/app/auth/jwt.interceptor.ts +++ b/admin/src/app/auth/jwt.interceptor.ts @@ -9,14 +9,13 @@ import { import { Observable, throwError, BehaviorSubject } from 'rxjs'; import { catchError, switchMap, filter, take } from 'rxjs/operators'; import { AuthService } from './auth.service'; -import { Router } from '@angular/router'; @Injectable() export class JwtInterceptor implements HttpInterceptor { private isRefreshing = false; private refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null); - constructor(private authService: AuthService, private router: Router) {} + constructor(private authService: AuthService) {} intercept( request: HttpRequest, @@ -42,9 +41,6 @@ export class JwtInterceptor implements HttpInterceptor { private handle401Error(request: HttpRequest, next: HttpHandler) { if (!this.isRefreshing) { this.isRefreshing = true; - - // The subject is now single-use. Re-create it for each refresh cycle. - // The initial `null` value is what makes followers wait. this.refreshTokenSubject = new BehaviorSubject(null); return this.authService.refreshToken().pipe( @@ -55,16 +51,12 @@ export class JwtInterceptor implements HttpInterceptor { }), catchError((err) => { this.isRefreshing = false; - - // Propagate the error to all waiting followers and kill the subject. this.refreshTokenSubject.error(err); - // Perform the logout and redirect - this.authService.logout().subscribe(() => { - this.router.navigate(['/login']); - }); + // In a refresh failure, the user MUST be logged out. + // Call the synchronous client-side logout to avoid re-intercepting. + this.authService.clientSideLogout(); - // Also ensure the original caller gets the error return throwError(() => err); }) ); diff --git a/server/api.http b/server/api.http index 01f1c4d..fe444d4 100644 --- a/server/api.http +++ b/server/api.http @@ -1,4 +1,4 @@ -POST http://localhost:3000/auth/login +POST {{apiBaseUrl}}/auth/login Content-Type: application/json { @@ -6,10 +6,10 @@ Content-Type: application/json "password": "123456" } -> {% client.global.set("auth_token", response.body.access_token); %} +> {% client.global.set("auth_token", response.body.accessToken); %} ### GET request with parameter -GET http://localhost:3000/users +GET {{apiBaseUrl}}/users Accept: application/json Authorization: Bearer {{auth_token}} diff --git a/server/http-client.env.json b/server/http-client.env.json new file mode 100644 index 0000000..022ab66 --- /dev/null +++ b/server/http-client.env.json @@ -0,0 +1,5 @@ +{ + "dev": { + "apiBaseUrl": "http://localhost:3000/api" + } +} \ No newline at end of file