-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathinterceptor.ts
More file actions
135 lines (113 loc) ยท 4.37 KB
/
Copy pathinterceptor.ts
File metadata and controls
135 lines (113 loc) ยท 4.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { cache } from 'react';
import { getSession } from 'next-auth/react';
import {
setRenderRequestInterceptor,
setRenderResponseInterceptor,
setRequestInterceptor,
setResponseInterceptor,
} from '@gitanimals/api';
import { CustomException } from '@gitanimals/exception';
import type { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { getServerAuth } from '@/auth';
import type { ApiErrorScheme } from '@/exceptions/type';
import { triggerSessionExpired } from '@/utils/sessionExpired';
// Server path: request-scoped memoization via React cache(). Deduped within a
// single render (one JWT decode instead of one per outbound request), and never
// shared across concurrent requests from different users.
const getServerAccessToken = cache(async (): Promise<string | null> => {
const session = await getServerAuth();
return session?.user?.accessToken ?? null;
});
// Client-only module-global cache. The browser global is per-user, so caching
// here is safe. It MUST NOT be shared on the server, where a single module
// instance is shared across concurrent requests from different users โ the
// server path above deliberately uses request-scoped cache() instead.
interface CachedSession {
accessToken: string;
expiresAt: number;
}
let cachedSession: CachedSession | null = null;
let sessionPromise: Promise<string | null> | null = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
export const clearSessionCache = () => {
cachedSession = null;
sessionPromise = null;
};
const getAccessToken = async (): Promise<string | null> => {
if (typeof window === 'undefined') {
return getServerAccessToken();
}
if (cachedSession && Date.now() < cachedSession.expiresAt) {
return cachedSession.accessToken;
}
if (sessionPromise) {
return sessionPromise;
}
sessionPromise = (async () => {
try {
const session = await getSession();
if (session?.user?.accessToken) {
cachedSession = {
accessToken: session.user.accessToken,
expiresAt: Date.now() + CACHE_DURATION,
};
return cachedSession.accessToken;
}
return null;
} finally {
sessionPromise = null;
}
})();
return sessionPromise;
};
export const interceptorRequestFulfilled = async (config: InternalAxiosRequestConfig) => {
const accessToken = await getAccessToken();
if (!config.headers) return config;
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
};
// Response interceptor
export const interceptorResponseFulfilled = (res: AxiosResponse) => {
return res;
};
// Response interceptor
export const interceptorResponseRejected = async (error: AxiosError<ApiErrorScheme>) => {
if (error?.response?.status === 401) {
// ์บ์ ๋ฌดํจํ๋ ํ๊ฒฝ๊ณผ ๋ฌด๊ดํ๊ฒ ๋จผ์ ์ํ โ ๋ง๋ฃ๋ ํ ํฐ ์ฌ์ฌ์ฉ ๋ฐฉ์ง.
clearSessionCache();
// ๋ณต๊ตฌ UX(์ธ์
๋ง๋ฃ ๋ค์ด์ผ๋ก๊ทธ)๋ ํด๋ผ์ด์ธํธ์์๋ง ๋์ด๋ค.
if (typeof window !== 'undefined') {
triggerSessionExpired(window.location.pathname + window.location.search);
}
throw new CustomException('TOKEN_EXPIRED', 'token expired');
}
// TODO: 403 ์ฒ๋ฆฌ
return Promise.reject(error);
};
let registered = false;
/**
* Attaches the request/response interceptors to the shared @gitanimals/api
* instances exactly once. Idempotent: safe to call from multiple module scopes
* (server root layout, client provider) without stacking handlers.
*/
export const registerInterceptors = () => {
if (registered) return;
registered = true;
setRequestInterceptor(interceptorRequestFulfilled);
setResponseInterceptor(interceptorResponseFulfilled, interceptorResponseRejected);
setRenderRequestInterceptor(interceptorRequestFulfilled);
setRenderResponseInterceptor(interceptorResponseFulfilled, interceptorResponseRejected);
};
export const setInterceptors = (instance: AxiosInstance) => {
instance.interceptors.request.use(interceptorRequestFulfilled);
if (typeof window !== 'undefined') {
// ํด๋ผ์ด์ธํธ์์๋ง ๋ฆฌ๋ค์ด๋ ํธ ๊ฐ์ง ์ธํฐ์
ํฐ ๋ฑ๋ก
instance.interceptors.response.use(interceptorResponseFulfilled, interceptorResponseRejected);
} else {
// ์๋ฒ์์๋ ๊ธฐ๋ณธ ์๋ต๋ง ๋ฐํ (ํญ์ res ๋ฐํ)
instance.interceptors.response.use((res) => res, interceptorResponseRejected);
}
return instance;
};