@wheeparam/library 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.cjs +239 -102
- package/dist/client/index.d.cts +95 -82
- package/dist/client/index.d.ts +95 -82
- package/dist/client/index.js +242 -103
- package/package.json +1 -1
package/dist/client/index.cjs
CHANGED
|
@@ -34,169 +34,306 @@ __export(client_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(client_exports);
|
|
36
36
|
|
|
37
|
-
// src/client/
|
|
37
|
+
// src/client/createAxiosClient.ts
|
|
38
38
|
var import_axios = __toESM(require("axios"), 1);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
|
|
40
|
+
// src/client/error.ts
|
|
41
|
+
var defaultShouldRefresh = (error) => {
|
|
42
|
+
return error.response?.status === 401;
|
|
43
|
+
};
|
|
44
|
+
function defaultParseRefreshResponse(data) {
|
|
45
|
+
const source = typeof data === "object" && data !== null ? data : {};
|
|
46
|
+
const accessToken = typeof source.result?.accessToken === "string" && source.result.accessToken || typeof source.accessToken === "string" && source.accessToken || typeof source.result?.token === "string" && source.result.token || typeof source.token === "string" && source.token || "";
|
|
47
|
+
const refreshToken = typeof source.result?.refreshToken === "string" && source.result.refreshToken || typeof source.refreshToken === "string" && source.refreshToken || void 0;
|
|
48
|
+
if (!accessToken) {
|
|
49
|
+
throw new Error("Refresh response does not contain accessToken");
|
|
50
|
+
}
|
|
42
51
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
accessToken,
|
|
53
|
+
refreshToken
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function getDefaultErrorMessage(error) {
|
|
57
|
+
const axiosError = error;
|
|
58
|
+
const status = axiosError.response?.status;
|
|
59
|
+
const serverMessage = axiosError.response?.data?.error;
|
|
60
|
+
if (serverMessage && serverMessage.trim().length > 0) {
|
|
61
|
+
return serverMessage;
|
|
62
|
+
}
|
|
63
|
+
switch (status) {
|
|
64
|
+
case 0:
|
|
65
|
+
return "REST API \uC11C\uBC84\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\uC11C\uBC84 \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.";
|
|
66
|
+
case 400:
|
|
67
|
+
return "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.";
|
|
68
|
+
case 401:
|
|
69
|
+
return "\uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
|
|
70
|
+
case 403:
|
|
71
|
+
return "\uC811\uADFC \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
72
|
+
case 404:
|
|
73
|
+
return "[404] REST API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.";
|
|
74
|
+
case 500:
|
|
75
|
+
return "\uC11C\uBC84\uC5D0\uC11C \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD558\uC600\uC2B5\uB2C8\uB2E4.";
|
|
76
|
+
default:
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/client/refreshManager.ts
|
|
82
|
+
var RefreshManager = class {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.isRefreshing = false;
|
|
85
|
+
this.subscribers = [];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 현재 refresh 진행 중인지 반환합니다.
|
|
89
|
+
*/
|
|
90
|
+
get refreshing() {
|
|
91
|
+
return this.isRefreshing;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* refresh 시작 상태로 전환합니다.
|
|
95
|
+
*/
|
|
96
|
+
begin() {
|
|
97
|
+
this.isRefreshing = true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* refresh 종료 상태로 전환합니다.
|
|
101
|
+
*/
|
|
102
|
+
end() {
|
|
103
|
+
this.isRefreshing = false;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* refresh 완료를 기다리는 subscriber를 등록합니다.
|
|
107
|
+
*
|
|
108
|
+
* @param callback refresh 완료 후 호출될 함수
|
|
109
|
+
*/
|
|
110
|
+
subscribe(callback) {
|
|
111
|
+
this.subscribers.push(callback);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 대기 중인 subscriber들에게 결과를 알리고 큐를 초기화합니다.
|
|
115
|
+
*
|
|
116
|
+
* @param token 새 access token
|
|
117
|
+
*/
|
|
118
|
+
notify(token) {
|
|
119
|
+
this.subscribers.forEach((callback) => callback(token));
|
|
120
|
+
this.subscribers = [];
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
async function requestRefresh(params) {
|
|
124
|
+
const {
|
|
125
|
+
refreshUrl,
|
|
126
|
+
withCredentials,
|
|
127
|
+
refreshStrategy,
|
|
128
|
+
refreshToken,
|
|
129
|
+
parseRefreshResponse
|
|
130
|
+
} = params;
|
|
131
|
+
const requestInit = {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json"
|
|
48
135
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
136
|
+
credentials: withCredentials ? "include" : "same-origin"
|
|
137
|
+
};
|
|
138
|
+
if (refreshStrategy === "body") {
|
|
139
|
+
if (!refreshToken) {
|
|
140
|
+
throw new Error("Refresh token not found");
|
|
141
|
+
}
|
|
142
|
+
requestInit.body = JSON.stringify({ refreshToken });
|
|
143
|
+
}
|
|
144
|
+
const response = await fetch(refreshUrl, requestInit);
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error("Token refresh failed");
|
|
147
|
+
}
|
|
148
|
+
const json = await response.json();
|
|
149
|
+
return parseRefreshResponse(json);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/client/tokenStorage.ts
|
|
153
|
+
function createAccessTokenAccess(mode, key = "accessToken") {
|
|
154
|
+
let memoryToken = null;
|
|
155
|
+
if (mode === "localStorage") {
|
|
156
|
+
return {
|
|
157
|
+
get: () => window.localStorage.getItem(key),
|
|
158
|
+
set: (token) => {
|
|
159
|
+
if (!token) {
|
|
160
|
+
window.localStorage.removeItem(key);
|
|
161
|
+
} else {
|
|
162
|
+
window.localStorage.setItem(key, token);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
clear: () => {
|
|
166
|
+
window.localStorage.removeItem(key);
|
|
52
167
|
}
|
|
53
|
-
|
|
54
|
-
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (mode === "memory") {
|
|
171
|
+
return {
|
|
172
|
+
get: () => memoryToken,
|
|
173
|
+
set: (token) => {
|
|
174
|
+
memoryToken = token;
|
|
175
|
+
},
|
|
176
|
+
clear: () => {
|
|
177
|
+
memoryToken = null;
|
|
55
178
|
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
get: () => null,
|
|
183
|
+
set: () => {
|
|
56
184
|
},
|
|
57
|
-
clear() {
|
|
58
|
-
window.localStorage.removeItem(accessTokenKey);
|
|
59
|
-
window.localStorage.removeItem(refreshTokenKey);
|
|
185
|
+
clear: () => {
|
|
60
186
|
}
|
|
61
187
|
};
|
|
62
188
|
}
|
|
63
|
-
function
|
|
64
|
-
const refreshTokenKey = keys?.refreshTokenKey ?? "refreshToken";
|
|
189
|
+
function createRefreshTokenAccess(key = "refreshToken") {
|
|
65
190
|
return {
|
|
66
|
-
get()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
},
|
|
73
|
-
set(data) {
|
|
74
|
-
if (data.refreshToken !== void 0) {
|
|
75
|
-
window.localStorage.setItem(refreshTokenKey, data.refreshToken ?? "");
|
|
191
|
+
get: () => window.localStorage.getItem(key),
|
|
192
|
+
set: (token) => {
|
|
193
|
+
if (!token) {
|
|
194
|
+
window.localStorage.removeItem(key);
|
|
195
|
+
} else {
|
|
196
|
+
window.localStorage.setItem(key, token);
|
|
76
197
|
}
|
|
77
198
|
},
|
|
78
|
-
clear() {
|
|
79
|
-
window.localStorage.removeItem(
|
|
199
|
+
clear: () => {
|
|
200
|
+
window.localStorage.removeItem(key);
|
|
80
201
|
}
|
|
81
202
|
};
|
|
82
203
|
}
|
|
204
|
+
|
|
205
|
+
// src/client/createAxiosClient.ts
|
|
206
|
+
function setAuthorizationHeader(config, accessToken) {
|
|
207
|
+
const headers = new import_axios.AxiosHeaders();
|
|
208
|
+
if (config.headers) {
|
|
209
|
+
const current = config.headers instanceof import_axios.AxiosHeaders ? config.headers.toJSON() : config.headers;
|
|
210
|
+
for (const [key, value] of Object.entries(current)) {
|
|
211
|
+
if (value !== void 0) {
|
|
212
|
+
headers.set(key, String(value));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
217
|
+
config.headers = headers;
|
|
218
|
+
return config;
|
|
219
|
+
}
|
|
83
220
|
function createAxiosClient(options) {
|
|
84
221
|
const {
|
|
85
222
|
baseURL,
|
|
86
223
|
timeout = 1e4,
|
|
87
224
|
withCredentials = true,
|
|
88
225
|
refreshUrl,
|
|
89
|
-
|
|
226
|
+
accessTokenStorage = "localStorage",
|
|
227
|
+
refreshStrategy = "body",
|
|
90
228
|
storageKeys,
|
|
91
229
|
onAuthStateChange,
|
|
92
230
|
onError,
|
|
93
|
-
getErrorMessage
|
|
231
|
+
getErrorMessage,
|
|
232
|
+
shouldRefresh = defaultShouldRefresh,
|
|
233
|
+
parseRefreshResponse = defaultParseRefreshResponse
|
|
94
234
|
} = options;
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
235
|
+
const accessTokenKey = storageKeys?.accessTokenKey ?? "accessToken";
|
|
236
|
+
const refreshTokenKey = storageKeys?.refreshTokenKey ?? "refreshToken";
|
|
237
|
+
const instance = import_axios.default.create({
|
|
238
|
+
baseURL,
|
|
239
|
+
timeout,
|
|
240
|
+
withCredentials
|
|
241
|
+
});
|
|
242
|
+
const accessTokenAccess = createAccessTokenAccess(accessTokenStorage, accessTokenKey);
|
|
243
|
+
const refreshTokenAccess = createRefreshTokenAccess(refreshTokenKey);
|
|
244
|
+
const refreshManager = new RefreshManager();
|
|
103
245
|
const clearAuth = () => {
|
|
104
|
-
|
|
246
|
+
accessTokenAccess.clear();
|
|
247
|
+
if (refreshStrategy === "body") {
|
|
248
|
+
refreshTokenAccess.clear();
|
|
249
|
+
}
|
|
105
250
|
onAuthStateChange?.("unauthenticated");
|
|
106
251
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
252
|
+
const getToken = () => {
|
|
253
|
+
return {
|
|
254
|
+
accessToken: accessTokenAccess.get(),
|
|
255
|
+
refreshToken: refreshStrategy === "body" ? refreshTokenAccess.get() : null
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
const setToken = (data) => {
|
|
259
|
+
if (data.accessToken !== void 0) {
|
|
260
|
+
accessTokenAccess.set(data.accessToken ?? null);
|
|
261
|
+
}
|
|
262
|
+
if (refreshStrategy === "body" && data.refreshToken !== void 0) {
|
|
263
|
+
refreshTokenAccess.set(data.refreshToken ?? null);
|
|
116
264
|
}
|
|
117
|
-
return await res.json();
|
|
118
265
|
};
|
|
119
266
|
instance.interceptors.request.use(
|
|
120
267
|
(config) => {
|
|
121
|
-
const
|
|
268
|
+
const accessToken = accessTokenAccess.get();
|
|
122
269
|
if (accessToken) {
|
|
123
|
-
config
|
|
124
|
-
config.headers.Authorization = `Bearer ${accessToken}`;
|
|
270
|
+
setAuthorizationHeader(config, accessToken);
|
|
125
271
|
}
|
|
126
272
|
return config;
|
|
127
273
|
},
|
|
128
|
-
(
|
|
274
|
+
(error) => Promise.reject(error)
|
|
129
275
|
);
|
|
130
276
|
instance.interceptors.response.use(
|
|
131
|
-
(
|
|
277
|
+
(response) => response,
|
|
132
278
|
async (error) => {
|
|
133
|
-
const response = error.response;
|
|
134
|
-
const status = response?.status;
|
|
135
279
|
const originalRequest = error.config ?? {};
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
case 0:
|
|
141
|
-
message = "REST API \uC11C\uBC84\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4\n\uC11C\uBC84 \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694";
|
|
142
|
-
break;
|
|
143
|
-
case 400:
|
|
144
|
-
message = "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.";
|
|
145
|
-
break;
|
|
146
|
-
case 404:
|
|
147
|
-
message = "[404] REST API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4";
|
|
148
|
-
break;
|
|
149
|
-
case 500:
|
|
150
|
-
message = "\uC11C\uBC84\uC5D0\uC11C \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD558\uC600\uC2B5\uB2C8\uB2E4.";
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
280
|
+
if (!shouldRefresh(error)) {
|
|
281
|
+
const message = getErrorMessage?.(error) ?? getDefaultErrorMessage(error);
|
|
282
|
+
if (message) {
|
|
283
|
+
onError?.(message, error);
|
|
153
284
|
}
|
|
154
|
-
if (message) onError?.(message, error);
|
|
155
285
|
return Promise.reject(error);
|
|
156
286
|
}
|
|
157
|
-
|
|
287
|
+
const requestUrl = originalRequest.url ?? "";
|
|
288
|
+
if (typeof requestUrl === "string" && requestUrl.includes(refreshUrl)) {
|
|
158
289
|
clearAuth();
|
|
159
290
|
return Promise.reject(error);
|
|
160
291
|
}
|
|
161
|
-
originalRequest._retry
|
|
162
|
-
const { refreshToken } = tokenAccess.get();
|
|
163
|
-
if (!refreshToken) {
|
|
292
|
+
if (originalRequest._retry) {
|
|
164
293
|
clearAuth();
|
|
165
294
|
return Promise.reject(error);
|
|
166
295
|
}
|
|
167
|
-
|
|
296
|
+
originalRequest._retry = true;
|
|
297
|
+
if (refreshManager.refreshing) {
|
|
168
298
|
return new Promise((resolve, reject) => {
|
|
169
|
-
|
|
170
|
-
if (!
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
299
|
+
refreshManager.subscribe((newAccessToken) => {
|
|
300
|
+
if (!newAccessToken) {
|
|
301
|
+
reject(error);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const retryRequest = setAuthorizationHeader(originalRequest, newAccessToken);
|
|
305
|
+
resolve(instance(retryRequest));
|
|
174
306
|
});
|
|
175
307
|
});
|
|
176
308
|
}
|
|
177
|
-
|
|
309
|
+
refreshManager.begin();
|
|
178
310
|
try {
|
|
179
|
-
const newTokens = await requestRefresh(
|
|
180
|
-
|
|
311
|
+
const newTokens = await requestRefresh({
|
|
312
|
+
refreshUrl,
|
|
313
|
+
withCredentials,
|
|
314
|
+
refreshStrategy,
|
|
315
|
+
refreshToken: refreshStrategy === "body" ? refreshTokenAccess.get() : null,
|
|
316
|
+
parseRefreshResponse
|
|
317
|
+
});
|
|
318
|
+
setToken(newTokens);
|
|
181
319
|
onAuthStateChange?.("authenticated");
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
notifySubscribers(null);
|
|
320
|
+
refreshManager.end();
|
|
321
|
+
refreshManager.notify(newTokens.accessToken);
|
|
322
|
+
const retryRequest = setAuthorizationHeader(originalRequest, newTokens.accessToken);
|
|
323
|
+
return instance(retryRequest);
|
|
324
|
+
} catch (refreshError) {
|
|
325
|
+
refreshManager.end();
|
|
326
|
+
refreshManager.notify(null);
|
|
190
327
|
clearAuth();
|
|
191
|
-
return Promise.reject(
|
|
328
|
+
return Promise.reject(refreshError);
|
|
192
329
|
}
|
|
193
330
|
}
|
|
194
331
|
);
|
|
195
332
|
return {
|
|
196
333
|
axios: instance,
|
|
197
|
-
setToken
|
|
198
|
-
getToken
|
|
199
|
-
clearToken:
|
|
334
|
+
setToken,
|
|
335
|
+
getToken,
|
|
336
|
+
clearToken: clearAuth
|
|
200
337
|
};
|
|
201
338
|
}
|
|
202
339
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/client/index.d.cts
CHANGED
|
@@ -1,144 +1,173 @@
|
|
|
1
|
-
import { AxiosInstance } from 'axios';
|
|
1
|
+
import { AxiosInstance, AxiosError } from 'axios';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @packageDocumentation
|
|
5
|
-
* Axios
|
|
6
|
-
*
|
|
7
|
-
* 공통 요구사항:
|
|
8
|
-
* - accessToken이 있으면 Authorization 헤더에 첨부
|
|
9
|
-
* - 401 발생 시 refreshToken으로 토큰 재발급을 시도하고, 원래 요청을 재시도
|
|
10
|
-
* - refresh 중복 호출 방지(큐 방식)
|
|
11
|
-
*
|
|
12
|
-
* 단순화 정책:
|
|
13
|
-
* - refresh URL만 옵션으로 받아 내부에서 fetch로 토큰 재발급을 수행한다.
|
|
14
|
-
* - 토큰 저장은 "localStorage" 또는 "cookie" 2가지 모드만 지원한다.
|
|
15
|
-
*
|
|
16
|
-
* ⚠️ 이 모듈은 브라우저 전용이다(window/localStorage 사용 가능).
|
|
17
|
-
* Next.js에서는 반드시 Client Component 또는 클라이언트 전용 모듈에서 import 해야 한다.
|
|
5
|
+
* Axios client type definitions.
|
|
18
6
|
*/
|
|
19
7
|
|
|
20
8
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
9
|
+
* AccessToken 저장 방식
|
|
10
|
+
*
|
|
11
|
+
* - localStorage: 브라우저 localStorage에 저장
|
|
12
|
+
* - memory: 현재 탭 메모리에만 저장
|
|
13
|
+
* - none: 저장하지 않음
|
|
24
14
|
*/
|
|
25
|
-
type
|
|
15
|
+
type AccessTokenStorageMode = "localStorage" | "memory" | "none";
|
|
26
16
|
/**
|
|
27
|
-
*
|
|
17
|
+
* Refresh 요청 전략
|
|
18
|
+
*
|
|
19
|
+
* - body:
|
|
20
|
+
* refreshToken을 클라이언트가 보관하고 있다가 body에 포함해 전송
|
|
21
|
+
*
|
|
22
|
+
* - cookieOnly:
|
|
23
|
+
* refreshToken을 클라이언트 JS가 보관하지 않고,
|
|
24
|
+
* HttpOnly Cookie 등 서버가 읽을 수 있는 방식만 사용
|
|
25
|
+
*/
|
|
26
|
+
type RefreshStrategy = "body" | "cookieOnly";
|
|
27
|
+
/**
|
|
28
|
+
* Refresh 요청 body
|
|
28
29
|
*/
|
|
29
30
|
type RefreshRequestBody = {
|
|
30
31
|
refreshToken: string;
|
|
31
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Refresh 응답 body
|
|
35
|
+
*/
|
|
32
36
|
type RefreshResponseBody = {
|
|
33
37
|
accessToken: string;
|
|
34
|
-
refreshToken
|
|
38
|
+
refreshToken?: string;
|
|
35
39
|
};
|
|
36
40
|
/**
|
|
37
|
-
*
|
|
41
|
+
* 로그인 상태
|
|
42
|
+
*/
|
|
43
|
+
type AuthState = "authenticated" | "unauthenticated";
|
|
44
|
+
/**
|
|
45
|
+
* localStorage key 설정
|
|
46
|
+
*/
|
|
47
|
+
type TokenStorageKeys = {
|
|
48
|
+
accessTokenKey?: string;
|
|
49
|
+
refreshTokenKey?: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* refresh 여부 판별 함수
|
|
53
|
+
*/
|
|
54
|
+
type ShouldRefreshFn = (error: AxiosError<unknown>) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* refresh 응답 파싱 함수
|
|
57
|
+
*/
|
|
58
|
+
type ParseRefreshResponseFn = (data: unknown) => RefreshResponseBody;
|
|
59
|
+
/**
|
|
60
|
+
* axios client 생성 옵션
|
|
38
61
|
*/
|
|
39
62
|
type CreateAxiosClientOptions = {
|
|
40
63
|
/**
|
|
41
|
-
* axios baseURL
|
|
64
|
+
* axios baseURL
|
|
42
65
|
*/
|
|
43
66
|
baseURL?: string;
|
|
44
67
|
/**
|
|
45
|
-
* 요청
|
|
68
|
+
* 요청 timeout(ms)
|
|
46
69
|
* @default 10000
|
|
47
70
|
*/
|
|
48
71
|
timeout?: number;
|
|
49
72
|
/**
|
|
50
73
|
* 쿠키 전송 여부
|
|
51
|
-
* - cookie 모드에서는 거의 필수
|
|
52
74
|
* @default true
|
|
53
75
|
*/
|
|
54
76
|
withCredentials?: boolean;
|
|
55
77
|
/**
|
|
56
|
-
*
|
|
78
|
+
* refresh API URL
|
|
57
79
|
* @example "/api/authorize/token"
|
|
58
80
|
*/
|
|
59
81
|
refreshUrl: string;
|
|
60
82
|
/**
|
|
61
|
-
*
|
|
83
|
+
* accessToken 저장 방식
|
|
62
84
|
* @default "localStorage"
|
|
63
85
|
*/
|
|
64
|
-
|
|
86
|
+
accessTokenStorage?: AccessTokenStorageMode;
|
|
65
87
|
/**
|
|
66
|
-
*
|
|
67
|
-
* @default
|
|
88
|
+
* refresh 전략
|
|
89
|
+
* @default "body"
|
|
68
90
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
refreshStrategy?: RefreshStrategy;
|
|
92
|
+
/**
|
|
93
|
+
* 저장 key 이름
|
|
94
|
+
*/
|
|
95
|
+
storageKeys?: TokenStorageKeys;
|
|
73
96
|
/**
|
|
74
|
-
*
|
|
75
|
-
* - 프로젝트의 auth context/ref/store 업데이트에 사용
|
|
97
|
+
* 인증 상태 변경 콜백
|
|
76
98
|
*/
|
|
77
|
-
onAuthStateChange?: (status:
|
|
99
|
+
onAuthStateChange?: (status: AuthState) => void;
|
|
78
100
|
/**
|
|
79
|
-
* 일반 에러
|
|
80
|
-
* - Swal/toast 등 프로젝트 스타일로 구현
|
|
101
|
+
* 일반 에러 출력 콜백
|
|
81
102
|
*/
|
|
82
103
|
onError?: (message: string, error: unknown) => void;
|
|
83
104
|
/**
|
|
84
|
-
*
|
|
85
|
-
* - null 반환 시 내부 기본 로직 사용
|
|
105
|
+
* 에러 메시지 추출 커스터마이즈
|
|
86
106
|
*/
|
|
87
107
|
getErrorMessage?: (error: unknown) => string | null;
|
|
108
|
+
/**
|
|
109
|
+
* refresh 여부 판별 커스터마이즈
|
|
110
|
+
*/
|
|
111
|
+
shouldRefresh?: ShouldRefreshFn;
|
|
112
|
+
/**
|
|
113
|
+
* refresh 응답 파싱 커스터마이즈
|
|
114
|
+
*/
|
|
115
|
+
parseRefreshResponse?: ParseRefreshResponseFn;
|
|
88
116
|
};
|
|
89
117
|
/**
|
|
90
|
-
* createAxiosClient
|
|
118
|
+
* createAxiosClient 반환 타입
|
|
91
119
|
*/
|
|
92
120
|
type AxiosClient = {
|
|
93
121
|
/**
|
|
94
|
-
*
|
|
122
|
+
* 구성된 axios instance
|
|
95
123
|
*/
|
|
96
124
|
axios: AxiosInstance;
|
|
97
125
|
/**
|
|
98
|
-
* 토큰
|
|
99
|
-
* - localStorage 모드: localStorage에 저장
|
|
100
|
-
* - cookie 모드: 기본 구현은 refreshToken만 저장(필요 시)
|
|
126
|
+
* 토큰 저장
|
|
101
127
|
*/
|
|
102
128
|
setToken: (data: Partial<RefreshResponseBody>) => void;
|
|
103
129
|
/**
|
|
104
|
-
* 토큰 조회
|
|
130
|
+
* 현재 토큰 조회
|
|
105
131
|
*/
|
|
106
132
|
getToken: () => {
|
|
107
133
|
accessToken: string | null;
|
|
108
134
|
refreshToken: string | null;
|
|
109
135
|
};
|
|
110
136
|
/**
|
|
111
|
-
* 토큰 제거
|
|
137
|
+
* 토큰 제거
|
|
112
138
|
*/
|
|
113
139
|
clearToken: () => void;
|
|
114
140
|
};
|
|
141
|
+
|
|
115
142
|
/**
|
|
116
|
-
* 공용 axios
|
|
143
|
+
* 공용 axios client를 생성합니다.
|
|
117
144
|
*
|
|
118
|
-
*
|
|
145
|
+
* 이 클라이언트는 다음 기능을 제공합니다.
|
|
146
|
+
* - accessToken 자동 첨부
|
|
147
|
+
* - 401 발생 시 refresh 후 원래 요청 재시도
|
|
148
|
+
* - refresh 중복 호출 방지
|
|
119
149
|
*
|
|
120
|
-
* @example
|
|
150
|
+
* @example
|
|
121
151
|
* ```ts
|
|
122
152
|
* "use client";
|
|
123
153
|
*
|
|
124
154
|
* import Swal from "sweetalert2";
|
|
125
|
-
* import
|
|
126
|
-
* import { authContextRef } from "@/components/providers/auth-provider";
|
|
127
|
-
* import { createAxiosClient } from "@your-scope/toolkit/client";
|
|
155
|
+
* import { createAxiosClient } from "@your-scope/your-package/client";
|
|
128
156
|
*
|
|
129
|
-
* export const { axios, setToken,
|
|
157
|
+
* export const { axios, setToken, getToken, clearToken } = createAxiosClient({
|
|
130
158
|
* refreshUrl: "/api/authorize/token",
|
|
131
|
-
*
|
|
159
|
+
* accessTokenStorage: "localStorage",
|
|
160
|
+
* refreshStrategy: "cookieOnly",
|
|
161
|
+
* withCredentials: true,
|
|
132
162
|
*
|
|
133
163
|
* onAuthStateChange: (status) => {
|
|
134
|
-
*
|
|
164
|
+
* console.log("auth state:", status);
|
|
135
165
|
* },
|
|
136
166
|
*
|
|
137
167
|
* onError: (message) => {
|
|
138
168
|
* Swal.fire({
|
|
139
169
|
* title: "오류 발생",
|
|
140
|
-
* html:
|
|
141
|
-
* backdrop: false,
|
|
170
|
+
* html: message.replace(/\\n/g, "<br />"),
|
|
142
171
|
* confirmButtonText: "확인",
|
|
143
172
|
* });
|
|
144
173
|
* },
|
|
@@ -147,33 +176,17 @@ type AxiosClient = {
|
|
|
147
176
|
* export default axios;
|
|
148
177
|
* ```
|
|
149
178
|
*
|
|
150
|
-
* @example
|
|
179
|
+
* @example
|
|
151
180
|
* ```ts
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* body: JSON.stringify({ id, pw })
|
|
158
|
-
* }).then(r => r.json());
|
|
159
|
-
*
|
|
160
|
-
* setToken({
|
|
161
|
-
* accessToken: res.accessToken,
|
|
162
|
-
* refreshToken: res.refreshToken
|
|
163
|
-
* });
|
|
164
|
-
* }
|
|
181
|
+
* const { axios } = createAxiosClient({
|
|
182
|
+
* refreshUrl: "/api/authorize/token",
|
|
183
|
+
* accessTokenStorage: "localStorage",
|
|
184
|
+
* refreshStrategy: "body",
|
|
185
|
+
* });
|
|
165
186
|
* ```
|
|
166
187
|
*
|
|
167
|
-
* @
|
|
168
|
-
* ```ts
|
|
169
|
-
* import { clearToken } from "@/lib/axios";
|
|
170
|
-
*
|
|
171
|
-
* function logout() {
|
|
172
|
-
* clearToken();
|
|
173
|
-
* location.href = "/login";
|
|
174
|
-
* }
|
|
175
|
-
* ```
|
|
188
|
+
* @param options 생성 옵션
|
|
176
189
|
*/
|
|
177
190
|
declare function createAxiosClient(options: CreateAxiosClientOptions): AxiosClient;
|
|
178
191
|
|
|
179
|
-
export { type AxiosClient, type CreateAxiosClientOptions, type RefreshRequestBody, type RefreshResponseBody, type
|
|
192
|
+
export { type AccessTokenStorageMode, type AuthState, type AxiosClient, type CreateAxiosClientOptions, type ParseRefreshResponseFn, type RefreshRequestBody, type RefreshResponseBody, type RefreshStrategy, type ShouldRefreshFn, type TokenStorageKeys, createAxiosClient };
|