sh-ui-cli 0.31.1 → 0.32.0
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/data/changelog/versions.json +13 -0
- package/package.json +1 -1
- package/src/create/cli-args.js +1 -1
- package/src/create/index.mjs +1 -1
- package/src/create/plugins/authJwt.js +340 -0
- package/src/create/plugins/index.js +2 -1
- package/src/create/plugins/sentry.js +32 -280
- package/src/mcp.mjs +1 -1
- package/templates/nextjs-app/app/api/proxy/[...path]/route.ts +31 -4
- package/templates/nextjs-app/package.json +0 -1
- package/templates/nextjs-app/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
- package/templates/nextjs-app/src/shared/api/clientFetch.ts +40 -0
- package/templates/nextjs-app/src/shared/api/http.ts +13 -56
- package/templates/nextjs-app/src/shared/api/observability.ts +20 -0
- package/templates/nextjs-app/src/shared/api/queryClient.ts +30 -0
- package/templates/nextjs-app/src/shared/api/serverFetch.ts +59 -0
- package/templates/nextjs-app/src/shared/hooks/useAppMutation.ts +52 -0
- package/templates/nextjs-standalone/app/api/proxy/[...path]/route.ts +31 -4
- package/templates/nextjs-standalone/package.json +0 -1
- package/templates/nextjs-standalone/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
- package/templates/nextjs-standalone/src/shared/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/src/shared/api/http.ts +13 -56
- package/templates/nextjs-standalone/src/shared/api/observability.ts +20 -0
- package/templates/nextjs-standalone/src/shared/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/src/shared/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/src/shared/hooks/useAppMutation.ts +52 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.32.0",
|
|
7
|
+
"date": "2026-04-29",
|
|
8
|
+
"title": "auth-jwt 플러그인 신설 + isomorphic HTTP transport 재설계",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"신규 --plugins auth-jwt — Next 16 proxy.ts 미들웨어, refresh placeholder, withAuthRetry 헬퍼, refresh-aware BFF 를 한 번에 추가. 백엔드 refresh API 명세 확정 후 refreshSession.ts 본문만 채우면 자동 활성화",
|
|
12
|
+
"베이스 HTTP 레이어 재설계 — axios 단일 인스턴스를 isomorphic http() + serverFetch / clientFetch 두 갈래 transport 로 교체. RSC 는 백엔드 직통, 브라우저는 /api/proxy 경유 → API 함수는 한 번만 작성하고 RSC prefetch + hydration 패턴과 자연스럽게 어울림",
|
|
13
|
+
"Sentry 플러그인 슬림화 — HTTP/proxy 인프라 파일을 베이스로 이관하고, Sentry 는 observability.ts 를 Sentry-aware 버전으로 덮어써 5xx 캡처/로깅을 활성화. 두 플러그인이 독립적으로 조합됨",
|
|
14
|
+
"queryClient.ts 추가 — React cache() 기반 RSC 스코프 + 브라우저 싱글톤. 요청 간 캐시 누수 방지. apps/docs 의 auth / api-layer / testing 레시피도 새 설계에 맞춰 갱신"
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.32.0"
|
|
17
|
+
},
|
|
5
18
|
{
|
|
6
19
|
"version": "0.31.1",
|
|
7
20
|
"date": "2026-04-28",
|
package/package.json
CHANGED
package/src/create/cli-args.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const VALID_PLATFORMS = ['next', 'flutter'];
|
|
2
2
|
const VALID_STRUCTURES = ['standalone', 'monorepo'];
|
|
3
|
-
const VALID_PLUGINS = ['sentry', 'next-intl'];
|
|
3
|
+
const VALID_PLUGINS = ['sentry', 'next-intl', 'auth-jwt'];
|
|
4
4
|
|
|
5
5
|
const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app'];
|
|
6
6
|
const BOOL_FLAGS = ['yes', 'help'];
|
package/src/create/index.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
|
|
|
13
13
|
옵션:
|
|
14
14
|
--platform <next|flutter> 타겟 플랫폼
|
|
15
15
|
--structure <standalone|monorepo> Next.js 프로젝트 구조 (next 일 때)
|
|
16
|
-
--plugins <a,b> 플러그인 (sentry, next-intl). 미지정/"" → 없음
|
|
16
|
+
--plugins <a,b> 플러그인 (sentry, next-intl, auth-jwt). 미지정/"" → 없음
|
|
17
17
|
--theme <base64> 테마 JSON (base64). 선택
|
|
18
18
|
--yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
|
|
19
19
|
-h, --help 이 도움말
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
export const authJwtPlugin = {
|
|
2
|
+
name: 'auth-jwt',
|
|
3
|
+
label: '쿠키 기반 JWT 인증 (refresh 자리표시자 포함)',
|
|
4
|
+
priority: 2,
|
|
5
|
+
|
|
6
|
+
// 의존성 추가 없음 — 베이스의 fetch + cookies + react-query 만 사용
|
|
7
|
+
|
|
8
|
+
envVars: [
|
|
9
|
+
'# Auth (auth-jwt)',
|
|
10
|
+
'COOKIE_SECURE=false',
|
|
11
|
+
],
|
|
12
|
+
|
|
13
|
+
turboEnvVars: [
|
|
14
|
+
'COOKIE_SECURE',
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
providerImports: [],
|
|
18
|
+
providerWrappers: [],
|
|
19
|
+
|
|
20
|
+
// ─── 독립 파일 ───
|
|
21
|
+
//
|
|
22
|
+
// 베이스의 BFF (app/api/proxy/[...path]/route.ts) 를 refresh-aware 버전으로
|
|
23
|
+
// 덮어쓰고, 미들웨어와 인증 헬퍼를 추가한다.
|
|
24
|
+
// refreshSession.ts 는 v1 placeholder — 백엔드 명세 확정 후 본문만 채우면
|
|
25
|
+
// BFF 와 withAuthRetry 가 자동 활용한다.
|
|
26
|
+
|
|
27
|
+
files: {
|
|
28
|
+
'src/proxy.ts': `import { NextRequest, NextResponse } from 'next/server';
|
|
29
|
+
|
|
30
|
+
const AUTH_ROUTES = ['/sign-in', '/sign-up'];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Next 16+ 의 proxy.ts (구 middleware.ts).
|
|
34
|
+
* 토큰 존재 여부만 검사한다 — 만료 검사나 refresh 는 하지 않는다.
|
|
35
|
+
*
|
|
36
|
+
* - AT 쿠키 없음 + 인증 라우트 아님 → /sign-in 으로 리다이렉트
|
|
37
|
+
* - AT 쿠키 있음 또는 인증 라우트 → 통과
|
|
38
|
+
*
|
|
39
|
+
* AT 가 만료된 채 통과한 요청은 BFF (/api/proxy) 가 401 을 받아
|
|
40
|
+
* refreshSession 으로 갱신을 시도한다.
|
|
41
|
+
*/
|
|
42
|
+
export default function proxy(req: NextRequest) {
|
|
43
|
+
const { pathname } = req.nextUrl;
|
|
44
|
+
const hasToken = !!req.cookies.get('accessToken')?.value;
|
|
45
|
+
const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
|
|
46
|
+
|
|
47
|
+
if (isAuthRoute) return NextResponse.next();
|
|
48
|
+
if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
|
|
49
|
+
|
|
50
|
+
return NextResponse.next();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const config = {
|
|
54
|
+
matcher: '/((?!api|_next|.*\\\\..*).*)',
|
|
55
|
+
};
|
|
56
|
+
`,
|
|
57
|
+
|
|
58
|
+
'src/shared/api/refreshSession.ts': `type RefreshResult =
|
|
59
|
+
| { ok: true; accessToken: string; refreshToken: string }
|
|
60
|
+
| { ok: false };
|
|
61
|
+
|
|
62
|
+
let inflight: Promise<RefreshResult> | null = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* refreshToken 으로 새 accessToken / refreshToken 을 발급받는다.
|
|
66
|
+
*
|
|
67
|
+
* v1 placeholder — 백엔드 refresh API 명세가 확정되면 아래 TODO 부분을 채운다.
|
|
68
|
+
* 본문이 채워지면 BFF (/api/proxy) 와 withAuthRetry 가 자동으로 활용한다.
|
|
69
|
+
*
|
|
70
|
+
* 동시에 여러 요청이 401 을 만나도 inflight 모듈 변수로 코얼레싱돼서
|
|
71
|
+
* refresh 는 한 번만 발사된다.
|
|
72
|
+
*
|
|
73
|
+
* 참고 구현 예시:
|
|
74
|
+
*
|
|
75
|
+
* const res = await fetch(\`\${process.env.API_URL}/v1/auth/token/refresh\`, {
|
|
76
|
+
* method: 'POST',
|
|
77
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
* body: JSON.stringify({ refreshToken }),
|
|
79
|
+
* });
|
|
80
|
+
* const body = await res.json();
|
|
81
|
+
* if (body.result === 'SUCCESS') {
|
|
82
|
+
* return {
|
|
83
|
+
* ok: true,
|
|
84
|
+
* accessToken: body.data.accessToken,
|
|
85
|
+
* refreshToken: body.data.refreshToken,
|
|
86
|
+
* };
|
|
87
|
+
* }
|
|
88
|
+
* return { ok: false };
|
|
89
|
+
*/
|
|
90
|
+
export async function refreshSession(
|
|
91
|
+
refreshToken: string,
|
|
92
|
+
): Promise<RefreshResult> {
|
|
93
|
+
if (inflight) return inflight;
|
|
94
|
+
|
|
95
|
+
inflight = (async (): Promise<RefreshResult> => {
|
|
96
|
+
try {
|
|
97
|
+
// TODO: 백엔드 refresh API 명세 확정 후 여기에 fetch 호출을 작성.
|
|
98
|
+
// 지금은 placeholder 라 항상 실패 → BFF 가 쿠키 삭제 + 401 응답.
|
|
99
|
+
void refreshToken;
|
|
100
|
+
return { ok: false };
|
|
101
|
+
} finally {
|
|
102
|
+
inflight = null;
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
|
|
106
|
+
return inflight;
|
|
107
|
+
}
|
|
108
|
+
`,
|
|
109
|
+
|
|
110
|
+
'src/shared/api/withAuthRetry.ts': `import { cookies } from 'next/headers';
|
|
111
|
+
|
|
112
|
+
import { ApiError } from './error';
|
|
113
|
+
import { refreshSession } from './refreshSession';
|
|
114
|
+
|
|
115
|
+
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
116
|
+
const REFRESH_TOKEN_COOKIE = 'refreshToken';
|
|
117
|
+
|
|
118
|
+
const COOKIE = {
|
|
119
|
+
httpOnly: true,
|
|
120
|
+
secure: process.env.COOKIE_SECURE === 'true',
|
|
121
|
+
sameSite: 'lax' as const,
|
|
122
|
+
path: '/',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Route Handler 또는 Server Action 안에서 인증된 요청을 보낼 때 사용한다.
|
|
127
|
+
* 401 을 만나면 refreshSession 으로 토큰을 갱신하고 fn 을 한 번 더 실행한다.
|
|
128
|
+
*
|
|
129
|
+
* RSC (Server Component) 에서는 cookies().set() 이 막혀 있으므로 여기서 호출하면 안 된다.
|
|
130
|
+
* RSC 는 serverFetch 를 직접 호출하고, 401 은 prefetchQuery 가 swallow 한 뒤
|
|
131
|
+
* 클라이언트 refetch 가 BFF 경유로 자동 복구한다.
|
|
132
|
+
*
|
|
133
|
+
* 사용 예 (Server Action):
|
|
134
|
+
*
|
|
135
|
+
* 'use server';
|
|
136
|
+
* import { serverFetch } from '@/src/shared/api/serverFetch';
|
|
137
|
+
* import { withAuthRetry } from '@/src/shared/api/withAuthRetry';
|
|
138
|
+
*
|
|
139
|
+
* export async function toggleFavoriteAction(id: number) {
|
|
140
|
+
* return withAuthRetry(() =>
|
|
141
|
+
* serverFetch(\`/v1/products/\${id}/favorite\`, { method: 'POST' }),
|
|
142
|
+
* );
|
|
143
|
+
* }
|
|
144
|
+
*/
|
|
145
|
+
export async function withAuthRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
146
|
+
try {
|
|
147
|
+
return await fn();
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (!(e instanceof ApiError) || e.status !== 401) throw e;
|
|
150
|
+
|
|
151
|
+
const jar = await cookies();
|
|
152
|
+
const refreshToken = jar.get(REFRESH_TOKEN_COOKIE)?.value;
|
|
153
|
+
if (!refreshToken) throw e;
|
|
154
|
+
|
|
155
|
+
const r = await refreshSession(refreshToken);
|
|
156
|
+
if (!r.ok) throw new ApiError(401, 'UNAUTHORIZED', null);
|
|
157
|
+
|
|
158
|
+
jar.set(ACCESS_TOKEN_COOKIE, r.accessToken, COOKIE);
|
|
159
|
+
jar.set(REFRESH_TOKEN_COOKIE, r.refreshToken, COOKIE);
|
|
160
|
+
|
|
161
|
+
return await fn();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
`,
|
|
165
|
+
|
|
166
|
+
'app/api/proxy/[...path]/route.ts': `import { cookies } from 'next/headers';
|
|
167
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
168
|
+
|
|
169
|
+
import {
|
|
170
|
+
captureApiError,
|
|
171
|
+
logApiError,
|
|
172
|
+
} from '@/src/shared/api/observability';
|
|
173
|
+
import { refreshSession } from '@/src/shared/api/refreshSession';
|
|
174
|
+
|
|
175
|
+
const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
|
|
176
|
+
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
177
|
+
const REFRESH_TOKEN_COOKIE = 'refreshToken';
|
|
178
|
+
const LOCALE_COOKIE = 'NEXT_LOCALE';
|
|
179
|
+
|
|
180
|
+
const COOKIE = {
|
|
181
|
+
httpOnly: true,
|
|
182
|
+
secure: process.env.COOKIE_SECURE === 'true',
|
|
183
|
+
sameSite: 'lax' as const,
|
|
184
|
+
path: '/',
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const clearAuthCookies = (res: NextResponse) => {
|
|
188
|
+
res.cookies.set(ACCESS_TOKEN_COOKIE, '', { ...COOKIE, maxAge: 0 });
|
|
189
|
+
res.cookies.set(REFRESH_TOKEN_COOKIE, '', { ...COOKIE, maxAge: 0 });
|
|
190
|
+
return res;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const proxyRequest = async (
|
|
194
|
+
request: NextRequest,
|
|
195
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
196
|
+
method: string,
|
|
197
|
+
) => {
|
|
198
|
+
const { path } = await ctx.params;
|
|
199
|
+
const apiPath = path.join('/');
|
|
200
|
+
const url = new URL(\`\${API_URL}/\${apiPath}\`);
|
|
201
|
+
|
|
202
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
203
|
+
url.searchParams.set(key, value);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const cookieStore = await cookies();
|
|
207
|
+
const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
208
|
+
const refreshToken = cookieStore.get(REFRESH_TOKEN_COOKIE)?.value;
|
|
209
|
+
const locale =
|
|
210
|
+
cookieStore.get(LOCALE_COOKIE)?.value ??
|
|
211
|
+
request.headers.get('Accept-Language') ??
|
|
212
|
+
undefined;
|
|
213
|
+
|
|
214
|
+
const headers: Record<string, string> = {};
|
|
215
|
+
if (accessToken) headers.Authorization = \`Bearer \${accessToken}\`;
|
|
216
|
+
if (locale) headers['Accept-Language'] = locale;
|
|
217
|
+
|
|
218
|
+
let body: BodyInit | undefined;
|
|
219
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
220
|
+
const contentType = request.headers.get('Content-Type');
|
|
221
|
+
if (contentType?.includes('multipart/form-data')) {
|
|
222
|
+
body = await request.formData();
|
|
223
|
+
} else {
|
|
224
|
+
headers['Content-Type'] = 'application/json';
|
|
225
|
+
body = await request.text();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let response: Response;
|
|
230
|
+
try {
|
|
231
|
+
response = await fetch(url.toString(), { method, headers, body });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(\`[PROXY] \${method} \${url.toString()} —\`, error);
|
|
234
|
+
return NextResponse.json(
|
|
235
|
+
{
|
|
236
|
+
result: 'ERROR',
|
|
237
|
+
data: null,
|
|
238
|
+
error: {
|
|
239
|
+
code: 'NETWORK_ERROR',
|
|
240
|
+
message: '서버에 연결할 수 없습니다.',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{ status: 502 },
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let data = await response.json();
|
|
248
|
+
|
|
249
|
+
// 401 → refresh 시도 (RT 가 있을 때만)
|
|
250
|
+
if (response.status === 401) {
|
|
251
|
+
if (!refreshToken) {
|
|
252
|
+
return clearAuthCookies(NextResponse.json(data, { status: 401 }));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const r = await refreshSession(refreshToken);
|
|
256
|
+
|
|
257
|
+
// refresh placeholder 가 본문 미구현이면 항상 ok:false
|
|
258
|
+
// → 쿠키 삭제 + 401 그대로 (clientFetch 가 /sign-in 으로 이동)
|
|
259
|
+
if (!r.ok) {
|
|
260
|
+
return clearAuthCookies(NextResponse.json(data, { status: 401 }));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 새 AT 로 재시도
|
|
264
|
+
headers.Authorization = \`Bearer \${r.accessToken}\`;
|
|
265
|
+
const retry = await fetch(url.toString(), { method, headers, body });
|
|
266
|
+
data = await retry.json();
|
|
267
|
+
|
|
268
|
+
const res = NextResponse.json(data, { status: retry.status });
|
|
269
|
+
res.cookies.set(ACCESS_TOKEN_COOKIE, r.accessToken, COOKIE);
|
|
270
|
+
res.cookies.set(REFRESH_TOKEN_COOKIE, r.refreshToken, COOKIE);
|
|
271
|
+
|
|
272
|
+
if (!retry.ok) {
|
|
273
|
+
logApiError('PROXY', {
|
|
274
|
+
url: url.toString(),
|
|
275
|
+
method,
|
|
276
|
+
status: retry.status,
|
|
277
|
+
requestBody: typeof body === 'string' ? body : undefined,
|
|
278
|
+
responseBody: data,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
captureApiError({
|
|
282
|
+
url: url.toString(),
|
|
283
|
+
apiPath,
|
|
284
|
+
method,
|
|
285
|
+
status: retry.status,
|
|
286
|
+
responseBody: data,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return res;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
logApiError('PROXY', {
|
|
295
|
+
url: url.toString(),
|
|
296
|
+
method,
|
|
297
|
+
status: response.status,
|
|
298
|
+
requestBody: typeof body === 'string' ? body : undefined,
|
|
299
|
+
responseBody: data,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
captureApiError({
|
|
303
|
+
url: url.toString(),
|
|
304
|
+
apiPath,
|
|
305
|
+
method,
|
|
306
|
+
status: response.status,
|
|
307
|
+
responseBody: data,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return NextResponse.json(data, { status: response.status });
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const GET = (
|
|
315
|
+
req: NextRequest,
|
|
316
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
317
|
+
) => proxyRequest(req, ctx, 'GET');
|
|
318
|
+
|
|
319
|
+
export const POST = (
|
|
320
|
+
req: NextRequest,
|
|
321
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
322
|
+
) => proxyRequest(req, ctx, 'POST');
|
|
323
|
+
|
|
324
|
+
export const PUT = (
|
|
325
|
+
req: NextRequest,
|
|
326
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
327
|
+
) => proxyRequest(req, ctx, 'PUT');
|
|
328
|
+
|
|
329
|
+
export const PATCH = (
|
|
330
|
+
req: NextRequest,
|
|
331
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
332
|
+
) => proxyRequest(req, ctx, 'PATCH');
|
|
333
|
+
|
|
334
|
+
export const DELETE = (
|
|
335
|
+
req: NextRequest,
|
|
336
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
337
|
+
) => proxyRequest(req, ctx, 'DELETE');
|
|
338
|
+
`,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { sentryPlugin } from './sentry.js';
|
|
2
2
|
import { nextIntlPlugin } from './nextIntl.js';
|
|
3
|
+
import { authJwtPlugin } from './authJwt.js';
|
|
3
4
|
|
|
4
|
-
export const allPlugins = [sentryPlugin, nextIntlPlugin];
|
|
5
|
+
export const allPlugins = [sentryPlugin, nextIntlPlugin, authJwtPlugin];
|
|
5
6
|
|
|
6
7
|
export function getPluginChoices() {
|
|
7
8
|
return allPlugins.map((p) => ({
|