Skip to content

Commit 17c0e12

Browse files
committed
add jwt headers to local storage
1 parent d6bb530 commit 17c0e12

15 files changed

+290
-53
lines changed

apps/studyum/src/app/app.module.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import {NgModule} from "@angular/core"
22
import {BrowserModule} from "@angular/platform-browser"
3-
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from "@angular/common/http"
3+
import {
4+
HTTP_INTERCEPTORS,
5+
HttpClient,
6+
HttpClientModule,
7+
provideHttpClient,
8+
withInterceptors
9+
} from "@angular/common/http"
410
import {RouterModule, Routes, TitleStrategy} from "@angular/router"
511
import {AppComponent} from "./app.component"
612
import {BrowserAnimationsModule} from "@angular/platform-browser/animations"
@@ -17,6 +23,7 @@ import {ToastComponent} from "./shared/components/toast/toast.component"
1723
import {ThemeSelectorComponent} from "../../../../libs/common/theme-selector/theme-selector.component"
1824
import {MatIconModule} from "@angular/material/icon"
1925
import {HomeModule} from "./modules/home/home.module"
26+
import {jwtInterceptor} from "../../../../libs/common/jwt-http/src/lib/jwt.interceptor"
2027

2128
const appRoutes: Routes = [
2229
{title: "Studyum", path: "", component: HomeComponent},
@@ -69,7 +76,8 @@ export function HttpLoaderFactory(http: HttpClient) {
6976
{provide: TitleStrategy, useClass: HeaderTitleStrategy},
7077
{provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true},
7178
{provide: HTTP_INTERCEPTORS, useClass: HttpAuthInterceptor, multi: true},
72-
{provide: HTTP_INTERCEPTORS, useClass: MomentJsInterceptor, multi: true}
79+
{provide: HTTP_INTERCEPTORS, useClass: MomentJsInterceptor, multi: true},
80+
provideHttpClient(withInterceptors([jwtInterceptor]))
7381
],
7482
bootstrap: [AppComponent]
7583
})

apps/studyum/src/app/shared/interseptors/http-error.interceptor.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import {Injectable} from "@angular/core"
2-
import {
3-
HttpErrorResponse,
4-
HttpEvent,
5-
HttpHandler,
6-
HttpInterceptor,
7-
HttpRequest,
8-
} from "@angular/common/http"
2+
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"
93
import {catchError, Observable, retry, throwError, timer} from "rxjs"
104
import {ToastService} from "../services/ui/toast.service"
115

126
@Injectable()
137
export class HttpErrorInterceptor implements HttpInterceptor {
14-
constructor(private toastService: ToastService) {}
8+
constructor(private toastService: ToastService) {
9+
}
1510

1611
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
1712
return next.handle(request).pipe(
@@ -35,7 +30,7 @@ export class HttpErrorInterceptor implements HttpInterceptor {
3530
default:
3631
return throwError(() => error)
3732
}
38-
},
33+
}
3934
})
4035
)
4136
}

libs/common/jwt-http/.eslintrc.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"extends": ["../../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts"],
7+
"rules": {
8+
"@angular-eslint/directive-selector": [
9+
"error",
10+
{
11+
"type": "attribute",
12+
"prefix": "app",
13+
"style": "camelCase"
14+
}
15+
],
16+
"@angular-eslint/component-selector": [
17+
"error",
18+
{
19+
"type": "element",
20+
"prefix": "app",
21+
"style": "kebab-case"
22+
}
23+
]
24+
},
25+
"extends": [
26+
"plugin:@nrwl/nx/angular",
27+
"plugin:@angular-eslint/template/process-inline-templates"
28+
]
29+
},
30+
{
31+
"files": ["*.html"],
32+
"extends": ["plugin:@nrwl/nx/angular-template"],
33+
"rules": {}
34+
}
35+
]
36+
}

