Ajout de l'authentification (compte unique, JWT, reset par email)
Application mono-utilisateur : inscription unique, connexion par JWT (12 h), protection de toutes les routes de données, réinitialisation de mot de passe par email avec repli de journalisation, onboarding réorganisé en deux étapes (inscription → connexion → profil + véhicule). Backend (SchotzIadKmTracker.Api) : - Entités Compte (singleton) et ResetPasswordToken + migration AjoutAuthentification. - Hachage PasswordHasher, JwtService (HS256, durée configurable), IEmailService (MailKit + repli LoggingEmailService selon Smtp:Host). - AuthService (register 409 si compte existant, login 401 générique, forgot-password non-divulgation, reset à usage unique + expiration), AuthController (5 endpoints publics), DTOs + validateurs FluentValidation. - Sécurité pipeline : JwtBearer + filtre d'autorisation global, UseAuthentication avant UseAuthorization ; /health et /api/auth/* publics. Frontend (schotziad-km-tracker, zoneless + signals) : - AuthService (localStorage), authInterceptor (Bearer + purge/redirection 401), authGuard, écrans Login/Register/ForgotPassword/ResetPassword, routes publiques et protection [authGuard, profilConfigureGuard]. Configuration : .env.example et docker-compose.yml (JWT__SecretKey, durées, Smtp__*). Tests : 54 tests backend (propriétés FsCheck P1–P14, exemples, intégration WebApplicationFactory) + tests frontend (AuthService, authGuard, authInterceptor, ResetPasswordComponent). Suite complète au vert. Spec : .kiro/specs/authentification (requirements, design, tasks).
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router, UrlTree } from '@angular/router';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot
|
||||
} from '@angular/router';
|
||||
|
||||
import { authGuard } from './auth.guard';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
/**
|
||||
* Tests unitaires de la garde `authGuard` (CanActivateFn).
|
||||
*
|
||||
* Tâche 10.4 — Exigences 4.6 (routes protégées), 9.3 (redirection onboarding/login).
|
||||
* La garde est exécutée via `TestBed.runInInjectionContext` pour disposer du
|
||||
* contexte d'injection (`inject(...)`).
|
||||
*/
|
||||
describe('authGuard', () => {
|
||||
let estAuthentifieValeur: boolean;
|
||||
let urlTreeFactice: UrlTree;
|
||||
let createUrlTreeSpy: jasmine.Spy;
|
||||
|
||||
// Paramètres de la CanActivateFn (non utilisés par la garde, mais requis).
|
||||
const route = {} as ActivatedRouteSnapshot;
|
||||
const state = { url: '/consultation' } as RouterStateSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
urlTreeFactice = {} as UrlTree;
|
||||
|
||||
const authServiceMock: Partial<AuthService> = {
|
||||
estAuthentifie: (() => estAuthentifieValeur) as AuthService['estAuthentifie']
|
||||
};
|
||||
|
||||
createUrlTreeSpy = jasmine.createSpy('createUrlTree').and.returnValue(urlTreeFactice);
|
||||
const routerMock: Partial<Router> = {
|
||||
createUrlTree: createUrlTreeSpy as unknown as Router['createUrlTree']
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authServiceMock },
|
||||
{ provide: Router, useValue: routerMock }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('retourne true lorsque l utilisateur est authentifié', () => {
|
||||
estAuthentifieValeur = true;
|
||||
|
||||
const resultat = TestBed.runInInjectionContext(() => authGuard(route, state));
|
||||
|
||||
expect(resultat).toBeTrue();
|
||||
expect(createUrlTreeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retourne un UrlTree vers /login lorsque l utilisateur n est pas authentifié', () => {
|
||||
estAuthentifieValeur = false;
|
||||
|
||||
const resultat = TestBed.runInInjectionContext(() => authGuard(route, state));
|
||||
|
||||
expect(createUrlTreeSpy).toHaveBeenCalledWith(['/login']);
|
||||
expect(resultat).toBe(urlTreeFactice);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router, UrlTree } from '@angular/router';
|
||||
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
/**
|
||||
* Garde de route `CanActivateFn` protégeant l'accès aux routes nécessitant une
|
||||
* authentification.
|
||||
*
|
||||
* Comportement (Exigences 4.6, 9.3) :
|
||||
* - Si `AuthService.estAuthentifie()` est vrai → navigation autorisée (retourne `true`).
|
||||
* - Sinon → redirection vers `/login` (retourne un `UrlTree`), ce qui bloque l'accès
|
||||
* à la route protégée tant que l'utilisateur n'est pas connecté. (Exigences 4.6, 9.3)
|
||||
*
|
||||
* Elle se compose avec la `profilConfigureGuard` existante : sur les routes de données,
|
||||
* on applique `[authGuard, profilConfigureGuard]` (l'authentification est vérifiée d'abord,
|
||||
* puis la configuration du profil).
|
||||
*/
|
||||
export const authGuard: CanActivateFn = (): boolean | UrlTree => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
return authService.estAuthentifie() ? true : router.createUrlTree(['/login']);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
HttpClient,
|
||||
provideHttpClient,
|
||||
withInterceptors
|
||||
} from '@angular/common/http';
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting
|
||||
} from '@angular/common/http/testing';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { authInterceptor } from './auth.interceptor';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
/**
|
||||
* Tests unitaires de l'intercepteur `authInterceptor`.
|
||||
*
|
||||
* Tâche 10.4 — Exigence 4.7 :
|
||||
* - injection du Bearer sur les appels API lorsqu'un token est présent ;
|
||||
* - pas de Bearer sur les endpoints publics d'auth ;
|
||||
* - sur une réponse 401 : purge du token (AuthService.clearToken) + navigation /login.
|
||||
*/
|
||||
describe('authInterceptor', () => {
|
||||
let httpMock: HttpTestingController;
|
||||
let http: HttpClient;
|
||||
let clearTokenSpy: jasmine.Spy;
|
||||
let navigateSpy: jasmine.Spy;
|
||||
let tokenCourant: string | null;
|
||||
|
||||
beforeEach(() => {
|
||||
tokenCourant = 'jwt-abc';
|
||||
clearTokenSpy = jasmine.createSpy('clearToken');
|
||||
navigateSpy = jasmine.createSpy('navigate');
|
||||
|
||||
const authServiceMock: Partial<AuthService> = {
|
||||
getToken: () => tokenCourant,
|
||||
clearToken: clearTokenSpy as unknown as AuthService['clearToken']
|
||||
};
|
||||
const routerMock: Partial<Router> = {
|
||||
navigate: navigateSpy as unknown as Router['navigate']
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(withInterceptors([authInterceptor])),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: AuthService, useValue: authServiceMock },
|
||||
{ provide: Router, useValue: routerMock }
|
||||
]
|
||||
});
|
||||
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
http = TestBed.inject(HttpClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('ajoute l en-tête Authorization Bearer sur un appel API lorsqu un token est présent', () => {
|
||||
const url = `${environment.apiUrl}/trajets`;
|
||||
http.get(url).subscribe();
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
expect(req.request.headers.get('Authorization')).toBe('Bearer jwt-abc');
|
||||
req.flush([]);
|
||||
});
|
||||
|
||||
it('n ajoute pas le Bearer sur un endpoint public d auth (/auth/login)', () => {
|
||||
const url = `${environment.apiUrl}/auth/login`;
|
||||
http.post(url, { email: 'a@b.fr', motDePasse: 'x' }).subscribe();
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
expect(req.request.headers.has('Authorization')).toBeFalse();
|
||||
req.flush({ token: 't', expiration: '2030-01-01T00:00:00Z' });
|
||||
});
|
||||
|
||||
it('n ajoute pas le Bearer lorsqu aucun token n est présent', () => {
|
||||
tokenCourant = null;
|
||||
const url = `${environment.apiUrl}/trajets`;
|
||||
http.get(url).subscribe();
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
expect(req.request.headers.has('Authorization')).toBeFalse();
|
||||
req.flush([]);
|
||||
});
|
||||
|
||||
it('purge le token et redirige vers /login sur une réponse 401', () => {
|
||||
const url = `${environment.apiUrl}/trajets`;
|
||||
http.get(url).subscribe({
|
||||
next: () => fail('la requête aurait dû échouer'),
|
||||
error: () => {
|
||||
/* l erreur est propagée : attendu */
|
||||
}
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
req.flush({ message: 'non autorisé' }, { status: 401, statusText: 'Unauthorized' });
|
||||
|
||||
expect(clearTokenSpy).toHaveBeenCalled();
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['/login']);
|
||||
});
|
||||
|
||||
it('ne purge pas le token sur une erreur non-401', () => {
|
||||
const url = `${environment.apiUrl}/trajets`;
|
||||
http.get(url).subscribe({
|
||||
next: () => fail('la requête aurait dû échouer'),
|
||||
error: () => {
|
||||
/* attendu */
|
||||
}
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
req.flush({ message: 'erreur serveur' }, { status: 500, statusText: 'Server Error' });
|
||||
|
||||
expect(clearTokenSpy).not.toHaveBeenCalled();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
/**
|
||||
* Fragments de chemin correspondant aux endpoints publics d'authentification.
|
||||
* Aucune requête vers ces endpoints ne doit porter l'en-tête `Authorization`
|
||||
* (l'utilisateur n'est pas encore authentifié à ce stade).
|
||||
*/
|
||||
const ENDPOINTS_AUTH_PUBLICS = [
|
||||
'/auth/login',
|
||||
'/auth/register',
|
||||
'/auth/forgot-password',
|
||||
'/auth/reset-password',
|
||||
'/auth/compte-existe'
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Intercepteur HTTP fonctionnel d'authentification (Exigence 4.7) :
|
||||
* - ajoute l'en-tête `Authorization: Bearer <token>` aux requêtes vers l'API
|
||||
* backend (`environment.apiUrl`) lorsqu'un token est présent, sauf pour les
|
||||
* endpoints publics d'auth et les appels externes (ex. BAN) ;
|
||||
* - sur une réponse en erreur 401, purge le token et redirige vers `/login`,
|
||||
* puis propage l'erreur.
|
||||
*
|
||||
* Il s'enregistre AVANT le `timeoutInterceptor` afin d'injecter le Bearer avant
|
||||
* le contrôle de timeout.
|
||||
*/
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
const cibleApi = req.url.startsWith(environment.apiUrl);
|
||||
const estEndpointPublic = ENDPOINTS_AUTH_PUBLICS.some((chemin) => req.url.includes(chemin));
|
||||
const token = authService.getToken();
|
||||
|
||||
// On n'ajoute le Bearer qu'aux appels vers notre API backend, hors endpoints
|
||||
// publics d'auth, et uniquement lorsqu'un token est disponible. Les appels
|
||||
// externes (BAN, etc.) restent inchangés.
|
||||
const requete =
|
||||
cibleApi && !estEndpointPublic && token
|
||||
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
|
||||
: req;
|
||||
|
||||
return next(requete).pipe(
|
||||
catchError((error: unknown) => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||
// Jeton absent, expiré ou invalide : purge sans double navigation puis
|
||||
// redirection explicite vers l'écran de connexion.
|
||||
authService.clearToken();
|
||||
router.navigate(['/login']);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
// Modèles Authentification — miroir de AuthDtos.cs (backend).
|
||||
|
||||
/** Identifiants de connexion — miroir de LoginDto. */
|
||||
export interface LoginDto {
|
||||
email: string;
|
||||
motDePasse: string;
|
||||
}
|
||||
|
||||
/** Données d'inscription — miroir de RegisterDto. */
|
||||
export interface RegisterDto {
|
||||
email: string;
|
||||
motDePasse: string;
|
||||
}
|
||||
|
||||
/** Résultat d'une connexion réussie — miroir de LoginResultDto. */
|
||||
export interface LoginResult {
|
||||
token: string;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
/** Réinitialisation du mot de passe — miroir de ResetPasswordDto. */
|
||||
export interface ResetPasswordDto {
|
||||
token: string;
|
||||
nouveauMotDePasse: string;
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export * from './vehicule.model';
|
||||
export * from './profil.model';
|
||||
export * from './bareme.model';
|
||||
export * from './adresse.model';
|
||||
export * from './auth.model';
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting
|
||||
} from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginResult } from '../models';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
/**
|
||||
* Tests unitaires du service d'authentification.
|
||||
*
|
||||
* Tâche 10.4 — Exigences 2.3 (persistance du token / session), 3.1 (déconnexion).
|
||||
* Vérifie le stockage du JWT dans localStorage (clé `auth_token`), le reflet via
|
||||
* le signal `estAuthentifie()`/`getToken()`, et la purge à la déconnexion.
|
||||
*/
|
||||
describe('AuthService', () => {
|
||||
const CLE_TOKEN = 'auth_token';
|
||||
const baseUrl = `${environment.apiUrl}/auth`;
|
||||
|
||||
let service: AuthService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
// Environnement propre : aucun token résiduel avant construction du service.
|
||||
localStorage.clear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
provideRouter([])
|
||||
]
|
||||
});
|
||||
|
||||
service = TestBed.inject(AuthService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('démarre non authentifié lorsque localStorage est vide', () => {
|
||||
expect(service.getToken()).toBeNull();
|
||||
expect(service.estAuthentifie()).toBeFalse();
|
||||
});
|
||||
|
||||
it('login() envoie un POST vers {apiUrl}/auth/login', () => {
|
||||
service.login({ email: 'a@b.fr', motDePasse: 'motdepasse' }).subscribe();
|
||||
|
||||
const req = httpMock.expectOne(`${baseUrl}/login`);
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({ email: 'a@b.fr', motDePasse: 'motdepasse' });
|
||||
|
||||
req.flush({ token: 'jwt-123', expiration: '2030-01-01T00:00:00Z' } as LoginResult);
|
||||
});
|
||||
|
||||
it('login() stocke le token dans localStorage et passe estAuthentifie() à true', () => {
|
||||
let resultat: LoginResult | undefined;
|
||||
service
|
||||
.login({ email: 'a@b.fr', motDePasse: 'motdepasse' })
|
||||
.subscribe((r) => (resultat = r));
|
||||
|
||||
const req = httpMock.expectOne(`${baseUrl}/login`);
|
||||
req.flush({ token: 'jwt-123', expiration: '2030-01-01T00:00:00Z' } as LoginResult);
|
||||
|
||||
expect(resultat?.token).toBe('jwt-123');
|
||||
expect(localStorage.getItem(CLE_TOKEN)).toBe('jwt-123');
|
||||
expect(service.getToken()).toBe('jwt-123');
|
||||
expect(service.estAuthentifie()).toBeTrue();
|
||||
});
|
||||
|
||||
it('logout() supprime le token de localStorage et repasse estAuthentifie() à false', () => {
|
||||
// On authentifie d'abord via login pour partir d'un état connecté réel.
|
||||
service.login({ email: 'a@b.fr', motDePasse: 'motdepasse' }).subscribe();
|
||||
httpMock
|
||||
.expectOne(`${baseUrl}/login`)
|
||||
.flush({ token: 'jwt-123', expiration: '2030-01-01T00:00:00Z' } as LoginResult);
|
||||
expect(service.estAuthentifie()).toBeTrue();
|
||||
|
||||
service.logout();
|
||||
|
||||
expect(localStorage.getItem(CLE_TOKEN)).toBeNull();
|
||||
expect(service.getToken()).toBeNull();
|
||||
expect(service.estAuthentifie()).toBeFalse();
|
||||
});
|
||||
|
||||
it('clearToken() purge le token sans naviguer', () => {
|
||||
service.login({ email: 'a@b.fr', motDePasse: 'motdepasse' }).subscribe();
|
||||
httpMock
|
||||
.expectOne(`${baseUrl}/login`)
|
||||
.flush({ token: 'jwt-123', expiration: '2030-01-01T00:00:00Z' } as LoginResult);
|
||||
expect(service.estAuthentifie()).toBeTrue();
|
||||
|
||||
service.clearToken();
|
||||
|
||||
expect(localStorage.getItem(CLE_TOKEN)).toBeNull();
|
||||
expect(service.getToken()).toBeNull();
|
||||
expect(service.estAuthentifie()).toBeFalse();
|
||||
});
|
||||
|
||||
it('getToken() reflète le contenu de localStorage à l initialisation', () => {
|
||||
// Simule un token déjà présent au démarrage puis reconstruit le service.
|
||||
localStorage.setItem(CLE_TOKEN, 'token-persistant');
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
provideRouter([])
|
||||
]
|
||||
});
|
||||
|
||||
const serviceReinit = TestBed.inject(AuthService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
|
||||
expect(serviceReinit.getToken()).toBe('token-persistant');
|
||||
expect(serviceReinit.estAuthentifie()).toBeTrue();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { LoginDto, LoginResult, RegisterDto, ResetPasswordDto } from '../models';
|
||||
|
||||
/** Clé de stockage du JWT dans localStorage. */
|
||||
const CLE_TOKEN = 'auth_token';
|
||||
|
||||
/**
|
||||
* Service d'authentification.
|
||||
*
|
||||
* Expose l'état d'authentification via des signals (contrainte zoneless).
|
||||
* Le JWT est conservé dans localStorage pour une session persistante, et le
|
||||
* signal `_token` reflète en permanence l'état courant.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly router = inject(Router);
|
||||
private readonly baseUrl = `${environment.apiUrl}/auth`;
|
||||
|
||||
/** Token courant (initialisé depuis localStorage au démarrage). */
|
||||
private readonly _token = signal<string | null>(this.lireTokenStocke());
|
||||
|
||||
/** Vrai lorsqu'un token est présent. */
|
||||
readonly estAuthentifie = computed(() => this._token() !== null);
|
||||
|
||||
/** Connexion (POST /auth/login) : stocke le token retourné sur succès. */
|
||||
login(dto: LoginDto): Observable<LoginResult> {
|
||||
return this.http
|
||||
.post<LoginResult>(`${this.baseUrl}/login`, dto)
|
||||
.pipe(tap((resultat) => this.enregistrerToken(resultat.token)));
|
||||
}
|
||||
|
||||
/** Inscription (POST /auth/register). */
|
||||
register(dto: RegisterDto): Observable<void> {
|
||||
return this.http.post<void>(`${this.baseUrl}/register`, dto);
|
||||
}
|
||||
|
||||
/** Demande de réinitialisation du mot de passe (POST /auth/forgot-password). */
|
||||
forgotPassword(email: string): Observable<void> {
|
||||
return this.http.post<void>(`${this.baseUrl}/forgot-password`, { email });
|
||||
}
|
||||
|
||||
/** Réinitialisation du mot de passe (POST /auth/reset-password). */
|
||||
resetPassword(dto: ResetPasswordDto): Observable<void> {
|
||||
return this.http.post<void>(`${this.baseUrl}/reset-password`, dto);
|
||||
}
|
||||
|
||||
/** Indique si un compte existe déjà (GET /auth/compte-existe). */
|
||||
compteExiste(): Observable<{ existe: boolean }> {
|
||||
return this.http.get<{ existe: boolean }>(`${this.baseUrl}/compte-existe`);
|
||||
}
|
||||
|
||||
/** Retourne le token courant, ou null si non authentifié. */
|
||||
getToken(): string | null {
|
||||
return this._token();
|
||||
}
|
||||
|
||||
/** Déconnexion : purge le token et redirige vers l'écran de connexion. */
|
||||
logout(): void {
|
||||
this.supprimerToken();
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge le token sans naviguer.
|
||||
* Utilisé par l'`authInterceptor` sur une réponse 401 : l'intercepteur gère
|
||||
* lui-même la redirection vers `/login` (évite une double navigation).
|
||||
*/
|
||||
clearToken(): void {
|
||||
this.supprimerToken();
|
||||
}
|
||||
|
||||
/** Lecture initiale du token depuis localStorage. */
|
||||
private lireTokenStocke(): string | null {
|
||||
return localStorage.getItem(CLE_TOKEN);
|
||||
}
|
||||
|
||||
/** Écriture du token dans localStorage et mise à jour du signal. */
|
||||
private enregistrerToken(token: string): void {
|
||||
localStorage.setItem(CLE_TOKEN, token);
|
||||
this._token.set(token);
|
||||
}
|
||||
|
||||
/** Suppression du token dans localStorage et mise à jour du signal. */
|
||||
private supprimerToken(): void {
|
||||
localStorage.removeItem(CLE_TOKEN);
|
||||
this._token.set(null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user