Skip to content

Commit 8820bbc

Browse files
authored
Merge pull request #745 from IT-Academy-BCN/fix/#104-localstorage
fix(auth): replace localStorage with sessionStorage in interceptor (#104)
2 parents 97cc507 + e3c8d73 commit 8820bbc

File tree

13 files changed

+147
-34
lines changed

13 files changed

+147
-34
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
44
and this project adheres to
55
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
### [itachallenge-frontend-3.26.0-RELEASE] - 2026-02-16
8+
9+
### Changed
10+
- Authentication now uses `sessionStorage` instead of `localStorage` for storing auth tokens and usernames.
11+
- Sessions are now tab-specific and expire when the browser tab is closed.
12+
- Users must log in again after closing the tab (no auto-login).
713
### [ita-challenges-frontend-3.25.1-RELEASE] - 2026-02-16
814

915
### Changed

conf/.env.CI.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
MICROSERVICE_DEPLOY=ita-challenges-frontend
2-
MICROSERVICE_VERSION=3.25.1-RELEASE
2+
MICROSERVICE_VERSION=3.26.0-RELEASE

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ita-challenges-frontend",
3-
"version": "3.25.1-RELEASE",
3+
"version": "3.26.0-RELEASE",
44
"scripts": {
55
"ng": "ng",
66
"start": "ng serve --proxy-config proxy.conf.dev.json",

src/app/core/layout/header/desktop-nav/desktop-nav.component.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { NavService } from 'src/app/services/nav.service'
44
import { TranslateModule } from '@ngx-translate/core'
55
import { RouterModule, ActivatedRoute } from '@angular/router'
66
import { AuthService } from 'src/app/services/auth.service';
7-
import { of } from 'rxjs';
7+
import { of, throwError } from 'rxjs'
88
import { By } from '@angular/platform-browser'
99
import { Component, Input } from '@angular/core';
1010

@@ -36,6 +36,7 @@ class MockAuthService {
3636
getUserPhoto = jest.fn(() => of('https://mock-photo-url.com/avatar.png'))
3737

3838
checkAndHandleExpiredToken = jest.fn()
39+
switchRole = jest.fn((role: string) => of({ token: 'new-token-123' }))
3940
}
4041

4142
const mockActivatedRoute = {
@@ -162,4 +163,23 @@ describe('DesktopNavComponent', () => {
162163
component.openRegisterUsersModal();
163164
expect(openRegisterUsersModalSpy).toHaveBeenCalled();
164165
});
166+
167+
it('✅ Should switch role successfully', () => {
168+
sessionStorage.clear()
169+
component.onSwitchRole('USER')
170+
expect(authService.switchRole).toHaveBeenCalledWith('USER')
171+
expect(sessionStorage.getItem('authToken')).toBe('new-token-123')
172+
expect(authService.updateUserRoleAndUserNameFromToken).toHaveBeenCalled()
173+
})
174+
175+
it('❌ Should handle switch role error', () => {
176+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
177+
jest.spyOn(authService, 'switchRole').mockReturnValue(
178+
throwError(() => new Error('Failed'))
179+
)
180+
component.onSwitchRole('ADMIN')
181+
182+
expect(consoleSpy).toHaveBeenCalled()
183+
consoleSpy.mockRestore()
184+
})
165185
})

src/app/core/layout/header/desktop-nav/desktop-nav.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class DesktopNavComponent implements OnInit, OnDestroy{
8989
onSwitchRole (newRole: 'ADMIN' | 'USER'): void {
9090
this._authService.switchRole(newRole).subscribe({
9191
next: (data) => {
92-
localStorage.setItem('authToken', data.token)
92+
sessionStorage.setItem('authToken', data.token)
9393
this._authService.updateUserRoleAndUserNameFromToken()
9494
},
9595
error: (error) => {

src/app/core/layout/header/mobile-nav/mobile-nav.component.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class MockAuthService {
2626
getUserPhoto = jest.fn(() => of('https://mock-photo-url.com/avatar.png'))
2727

2828
checkAndHandleExpiredToken = jest.fn()
29+
switchRole = jest.fn((role: string) => of({ token: 'new-token-123' }))
2930
}
3031

3132
const mockActivatedRoute = {

src/app/core/layout/header/mobile-nav/mobile-nav.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class MobileNavComponent implements OnInit, OnDestroy{
8888
onSwitchRole (newRole: 'ADMIN' | 'USER'): void {
8989
this._authService.switchRole(newRole).subscribe({
9090
next: (data) => {
91-
localStorage.setItem('authToken', data.token)
91+
sessionStorage.setItem('authToken', data.token)
9292
this._authService.updateUserRoleAndUserNameFromToken()
9393
},
9494
error: (error) => {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { TestBed } from '@angular/core/testing'
2+
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'
3+
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'
4+
import { JwtInterceptor } from './jwt-interceptor'
5+
import { CookieService } from 'ngx-cookie-service'
6+
import { environment } from 'src/environments/environment'
7+
8+
describe('JwtInterceptor', () => {
9+
let httpMock: HttpTestingController
10+
let httpClient: HttpClient
11+
const apiUrl = `${environment.BACKEND_ITA_CHALLENGE_BASE_URL}/test`
12+
13+
beforeEach(() => {
14+
TestBed.configureTestingModule({
15+
imports: [HttpClientTestingModule],
16+
providers: [
17+
{ provide: CookieService, useValue: {} },
18+
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }
19+
]
20+
})
21+
httpMock = TestBed.inject(HttpTestingController)
22+
httpClient = TestBed.inject(HttpClient)
23+
sessionStorage.clear()
24+
})
25+
26+
afterEach(() => {
27+
httpMock.verify()
28+
})
29+
30+
it('should add auth header with token', () => {
31+
sessionStorage.setItem('authToken', 'token')
32+
httpClient.get(apiUrl).subscribe()
33+
expect(httpMock.expectOne(apiUrl).request.headers.get('Authorization')).toBe('Bearer token')
34+
})
35+
36+
it('should NOT add header without token', () => {
37+
httpClient.get(apiUrl).subscribe()
38+
expect(httpMock.expectOne(apiUrl).request.headers.get('Authorization')).toBeNull()
39+
})
40+
41+
it('should NOT add header for external URLs', () => {
42+
sessionStorage.setItem('authToken', 'token')
43+
httpClient.get('https://ext.com').subscribe()
44+
expect(httpMock.expectOne('https://ext.com').request.headers.get('Authorization')).toBeNull()
45+
})
46+
})

src/app/interceptors/jwt-interceptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class JwtInterceptor implements HttpInterceptor {
1212
private readonly cookieService = inject(CookieService)
1313

1414
intercept (request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
15-
const token: string = this.cookieService.get('authToken')
15+
const token: string = sessionStorage.getItem('authToken') ?? ''
1616
const isApiUrl = request.url.startsWith(environment.BACKEND_ITA_CHALLENGE_BASE_URL)
1717
if (isApiUrl && token !== '') {
1818
// Agregar el token al encabezado Authorization

src/app/modules/mentor/mentor-login/mentor-login.component.spec.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,34 +150,58 @@ describe('MentorLoginComponent', () => {
150150
environment.BACKEND_ITA_CHALLENGE_BASE_URL + environment.BACKEND_GITHUB_VALIDATE_ENDPOINT,
151151
{ code: 'testCode' }
152152
)
153-
expect(localStorage.getItem('username')).toBe('testUser')
154-
expect(localStorage.getItem('authToken')).toBe('123456')
153+
expect(sessionStorage.getItem('username')).toBe('testUser')
154+
expect(sessionStorage.getItem('authToken')).toBe('123456')
155155
expect(authServiceMock.updateUserRoleAndUserNameFromToken).toHaveBeenCalled()
156156
expect(routerSpy).toHaveBeenCalledWith([], { queryParams: { code: null }, queryParamsHandling: 'merge' })
157157
expect(modalSpy).toHaveBeenCalled()
158158
expect(loginSuccessSpy).toHaveBeenCalledWith(true)
159159
})
160160

161+
it('❌ Should handle invalid GitHub response and clean storage', () => {
162+
sessionStorage.setItem('username', 'testUser')
163+
sessionStorage.setItem('authToken', '123456')
164+
165+
const mockResponse = { isValid: false, username: '', token: '' }
166+
jest.spyOn(component.http, 'post').mockReturnValue(of(mockResponse))
167+
const errorSpy = jest.spyOn(component, 'showError')
168+
169+
component.authenticateWithGitHub('testCode')
170+
171+
expect(errorSpy).toHaveBeenCalledWith('unauthorized')
172+
expect(sessionStorage.getItem('username')).toBeNull()
173+
expect(sessionStorage.getItem('authToken')).toBeNull()
174+
})
175+
161176
it.each<ErrorHandlingTestCase>([
162177
{ statusCode: 401, expectedError: 'unauthorized' },
163178
{ statusCode: 403, expectedError: 'unauthorized' },
164-
{ statusCode: 500, expectedError: 'server_error' }
179+
{ statusCode: 500, expectedError: 'server_error' },
180+
{ statusCode: 404, expectedError: 'unauthorized' }
165181
])('❌ Should handle error %i and show error message', ({ statusCode, expectedError }, done) => {
166-
localStorage.setItem('username', 'testUser')
167-
localStorage.setItem('authToken', '123456')
182+
sessionStorage.setItem('username', 'testUser')
183+
sessionStorage.setItem('authToken', '123456')
168184

169185
const httpSpy = jest.spyOn(component.http, 'post').mockReturnValue(
170186
throwError(() => ({ status: statusCode }))
171187
)
172188

173189
const errorSpy = jest.spyOn(component, 'showError')
174-
190+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
175191
component.authenticateWithGitHub('testCode')
176192

177193
setTimeout(() => {
178194
expect(httpSpy).toHaveBeenCalled()
179195
expect(errorSpy).toHaveBeenCalledWith(expectedError)
180196
expect(component.isLoading).toBe(false)
197+
if (statusCode === 403) {
198+
expect(sessionStorage.getItem('username')).toBeNull()
199+
expect(sessionStorage.getItem('authToken')).toBeNull()
200+
}
201+
if (statusCode === 404) {
202+
expect(consoleSpy).toHaveBeenCalled()
203+
}
204+
consoleSpy.mockRestore()
181205

182206
done()
183207
}, 100)
@@ -193,4 +217,13 @@ describe('MentorLoginComponent', () => {
193217
expect(component.isErrorVisible).toBe(false)
194218
expect(component.isShowTermsError).toBe(false)
195219
})
220+
221+
it('✅ Should redirect to GitHub signup', () => {
222+
delete (window as any).location
223+
window.location = { href: '' } as any
224+
225+
component.redirectToRegister()
226+
227+
expect(window.location.href).toBe('https://github.com/signup')
228+
})
196229
})

0 commit comments

Comments
 (0)