libs/common/jwt-http/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# common-jwt-http
2+
3+
This library was generated with [Nx](https://nx.dev).

libs/common/jwt-http/project.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "common-jwt-http",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"projectType": "library",
5+
"sourceRoot": "libs/common/jwt-http/src",
6+
"prefix": "app",
7+
"targets": {
8+
"lint": {
9+
"executor": "@nrwl/linter:eslint",
10+
"outputs": ["{options.outputFile}"],
11+
"options": {
12+
"lintFilePatterns": ["libs/common/jwt-http/**/*.ts", "libs/common/jwt-http/**/*.html"]
13+
}
14+
}
15+
},
16+
"tags": []
17+
}

libs/common/jwt-http/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./lib/common-jwt-http.module"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {NgModule} from "@angular/core"
2+
import {CommonModule} from "@angular/common"
3+
4+
@NgModule({
5+
imports: [CommonModule],
6+
})
7+
export class CommonJwtHttpModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest, HttpResponse} from "@angular/common/http"
2+
import {inject} from "@angular/core"
3+
import {JwtService} from "./jwt.service"
4+
import {catchError, filter, map, mergeMap, Observable, take, tap, throwError} from "rxjs"
5+
6+
7+
export const jwtInterceptor: HttpInterceptorFn = (req: HttpRequest<any>, next: HttpHandlerFn): Observable<any> => {
8+
if (!req.url.startsWith("/api") || req.url === JwtService.UPDATE_URL) return next(req)
9+
const service = inject(JwtService)
10+
11+
const setTokensPipe = tap((e: HttpResponse<any>) => {
12+
if (!e) return
13+
if (e.headers.has("SetAccessToken")) {
14+
service.access = e.headers.get("SetAccessToken") ?? ""
15+
}
16+
17+
if (e.headers.has("SetRefreshToken")) {
18+
service.refresh = e.headers.get("SetRefreshToken") ?? ""
19+
}
20+
})
21+
22+
const nextWithToken = () => next(addToken())
23+
.pipe(filter(e => e instanceof HttpResponse))
24+
.pipe(map(e => e as HttpResponse<any>))
25+
.pipe(setTokensPipe)
26+
.pipe(catchError(onError))
27+
28+
const addToken = (): HttpRequest<any> => {
29+
return req.clone({setHeaders: {Authorization: service.access}})
30+
}
31+
32+
const updateAndExecute = (): Observable<any> =>
33+
service.update().pipe(setTokensPipe, mergeMap(nextWithToken))
34+
35+
const waitUpdateAndExecute = (): Observable<any> => service.updatingChanges
36+
.pipe(filter(v => !v), take(1))
37+
.pipe(mergeMap(nextWithToken))
38+
39+
const onError = (err: any): Observable<any> => {
40+
if (!(err instanceof HttpErrorResponse)) return throwError(() => err)
41+
const httpErr: HttpErrorResponse = err as HttpErrorResponse
42+
if (httpErr.status !== 401) return throwError(() => err)
43+
44+
if (httpErr.error !== "access token has expired") {
45+
service.removeTokens()
46+
return throwError(() => err)
47+
}
48+
49+
return service.isUpdating ? waitUpdateAndExecute() : updateAndExecute()
50+
}
51+
52+
if (service.isUpdating) return waitUpdateAndExecute()
53+
if (service.isMustUpdate()) return updateAndExecute()
54+
55+
if (service.isNeedUpdate()) {
56+
service.update().pipe(take(1), setTokensPipe).subscribe()
57+
return nextWithToken()
58+
}
59+
60+
return nextWithToken()
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as moment from "moment"
2+
3+
export interface Data {
4+
exp: moment.Moment,
5+
claims: {
6+
ID: number
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {Injectable} from "@angular/core"
2+
import {BehaviorSubject, finalize, Observable} from "rxjs"
3+
import jwtDecode from "jwt-decode"
4+
import {Data} from "./jwt.models"
5+
import * as moment from "moment"
6+
import {HttpClient, HttpResponse} from "@angular/common/http"
7+
8+
@Injectable({
9+
providedIn: "root"
10+
})
11+
export class JwtService {
12+
static readonly UPDATE_URL = "/api/user/updateToken"
13+
static readonly REFRESH_TOKEN_NAME = "refresh_token"
14+
static readonly ACCESS_TOKEN_NAME = "access_token"
15+
static readonly SECOND_TO_NEED_UPDATE = 30
16+
17+
get access() {
18+
return localStorage.getItem(JwtService.ACCESS_TOKEN_NAME) ?? ""
19+
}
20+
21+
set access(value: string) {
22+
localStorage.setItem(JwtService.ACCESS_TOKEN_NAME, value)
23+
this._data = this.decode()
24+
}
25+
26+
get refresh() {
27+
return localStorage.getItem(JwtService.REFRESH_TOKEN_NAME) ?? ""
28+
}
29+
30+
set refresh(value: string) {
31+
localStorage.setItem(JwtService.REFRESH_TOKEN_NAME, value)
32+
}
33+
34+
get tokens() {
35+
return [this.access, this.refresh]
36+
}
37+
38+
get updatingChanges() {
39+
return this._updating$.asObservable()
40+
}
41+
42+
get isUpdating() {
43+
return this._updating$.value
44+
}
45+
46+
get data(): Data | null {
47+
return this._data
48+
}
49+
50+
constructor(private http: HttpClient) {
51+
this._data = this.decode()
52+
}
53+
54+
removeTokens(): void {
55+
localStorage.removeItem(JwtService.REFRESH_TOKEN_NAME)
56+
localStorage.removeItem(JwtService.ACCESS_TOKEN_NAME)
57+
}
58+
59+
isNeedUpdate(): boolean {
60+
//if null -> return false
61+
//null will be if res < JwtService.SECOND_TO_NEED_UPDATE,
62+
//so JwtService.SECOND_TO_NEED_UPDATE < JwtService.SECOND_TO_NEED_UPDATE = false
63+
return (this.expireDifferenceBetweenNow() ?? JwtService.SECOND_TO_NEED_UPDATE) < JwtService.SECOND_TO_NEED_UPDATE
64+
}
65+
66+
isMustUpdate(): boolean {
67+
//if null -> return false
68+
//null will be if res <= 0,
69+
//so 1 <= 0 = false
70+
return (this.expireDifferenceBetweenNow() ?? 1) <= 0
71+
}
72+
73+
expireDifferenceBetweenNow(): number | null {
74+
return this.data?.exp.diff(moment.now(), "second") ?? null
75+
}
76+
77+
update(): Observable<HttpResponse<void>> {
78+
this._updating$.next(true)
79+
return this.http
80+
.put<void>(JwtService.UPDATE_URL, `"${this.refresh}"`, {observe: "response"})
81+
.pipe(finalize(() => this._updating$.next(false)))
82+
}
83+
84+
private _updating$ = new BehaviorSubject<boolean>(false)
85+
private _data: Data | null
86+
87+
private decode(): Data | null {
88+
try {
89+
const data = jwtDecode<Data>(this.access)
90+
data.exp = moment(data.exp as any * 1000)
91+
return data
92+
} catch (error) {
93+
return null
94+
}
95+
}
96+
}

libs/common/jwt-http/tsconfig.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"useDefineForClassFields": false
5+
},
6+
"files": [],
7+
"include": [],
8+
"references": [
9+
{
10+
"path": "./tsconfig.lib.json"
11+
}
12+
],
13+
"extends": "../../../tsconfig.base.json"
14+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../../dist/out-tsc",
5+
"declaration": true,
6+
"declarationMap": true,
7+
"inlineSources": true,
8+
"types": []
9+
},
10+
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"],
11+
"include": ["src/**/*.ts"]
12+
}

package-lock.json

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@nrwl/cli": "15.8.9",
3131
"@popperjs/core": "^2.11.6",
3232
"file-saver": "^2.0.5",
33+
"jwt-decode": "^3.1.2",
3334
"moment": "^2.29.4",
3435
"ngx-popperjs": "^15.0.1",
3536
"postcss-preset-env": "^8.0.1",
@@ -60,4 +61,3 @@
6061
"typescript": "4.9.5"
6162
}
6263
}
63-

0 commit comments

Comments
 (0)