Skip to content

Commit 658f2aa

Browse files
committed
feat(authentication): remove cookies for tokens and use local storage
1 parent 911e537 commit 658f2aa

File tree

9 files changed

+191
-70
lines changed

9 files changed

+191
-70
lines changed

src/app/core/interceptors/authentication.interceptor.ts

Lines changed: 136 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ import { BehaviorSubject, throwError } from 'rxjs';
99
import { catchError, filter, switchMap, take } from 'rxjs/operators';
1010
import { inject } from '@angular/core';
1111
import { Router } from '@angular/router';
12-
import { AuthenticationService } from '~features/authentication/services/authentication.service';
12+
import {
13+
ACCESS_TOKEN_KEY,
14+
AuthenticationService,
15+
} from '~features/authentication/services/authentication.service';
1316
import { AppError } from '~core/enums/app-error.enum';
1417
import { AUTH_URLS } from '~core/constants/urls.constants';
18+
import { LOCAL_STORAGE } from '~core/providers/local-storage';
19+
import { translations } from '../../../locale/translations';
20+
import { AlertService } from '~core/services/alert.service';
1521

1622
const isRefreshing = new BehaviorSubject<boolean>(false);
1723

@@ -20,24 +26,74 @@ export function authenticationInterceptor(
2026
next: HttpHandlerFn,
2127
): Observable<HttpEvent<unknown>> {
2228
const authenticationService = inject(AuthenticationService);
29+
const alertService = inject(AlertService);
30+
const storageService = inject(LOCAL_STORAGE);
2331
const router = inject(Router);
24-
return next(request).pipe(
25-
catchError((errorResponse: HttpErrorResponse) => {
26-
if (isAccessTokenError(errorResponse)) {
27-
return tryRefreshToken(request, next, authenticationService);
28-
}
29-
30-
if (isRefreshTokenError(errorResponse)) {
31-
authenticationService.logOut();
32-
void router.navigate([AUTH_URLS.logIn]);
33-
return throwError(() => new Error('Session expired. Please log in again.'));
34-
}
35-
36-
return throwError(() => errorResponse);
37-
}),
32+
33+
const clonedRequest = attachAccessToken(request, storageService);
34+
return handleRequest({
35+
request: clonedRequest,
36+
next,
37+
authenticationService,
38+
alertService,
39+
storageService,
40+
router,
41+
});
42+
}
43+
44+
function attachAccessToken(
45+
request: HttpRequest<unknown>,
46+
storageService: Storage | null,
47+
): HttpRequest<unknown> {
48+
const accessToken = storageService?.getItem(ACCESS_TOKEN_KEY);
49+
if (accessToken) {
50+
return request.clone({
51+
setHeaders: { Authorization: `Bearer ${accessToken}` },
52+
});
53+
}
54+
return request;
55+
}
56+
57+
function handleRequest(parameters: {
58+
request: HttpRequest<unknown>;
59+
next: HttpHandlerFn;
60+
authenticationService: AuthenticationService;
61+
alertService: AlertService;
62+
storageService: Storage | null;
63+
router: Router;
64+
}): Observable<HttpEvent<unknown>> {
65+
return parameters.next(parameters.request).pipe(
66+
catchError((errorResponse: HttpErrorResponse) =>
67+
handleErrors({
68+
errorResponse,
69+
...parameters,
70+
}),
71+
),
3872
);
3973
}
4074

75+
function handleErrors(parameters: {
76+
request: HttpRequest<unknown>;
77+
next: HttpHandlerFn;
78+
authenticationService: AuthenticationService;
79+
alertService: AlertService;
80+
storageService: Storage | null;
81+
router: Router;
82+
errorResponse: HttpErrorResponse;
83+
}): Observable<HttpEvent<unknown>> {
84+
if (isAccessTokenError(parameters.errorResponse)) {
85+
return tryRefreshToken(parameters);
86+
}
87+
88+
if (isRefreshTokenError(parameters.errorResponse)) {
89+
parameters.authenticationService.logOut();
90+
void parameters.router.navigate([AUTH_URLS.logIn]);
91+
return throwError(() => new Error('Session expired. Please log in again.'));
92+
}
93+
94+
return throwError(() => parameters.errorResponse);
95+
}
96+
4197
function isAccessTokenError(errorResponse: HttpErrorResponse): boolean {
4298
return (
4399
errorResponse.status === 401 &&
@@ -56,30 +112,76 @@ function isRefreshTokenError(errorResponse: HttpErrorResponse): boolean {
56112
);
57113
}
58114

59-
// eslint-disable-next-line @typescript-eslint/max-params
60-
function tryRefreshToken(
61-
request: HttpRequest<unknown>,
62-
next: HttpHandlerFn,
63-
authenticationService: AuthenticationService,
64-
): Observable<HttpEvent<unknown>> {
115+
function tryRefreshToken(parameters: {
116+
request: HttpRequest<unknown>;
117+
next: HttpHandlerFn;
118+
authenticationService: AuthenticationService;
119+
alertService: AlertService;
120+
storageService: Storage | null;
121+
router: Router;
122+
}): Observable<HttpEvent<unknown>> {
65123
if (!isRefreshing.getValue()) {
66-
isRefreshing.next(true);
67-
68-
return authenticationService.refreshToken().pipe(
69-
switchMap(() => {
70-
isRefreshing.next(false);
71-
return next(request.clone({ withCredentials: true }));
72-
}),
73-
catchError((error: HttpErrorResponse) => {
74-
isRefreshing.next(false);
75-
return throwError(() => error);
76-
}),
77-
);
124+
return handleTokenRefresh(parameters);
78125
}
79126

127+
return waitForTokenRefresh(parameters);
128+
}
129+
130+
function handleTokenRefresh(parameters: {
131+
request: HttpRequest<unknown>;
132+
next: HttpHandlerFn;
133+
authenticationService: AuthenticationService;
134+
alertService: AlertService;
135+
storageService: Storage | null;
136+
router: Router;
137+
}): Observable<HttpEvent<unknown>> {
138+
isRefreshing.next(true);
139+
140+
return parameters.authenticationService.refreshToken().pipe(
141+
switchMap(() => {
142+
isRefreshing.next(false);
143+
return retryRequestWithRefreshedToken(parameters);
144+
}),
145+
catchError((error: HttpErrorResponse) => {
146+
isRefreshing.next(false);
147+
handleRefreshError(parameters);
148+
return throwError(() => error);
149+
}),
150+
);
151+
}
152+
153+
function waitForTokenRefresh(parameters: {
154+
request: HttpRequest<unknown>;
155+
next: HttpHandlerFn;
156+
storageService: Storage | null;
157+
}): Observable<HttpEvent<unknown>> {
80158
return isRefreshing.pipe(
81159
filter((refreshing) => !refreshing),
82160
take(1),
83-
switchMap(() => next(request.clone({ withCredentials: true }))),
161+
switchMap(() => retryRequestWithRefreshedToken(parameters)),
84162
);
85163
}
164+
165+
function retryRequestWithRefreshedToken(parameters: {
166+
request: HttpRequest<unknown>;
167+
next: HttpHandlerFn;
168+
storageService: Storage | null;
169+
}): Observable<HttpEvent<unknown>> {
170+
const refreshedToken = parameters.storageService?.getItem(ACCESS_TOKEN_KEY);
171+
const clonedRequest = refreshedToken
172+
? parameters.request.clone({
173+
setHeaders: { Authorization: `Bearer ${refreshedToken}` },
174+
})
175+
: parameters.request;
176+
return parameters.next(clonedRequest);
177+
}
178+
179+
function handleRefreshError(parameters: {
180+
authenticationService: AuthenticationService;
181+
alertService: AlertService;
182+
router: Router;
183+
}): void {
184+
parameters.authenticationService.logOut();
185+
parameters.alertService.createErrorAlert(translations.sessionExpired);
186+
void parameters.router.navigate([AUTH_URLS.logIn]);
187+
}

src/app/features/authentication/services/authentication.service.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import type {
1818
import { LanguageService } from '~core/services/language.service';
1919
import type { User } from '~features/authentication/types/user.type';
2020

21-
const IS_SESSION_ALIVE_KEY = 'isSessionAlive';
21+
export const ACCESS_TOKEN_KEY = 'access-token';
22+
export const REFRESH_TOKEN_KEY = 'refresh-token';
2223

2324
@Injectable({
2425
providedIn: 'root',
@@ -27,9 +28,7 @@ export class AuthenticationService {
2728
private readonly storageService = inject(LOCAL_STORAGE);
2829
private readonly httpClient = inject(HttpClient);
2930
private readonly languageService = inject(LanguageService);
30-
private readonly isUserLoggedInSignal = signal(
31-
!!this.storageService?.getItem(IS_SESSION_ALIVE_KEY),
32-
);
31+
private readonly isUserLoggedInSignal = signal(!!this.storageService?.getItem(ACCESS_TOKEN_KEY));
3332

3433
private readonly apiUrl = environment.apiBaseUrl;
3534

@@ -46,7 +45,6 @@ export class AuthenticationService {
4645
terms: registerRequest.terms,
4746
},
4847
{
49-
withCredentials: true,
5048
headers: {
5149
'Accept-Language': this.languageService.convertLocaleToAcceptLanguage(),
5250
},
@@ -55,7 +53,7 @@ export class AuthenticationService {
5553
.pipe(
5654
map((response: RegisterResponse) => {
5755
const { data } = response;
58-
this.storageService?.setItem(IS_SESSION_ALIVE_KEY, 'true');
56+
this.saveTokens(data);
5957
this.isUserLoggedInSignal.set(true);
6058
return data;
6159
}),
@@ -65,18 +63,14 @@ export class AuthenticationService {
6563
logIn(loginRequest: LoginRequest): Observable<User> {
6664
const loginEndpoint = `${this.apiUrl}/v1/authentication/login`;
6765
return this.httpClient
68-
.post<LoginResponse>(
69-
loginEndpoint,
70-
{
71-
email: loginRequest.email.trim().toLowerCase(),
72-
password: loginRequest.password,
73-
},
74-
{ withCredentials: true },
75-
)
66+
.post<LoginResponse>(loginEndpoint, {
67+
email: loginRequest.email.trim().toLowerCase(),
68+
password: loginRequest.password,
69+
})
7670
.pipe(
7771
map((response: LoginResponse) => {
7872
const { data } = response;
79-
this.storageService?.setItem(IS_SESSION_ALIVE_KEY, 'true');
73+
this.saveTokens(data);
8074
this.isUserLoggedInSignal.set(true);
8175
return data.user;
8276
}),
@@ -85,19 +79,37 @@ export class AuthenticationService {
8579

8680
refreshToken(): Observable<RefreshTokenResponseData> {
8781
const refreshTokenEndpoint = `${this.apiUrl}/v1/authentication/token/refresh`;
88-
return this.httpClient.post<RefreshTokenResponse>(
89-
refreshTokenEndpoint,
90-
{},
91-
{ withCredentials: true },
92-
);
82+
return this.httpClient
83+
.post<RefreshTokenResponse>(refreshTokenEndpoint, {
84+
refreshToken: this.storageService?.getItem(REFRESH_TOKEN_KEY),
85+
})
86+
.pipe(
87+
map((response: RefreshTokenResponse) => {
88+
const { data } = response;
89+
this.saveTokens(data);
90+
return data;
91+
}),
92+
);
9393
}
9494

9595
logOut() {
96-
this.storageService?.removeItem(IS_SESSION_ALIVE_KEY);
96+
this.removeTokens();
9797
this.isUserLoggedInSignal.set(false);
9898
}
9999

100100
isUserLoggedIn(): boolean {
101101
return this.isUserLoggedInSignal();
102102
}
103+
104+
private saveTokens(data: { accessToken: string; refreshToken?: string }) {
105+
this.storageService?.setItem(ACCESS_TOKEN_KEY, data.accessToken);
106+
if (data.refreshToken) {
107+
this.storageService?.setItem(REFRESH_TOKEN_KEY, data.refreshToken);
108+
}
109+
}
110+
111+
private removeTokens() {
112+
this.storageService?.removeItem(ACCESS_TOKEN_KEY);
113+
this.storageService?.removeItem(REFRESH_TOKEN_KEY);
114+
}
103115
}

src/app/features/authentication/services/user.service.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export class UserService {
2525
return this.httpClient
2626
.get<GetMeResponse>(getMeEndpoint, {
2727
context: new HttpContext().set(CACHING_ENABLED, cache),
28-
withCredentials: true,
2928
})
3029
.pipe(
3130
map((response: GetMeResponse) => {
@@ -37,24 +36,18 @@ export class UserService {
3736

3837
updateUser(updateUserRequest: UpdateUserRequest): Observable<User> {
3938
const updateUserEndpoint = `${this.apiUrl}/v1/user`;
40-
return this.httpClient
41-
.patch<UpdateUserResponse>(updateUserEndpoint, updateUserRequest, {
42-
withCredentials: true,
43-
})
44-
.pipe(
45-
map((response: UpdateUserResponse) => {
46-
const { data } = response;
47-
return data.user;
48-
}),
49-
);
39+
return this.httpClient.patch<UpdateUserResponse>(updateUserEndpoint, updateUserRequest).pipe(
40+
map((response: UpdateUserResponse) => {
41+
const { data } = response;
42+
return data.user;
43+
}),
44+
);
5045
}
5146

5247
catchPokemon(catchPokemonRequest: CatchPokemonRequest): Observable<User> {
5348
const catchPokemonEndpoint = `${this.apiUrl}/v1/user/pokemon/catch`;
5449
return this.httpClient
55-
.post<CatchPokemonResponse>(catchPokemonEndpoint, catchPokemonRequest, {
56-
withCredentials: true,
57-
})
50+
.post<CatchPokemonResponse>(catchPokemonEndpoint, catchPokemonRequest)
5851
.pipe(
5952
map((response: CatchPokemonResponse) => {
6053
const { data } = response;

src/app/features/authentication/types/login-response.type.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { User } from '~features/authentication/types/user.type';
22
import type { ApiResponse } from '~core/types/api-response.type';
33

44
export type LoginResponseData = {
5+
accessToken: string;
6+
refreshToken: string;
57
user: User;
68
};
79

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { ApiResponse } from '~core/types/api-response.type';
22

3-
export type RefreshTokenResponseData = object;
3+
export type RefreshTokenResponseData = {
4+
accessToken: string;
5+
};
46

57
export type RefreshTokenResponse = ApiResponse<RefreshTokenResponseData>;

src/app/features/authentication/types/register-response.type.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { User } from '~features/authentication/types/user.type';
22
import type { ApiResponse } from '~core/types/api-response.type';
33

44
export type RegisterResponseData = {
5+
accessToken: string;
6+
refreshToken: string;
57
user: User;
68
};
79

src/locale/messages.es.xlf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,10 @@
479479
<source>No real email validation. Field required. Format: [email protected]</source>
480480
<target state="new">Sin validación real de correo electrónico. Campo requerido. Formato: [email protected]</target>
481481
</trans-unit>
482+
<trans-unit id="1476832105912939227" datatype="html">
483+
<source>Session expired. Please log in.</source>
484+
<target state="new">La sesión ha caducado. Vuelve a hacer log in.</target>
485+
</trans-unit>
482486
</body>
483487
</file>
484488
</xliff>

src/locale/messages.xlf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@
355355
<trans-unit id="381575201384767307" datatype="html">
356356
<source>No real email validation. Field required. Format: [email protected]</source>
357357
</trans-unit>
358+
<trans-unit id="1476832105912939227" datatype="html">
359+
<source>Session expired. Please log in.</source>
360+
</trans-unit>
358361
</body>
359362
</file>
360363
</xliff>

0 commit comments

Comments
 (0)