@thinkingcat/auth-utils 1.0.4

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/index.js ADDED
@@ -0,0 +1,709 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyToken = verifyToken;
4
+ exports.extractRoleFromPayload = extractRoleFromPayload;
5
+ exports.createNextAuthJWT = createNextAuthJWT;
6
+ exports.encodeNextAuthToken = encodeNextAuthToken;
7
+ exports.setCustomTokens = setCustomTokens;
8
+ exports.setNextAuthToken = setNextAuthToken;
9
+ exports.createRedirectHTML = createRedirectHTML;
10
+ exports.createAuthResponse = createAuthResponse;
11
+ exports.validateServiceSubscription = validateServiceSubscription;
12
+ exports.refreshSSOToken = refreshSSOToken;
13
+ exports.getRefreshTokenFromSSO = getRefreshTokenFromSSO;
14
+ exports.verifyAndRefreshToken = verifyAndRefreshToken;
15
+ exports.redirectToError = redirectToError;
16
+ exports.clearAuthCookies = clearAuthCookies;
17
+ exports.getEffectiveRole = getEffectiveRole;
18
+ exports.requiresSubscription = requiresSubscription;
19
+ exports.checkRoleAccess = checkRoleAccess;
20
+ exports.redirectToSSOLogin = redirectToSSOLogin;
21
+ exports.redirectToRoleDashboard = redirectToRoleDashboard;
22
+ exports.isTokenExpired = isTokenExpired;
23
+ exports.isValidToken = isValidToken;
24
+ exports.hasRole = hasRole;
25
+ exports.hasAnyRole = hasAnyRole;
26
+ exports.isPublicPath = isPublicPath;
27
+ exports.isApiPath = isApiPath;
28
+ exports.isProtectedApiPath = isProtectedApiPath;
29
+ exports.checkAuthentication = checkAuthentication;
30
+ const jwt_1 = require("next-auth/jwt");
31
+ const jose_1 = require("jose");
32
+ const server_1 = require("next/server");
33
+ /**
34
+ * 토큰 검증 및 디코딩
35
+ * @param accessToken JWT access token
36
+ * @param secret JWT 서명에 사용할 secret key
37
+ * @returns 검증된 payload 또는 null
38
+ */
39
+ async function verifyToken(accessToken, secret) {
40
+ try {
41
+ const secretBytes = new TextEncoder().encode(secret);
42
+ const { payload } = await (0, jose_1.jwtVerify)(accessToken, secretBytes);
43
+ if (payload && typeof payload === 'object' && payload.email) {
44
+ return { payload: payload };
45
+ }
46
+ return null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * payload에서 역할 추출 (서비스별)
54
+ * @param payload JWT payload
55
+ * @param serviceId 서비스 ID (기본값: 'checkon')
56
+ * @param defaultRole 기본 역할 (기본값: 'ADMIN')
57
+ * @returns 추출된 역할
58
+ */
59
+ function extractRoleFromPayload(payload, serviceId = 'checkon', defaultRole = 'ADMIN') {
60
+ const services = payload.services || [];
61
+ const service = services.find((s) => s.serviceId === serviceId);
62
+ return service?.role || payload.role || defaultRole;
63
+ }
64
+ /**
65
+ * payload에서 NextAuth JWT 객체 생성
66
+ * @param payload JWT payload
67
+ * @param includeAcademies academies 정보 포함 여부
68
+ * @returns NextAuth JWT 객체
69
+ */
70
+ function createNextAuthJWT(payload, includeAcademies = false) {
71
+ const services = payload.services || [];
72
+ const checkonService = services.find((s) => s.serviceId === 'checkon');
73
+ const effectiveRole = checkonService?.role || payload.role || 'ADMIN';
74
+ const jwt = {
75
+ id: (payload.id || payload.sub),
76
+ email: payload.email,
77
+ name: payload.name,
78
+ role: effectiveRole, // Role enum 타입
79
+ services: payload.services,
80
+ academies: includeAcademies
81
+ ? payload.academies || []
82
+ : [],
83
+ phoneVerified: payload.phoneVerified ?? false,
84
+ emailVerified: payload.emailVerified ?? false,
85
+ smsVerified: payload.smsVerified ?? false,
86
+ iat: Math.floor(Date.now() / 1000),
87
+ exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60), // 30일
88
+ };
89
+ // 선택적 필드들
90
+ if (payload.phone)
91
+ jwt.phone = payload.phone;
92
+ if (payload.academyId)
93
+ jwt.academyId = payload.academyId;
94
+ if (payload.academyName)
95
+ jwt.academyName = payload.academyName;
96
+ if (payload.isPasswordReset)
97
+ jwt.isPasswordReset = payload.isPasswordReset;
98
+ if (payload.decryptedEmail)
99
+ jwt.decryptedEmail = payload.decryptedEmail;
100
+ if (payload.decryptedPhone)
101
+ jwt.decryptedPhone = payload.decryptedPhone;
102
+ if (payload.emailHash)
103
+ jwt.emailHash = payload.emailHash;
104
+ if (payload.maskedEmail)
105
+ jwt.maskedEmail = payload.maskedEmail;
106
+ if (payload.phoneHash)
107
+ jwt.phoneHash = payload.phoneHash;
108
+ if (payload.maskedPhone)
109
+ jwt.maskedPhone = payload.maskedPhone;
110
+ if (payload.refreshToken)
111
+ jwt.refreshToken = payload.refreshToken;
112
+ if (payload.accessToken)
113
+ jwt.accessToken = payload.accessToken;
114
+ if (payload.accessTokenExpires)
115
+ jwt.accessTokenExpires = payload.accessTokenExpires;
116
+ if (payload.serviceId)
117
+ jwt.serviceId = payload.serviceId;
118
+ return jwt;
119
+ }
120
+ /**
121
+ * NextAuth JWT를 인코딩된 세션 토큰으로 변환
122
+ * @param jwt NextAuth JWT 객체
123
+ * @param secret JWT 서명에 사용할 secret key
124
+ * @param maxAge 토큰 유효 기간 (초, 기본값: 30일)
125
+ * @returns 인코딩된 세션 토큰
126
+ */
127
+ async function encodeNextAuthToken(jwt, secret, maxAge = 30 * 24 * 60 * 60) {
128
+ return await (0, jwt_1.encode)({
129
+ token: jwt,
130
+ secret: secret,
131
+ maxAge: maxAge,
132
+ });
133
+ }
134
+ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options) {
135
+ // 옵션 파라미터 처리: refreshToken과 options를 분리
136
+ let refreshTokenValue;
137
+ let cookiePrefix;
138
+ let isProduction;
139
+ if (typeof optionsOrRefreshToken === 'string') {
140
+ // 기존 방식: refreshToken이 문자열로 전달된 경우
141
+ refreshTokenValue = optionsOrRefreshToken;
142
+ const { cookiePrefix: prefix = 'checkon', isProduction: prod = false, } = options || {};
143
+ cookiePrefix = prefix;
144
+ isProduction = prod;
145
+ }
146
+ else {
147
+ // 새로운 방식: options 객체로 전달된 경우
148
+ const opts = optionsOrRefreshToken || {};
149
+ refreshTokenValue = opts.refreshToken;
150
+ cookiePrefix = opts.cookiePrefix || 'checkon';
151
+ isProduction = opts.isProduction || false;
152
+ }
153
+ // access_token 설정
154
+ const accessTokenName = `${cookiePrefix}_access_token`;
155
+ response.cookies.delete(accessTokenName);
156
+ response.cookies.set(accessTokenName, accessToken, {
157
+ httpOnly: true,
158
+ secure: isProduction,
159
+ sameSite: isProduction ? 'none' : 'lax',
160
+ maxAge: 15 * 60, // 15분
161
+ path: '/',
162
+ });
163
+ // refresh_token 설정 (있는 경우)
164
+ if (refreshTokenValue) {
165
+ const refreshTokenName = `${cookiePrefix}_refresh_token`;
166
+ response.cookies.set(refreshTokenName, refreshTokenValue, {
167
+ httpOnly: true,
168
+ secure: isProduction,
169
+ sameSite: isProduction ? 'none' : 'lax',
170
+ maxAge: 30 * 24 * 60 * 60, // 30일
171
+ path: '/',
172
+ });
173
+ }
174
+ }
175
+ /**
176
+ * NextAuth 세션 토큰만 설정
177
+ * @param response NextResponse 객체
178
+ * @param sessionToken NextAuth session token
179
+ * @param options 쿠키 설정 옵션
180
+ * @param options.isProduction 프로덕션 환경 여부 (기본값: false)
181
+ * @param options.cookieDomain 쿠키 도메인 (선택)
182
+ */
183
+ function setNextAuthToken(response, sessionToken, options = {}) {
184
+ const { isProduction = false, cookieDomain, } = options;
185
+ const cookieName = isProduction
186
+ ? '__Secure-next-auth.session-token'
187
+ : 'next-auth.session-token';
188
+ response.cookies.delete(cookieName);
189
+ const cookieOptions = {
190
+ httpOnly: true,
191
+ secure: isProduction,
192
+ sameSite: 'lax',
193
+ maxAge: 30 * 24 * 60 * 60, // 30일
194
+ path: '/',
195
+ };
196
+ if (cookieDomain) {
197
+ cookieOptions.domain = cookieDomain;
198
+ }
199
+ response.cookies.set(cookieName, sessionToken, cookieOptions);
200
+ }
201
+ /**
202
+ * 리다이렉트용 HTML 생성
203
+ * @param redirectPath 리다이렉트할 경로
204
+ * @param text 표시할 텍스트 (기본값: 'checkon')
205
+ * @returns HTML 문자열
206
+ */
207
+ function createRedirectHTML(redirectPath, text = 'checkon') {
208
+ return `
209
+ <!DOCTYPE html>
210
+ <html>
211
+ <head>
212
+ <meta charset="utf-8">
213
+ <title>Redirecting...</title>
214
+ <style>
215
+ * { margin: 0; padding: 0; box-sizing: border-box; }
216
+ html, body {
217
+ width: 100%;
218
+ height: 100%;
219
+ overflow: hidden;
220
+ }
221
+ body {
222
+ display: flex;
223
+ align-items: flex-start;
224
+ justify-content: flex-start;
225
+ padding-left: 10%;
226
+ padding-top: 20%;
227
+ background: #fff;
228
+ }
229
+ .typing-text {
230
+ font-family: 'Courier New', monospace;
231
+ font-size: 2.5rem;
232
+ font-weight: bold;
233
+ color: #667eea;
234
+ letter-spacing: 0.1em;
235
+ }
236
+ .cursor {
237
+ display: inline-block;
238
+ width: 3px;
239
+ height: 2.5rem;
240
+ background-color: #667eea;
241
+ margin-left: 2px;
242
+ animation: blink 0.7s infinite;
243
+ }
244
+ @keyframes blink {
245
+ 0%, 50% { opacity: 1; }
246
+ 51%, 100% { opacity: 0; }
247
+ }
248
+ </style>
249
+ </head>
250
+ <body>
251
+ <div class="typing-text">
252
+ <span id="text"></span><span class="cursor"></span>
253
+ </div>
254
+ <script>
255
+ const text = '${text}';
256
+ let index = 0;
257
+ const speed = 100;
258
+
259
+ function type() {
260
+ if (index < text.length) {
261
+ document.getElementById('text').textContent += text.charAt(index);
262
+ index++;
263
+ setTimeout(type, speed);
264
+ } else {
265
+ setTimeout(() => window.location.href = '${redirectPath}', 200);
266
+ }
267
+ }
268
+
269
+ type();
270
+ </script>
271
+ </body>
272
+ </html>
273
+ `;
274
+ }
275
+ /**
276
+ * access token과 refresh token을 사용하여 완전한 인증 세션 생성
277
+ * @param accessToken access token
278
+ * @param secret JWT 서명에 사용할 secret key
279
+ * @param options 추가 옵션
280
+ * @param options.refreshToken refresh token (선택)
281
+ * @param options.redirectPath 리다이렉트할 경로 (기본값: 페이지 리로드)
282
+ * @param options.text 리다이렉트 HTML에 표시할 텍스트 (기본값: 'checkon')
283
+ * @param options.cookiePrefix 쿠키 이름 접두사 (기본값: 'checkon')
284
+ * @param options.isProduction 프로덕션 환경 여부 (기본값: false)
285
+ * @param options.cookieDomain 쿠키 도메인 (선택)
286
+ * @returns NextResponse 객체
287
+ */
288
+ async function createAuthResponse(accessToken, secret, options = {}) {
289
+ const { refreshToken, redirectPath, text = 'checkon', cookiePrefix = 'checkon', isProduction = false, cookieDomain, } = options;
290
+ // 1. 토큰 검증
291
+ const tokenResult = await verifyToken(accessToken, secret);
292
+ if (!tokenResult) {
293
+ throw new Error('Invalid token');
294
+ }
295
+ const { payload } = tokenResult;
296
+ // 2. 역할 추출
297
+ const role = extractRoleFromPayload(payload);
298
+ // 3. NextAuth JWT 생성 및 인코딩
299
+ const jwt = createNextAuthJWT(payload);
300
+ const sessionToken = await encodeNextAuthToken(jwt, secret);
301
+ // 4. HTML 생성
302
+ const html = redirectPath
303
+ ? createRedirectHTML(redirectPath, text)
304
+ : createRedirectHTML('', text).replace("window.location.href = ''", "window.location.reload()");
305
+ // 5. Response 생성
306
+ const response = new server_1.NextResponse(html, {
307
+ status: 200,
308
+ headers: {
309
+ 'Content-Type': 'text/html',
310
+ },
311
+ });
312
+ // 6. 쿠키 설정
313
+ // 자체 토큰 설정
314
+ if (refreshToken) {
315
+ setCustomTokens(response, accessToken, refreshToken, {
316
+ cookiePrefix,
317
+ isProduction,
318
+ });
319
+ }
320
+ else {
321
+ setCustomTokens(response, accessToken, {
322
+ cookiePrefix,
323
+ isProduction,
324
+ });
325
+ }
326
+ // NextAuth 토큰 설정
327
+ if (sessionToken) {
328
+ setNextAuthToken(response, sessionToken, {
329
+ isProduction,
330
+ cookieDomain,
331
+ });
332
+ }
333
+ return response;
334
+ }
335
+ /**
336
+ * 서비스 구독 유효성 확인 함수
337
+ * @param services 서비스 정보 배열
338
+ * @param serviceId 확인할 서비스 ID
339
+ * @returns 구독 유효성 결과
340
+ */
341
+ function validateServiceSubscription(services, serviceId) {
342
+ const filteredServices = services.filter(service => service.serviceId === serviceId);
343
+ if (filteredServices.length === 0) {
344
+ return {
345
+ isValid: false,
346
+ redirectUrl: `${process.env.SSO_AUTH_SERVER_URL || 'http://localhost:3000'}/services/${serviceId}?type=subscription_required`
347
+ };
348
+ }
349
+ const service = filteredServices[0];
350
+ if (service.status !== "ACTIVE") {
351
+ return {
352
+ isValid: false,
353
+ redirectUrl: `${process.env.SSO_AUTH_SERVER_URL || 'http://localhost:3000'}/services/${service.serviceId}?type=subscription_required`,
354
+ service
355
+ };
356
+ }
357
+ return {
358
+ isValid: true,
359
+ service
360
+ };
361
+ }
362
+ /**
363
+ * SSO 서버에서 refresh token을 사용하여 새로운 access token을 발급받는 함수
364
+ * @param refreshToken refresh token
365
+ * @param options 옵션
366
+ * @param options.ssoBaseURL SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
367
+ * @param options.authServiceKey 인증 서비스 키 (기본값: 환경 변수)
368
+ * @returns SSO refresh token 응답
369
+ */
370
+ async function refreshSSOToken(refreshToken, options) {
371
+ const ssoBaseURL = options?.ssoBaseURL || process.env.NEXT_PUBLIC_SSO_BASE_URL || 'https://sso.thinkingcatworks.com';
372
+ const authServiceKey = options?.authServiceKey || process.env.AUTH_SERVICE_SECRET_KEY;
373
+ if (!authServiceKey) {
374
+ throw new Error('AUTH_SERVICE_SECRET_KEY not configured');
375
+ }
376
+ const refreshResponse = await fetch(`${ssoBaseURL}/api/sso/refresh`, {
377
+ method: 'POST',
378
+ headers: {
379
+ 'Content-Type': 'application/json',
380
+ 'x-auth-service-key': authServiceKey,
381
+ },
382
+ body: JSON.stringify({ refreshToken }),
383
+ });
384
+ return await refreshResponse.json();
385
+ }
386
+ /**
387
+ * SSO 서버에서 사용자의 refresh token을 가져오는 함수
388
+ * @param userId 사용자 ID
389
+ * @param accessToken access token
390
+ * @param options 옵션
391
+ * @param options.ssoBaseURL SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
392
+ * @param options.authServiceKey 인증 서비스 키 (기본값: 환경 변수)
393
+ * @returns refresh token 또는 null
394
+ */
395
+ async function getRefreshTokenFromSSO(userId, accessToken, options) {
396
+ const ssoBaseURL = options?.ssoBaseURL || process.env.NEXT_PUBLIC_SSO_BASE_URL || 'https://sso.thinkingcatworks.com';
397
+ const authServiceKey = options?.authServiceKey || process.env.AUTH_SERVICE_SECRET_KEY;
398
+ if (!authServiceKey) {
399
+ return null;
400
+ }
401
+ try {
402
+ const refreshResponse = await fetch(`${ssoBaseURL}/api/sso/get-refresh-token`, {
403
+ method: 'POST',
404
+ headers: {
405
+ 'Content-Type': 'application/json',
406
+ 'x-auth-service-key': authServiceKey,
407
+ },
408
+ body: JSON.stringify({
409
+ userId,
410
+ accessToken
411
+ }),
412
+ });
413
+ const refreshResult = await refreshResponse.json();
414
+ if (refreshResponse.ok && refreshResult.success && refreshResult.refreshToken) {
415
+ return refreshResult.refreshToken;
416
+ }
417
+ }
418
+ catch {
419
+ // Refresh token 실패해도 access token으로 일단 로그인 가능
420
+ }
421
+ return null;
422
+ }
423
+ /**
424
+ * 토큰 검증 및 리프레시 처리 공통 함수
425
+ *
426
+ * @param req - NextRequest 객체
427
+ * @param secret - JWT 서명에 사용할 secret key
428
+ * @param options - 옵션
429
+ * @param options.cookiePrefix - 쿠키 이름 접두사 (기본값: 'checkon')
430
+ * @param options.isProduction - 프로덕션 환경 여부 (기본값: false)
431
+ * @param options.cookieDomain - 쿠키 도메인 (선택)
432
+ * @param options.text - 리다이렉트 HTML에 표시할 텍스트 (기본값: 'checkon')
433
+ * @param options.ssoBaseURL - SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
434
+ * @param options.authServiceKey - 인증 서비스 키 (기본값: 환경 변수)
435
+ * @returns 검증 결과 및 필요시 리프레시된 응답
436
+ */
437
+ async function verifyAndRefreshToken(req, secret, options) {
438
+ const { cookiePrefix = 'checkon', isProduction = false, cookieDomain, text = 'checkon', } = options || {};
439
+ // 1. access_token 쿠키 확인
440
+ const accessTokenName = `${cookiePrefix}_access_token`;
441
+ const accessToken = req.cookies.get(accessTokenName)?.value;
442
+ if (accessToken) {
443
+ const tokenResult = await verifyToken(accessToken, secret);
444
+ if (tokenResult) {
445
+ return { isValid: true, payload: tokenResult.payload };
446
+ }
447
+ // 토큰이 만료되었거나 유효하지 않음 (리프레시 시도)
448
+ }
449
+ // 2. 리프레시 토큰으로 갱신 시도
450
+ const refreshTokenName = `${cookiePrefix}_refresh_token`;
451
+ const refreshToken = req.cookies.get(refreshTokenName)?.value;
452
+ if (refreshToken) {
453
+ try {
454
+ const refreshResult = await refreshSSOToken(refreshToken, {
455
+ ssoBaseURL: options?.ssoBaseURL,
456
+ authServiceKey: options?.authServiceKey,
457
+ });
458
+ if (refreshResult.success && refreshResult.accessToken) {
459
+ // 토큰 갱신 성공 - createAuthResponse 사용
460
+ const newRefreshToken = refreshResult.refreshToken || refreshToken;
461
+ try {
462
+ const response = await createAuthResponse(refreshResult.accessToken, secret, {
463
+ refreshToken: newRefreshToken,
464
+ redirectPath: '', // 리다이렉트 없이 현재 페이지 유지
465
+ text,
466
+ cookiePrefix,
467
+ isProduction,
468
+ cookieDomain,
469
+ });
470
+ // payload 추출을 위해 토큰 검증
471
+ const tokenResult = await verifyToken(refreshResult.accessToken, secret);
472
+ const payload = tokenResult?.payload;
473
+ return { isValid: true, response, payload };
474
+ }
475
+ catch (error) {
476
+ console.error('Failed to create auth response:', error);
477
+ return { isValid: false, error: 'SESSION_CREATION_FAILED' };
478
+ }
479
+ }
480
+ else {
481
+ return { isValid: false, error: 'REFRESH_FAILED' };
482
+ }
483
+ }
484
+ catch (error) {
485
+ console.error('Token refresh error:', error);
486
+ return { isValid: false, error: 'REFRESH_ERROR' };
487
+ }
488
+ }
489
+ return { isValid: false, error: 'NO_TOKEN' };
490
+ }
491
+ /**
492
+ * 에러 페이지로 리다이렉트하는 헬퍼 함수
493
+ * @param req NextRequest 객체
494
+ * @param code 에러 코드
495
+ * @param message 에러 메시지
496
+ * @param errorPath 에러 페이지 경로 (기본값: '/error')
497
+ * @returns NextResponse 리다이렉트 응답
498
+ */
499
+ function redirectToError(req, code, message, errorPath = '/error') {
500
+ const url = new URL(errorPath, req.url);
501
+ url.searchParams.set('code', code);
502
+ url.searchParams.set('message', message);
503
+ return server_1.NextResponse.redirect(url);
504
+ }
505
+ /**
506
+ * 인증 쿠키를 삭제하는 헬퍼 함수
507
+ * @param response NextResponse 객체
508
+ * @param cookiePrefix 쿠키 이름 접두사 (기본값: 'checkon')
509
+ * @returns NextResponse 객체
510
+ */
511
+ function clearAuthCookies(response, cookiePrefix = 'checkon') {
512
+ response.cookies.delete(`${cookiePrefix}_access_token`);
513
+ response.cookies.delete(`${cookiePrefix}_refresh_token`);
514
+ return response;
515
+ }
516
+ /**
517
+ * JWT에서 서비스별 역할을 추출하는 헬퍼 함수
518
+ * @param token NextAuth JWT 객체 또는 null
519
+ * @param serviceId 서비스 ID (기본값: 'checkon')
520
+ * @returns 추출된 역할 또는 undefined
521
+ */
522
+ function getEffectiveRole(token, serviceId = 'checkon') {
523
+ if (!token)
524
+ return undefined;
525
+ // token이 이미 JWT 객체인 경우 role을 직접 사용
526
+ if (token.role) {
527
+ return token.role;
528
+ }
529
+ // services에서 추출
530
+ const services = token.services || [];
531
+ const service = services.find((s) => s.serviceId === serviceId);
532
+ return service?.role;
533
+ }
534
+ /**
535
+ * 구독이 필요한 경로인지 확인하는 헬퍼 함수
536
+ * @param pathname 경로명
537
+ * @param role 사용자 역할
538
+ * @param subscriptionRequiredPaths 구독이 필요한 경로 배열
539
+ * @param systemAdminRole 시스템 관리자 역할 (기본값: 'SYSTEM_ADMIN')
540
+ * @returns 구독이 필요한지 여부
541
+ */
542
+ function requiresSubscription(pathname, role, subscriptionRequiredPaths, systemAdminRole = 'SYSTEM_ADMIN') {
543
+ // 시스템 관리자는 구독 확인 제외
544
+ if (role === systemAdminRole) {
545
+ return false;
546
+ }
547
+ // 구독이 필요한 경로인지 확인
548
+ return subscriptionRequiredPaths.some(path => pathname.startsWith(path));
549
+ }
550
+ function checkRoleAccess(pathname, role, roleConfig) {
551
+ // 각 역할 설정을 확인
552
+ for (const [configRole, config] of Object.entries(roleConfig)) {
553
+ const isPathMatch = config.paths.some(path => pathname.startsWith(path));
554
+ if (isPathMatch) {
555
+ // 역할이 정확히 일치하거나 allowedRoles에 포함되어 있으면 허용
556
+ if (role === configRole || config.allowedRoles?.includes(role)) {
557
+ return { allowed: true };
558
+ }
559
+ // 접근 거부
560
+ return { allowed: false, message: config.message };
561
+ }
562
+ }
563
+ // 매칭되는 경로가 없으면 허용
564
+ return { allowed: true };
565
+ }
566
+ /**
567
+ * SSO 로그인 페이지로 리다이렉트하는 헬퍼 함수
568
+ * @param req NextRequest 객체
569
+ * @param serviceId 서비스 ID
570
+ * @param ssoBaseURL SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
571
+ * @returns NextResponse 리다이렉트 응답
572
+ */
573
+ function redirectToSSOLogin(req, serviceId, ssoBaseURL) {
574
+ const baseURL = ssoBaseURL || process.env.NEXT_PUBLIC_SSO_BASE_URL || "https://sso.thinkingcatworks.com";
575
+ return server_1.NextResponse.redirect(new URL(`${baseURL}/auth/login?serviceId=${serviceId}`, req.url));
576
+ }
577
+ /**
578
+ * 역할별 대시보드 경로로 리다이렉트하는 헬퍼 함수
579
+ * @param req NextRequest 객체
580
+ * @param role 사용자 역할
581
+ * @param rolePaths 역할별 경로 설정 객체
582
+ * @param defaultPath 기본 경로 (기본값: '/admin')
583
+ * @returns NextResponse 리다이렉트 응답
584
+ */
585
+ function redirectToRoleDashboard(req, role, rolePaths, defaultPath = '/admin') {
586
+ const redirectPath = rolePaths[role] || defaultPath;
587
+ return server_1.NextResponse.redirect(new URL(redirectPath, req.url));
588
+ }
589
+ /**
590
+ * 토큰이 만료되었는지 확인하는 함수
591
+ * @param token NextAuth JWT 객체
592
+ * @returns 만료 여부
593
+ */
594
+ function isTokenExpired(token) {
595
+ if (!token)
596
+ return true;
597
+ if (token.exp && typeof token.exp === 'number' && token.exp < Math.floor(Date.now() / 1000)) {
598
+ return true;
599
+ }
600
+ return false;
601
+ }
602
+ /**
603
+ * 토큰이 유효한지 확인하는 함수 (만료 및 필수 필드 체크)
604
+ * @param token NextAuth JWT 객체
605
+ * @returns 유효성 여부
606
+ */
607
+ function isValidToken(token) {
608
+ if (!token)
609
+ return false;
610
+ if (isTokenExpired(token))
611
+ return false;
612
+ if (!token.email || !token.id)
613
+ return false;
614
+ return true;
615
+ }
616
+ /**
617
+ * 특정 역할을 가지고 있는지 확인하는 함수
618
+ * @param token NextAuth JWT 객체
619
+ * @param role 확인할 역할
620
+ * @param serviceId 서비스 ID (기본값: 'checkon')
621
+ * @returns 역할 보유 여부
622
+ */
623
+ function hasRole(token, role, serviceId = 'checkon') {
624
+ if (!token)
625
+ return false;
626
+ const effectiveRole = getEffectiveRole(token, serviceId);
627
+ return effectiveRole === role;
628
+ }
629
+ /**
630
+ * 여러 역할 중 하나라도 가지고 있는지 확인하는 함수
631
+ * @param token NextAuth JWT 객체
632
+ * @param roles 확인할 역할 배열
633
+ * @param serviceId 서비스 ID (기본값: 'checkon')
634
+ * @returns 역할 보유 여부
635
+ */
636
+ function hasAnyRole(token, roles, serviceId = 'checkon') {
637
+ if (!token)
638
+ return false;
639
+ const effectiveRole = getEffectiveRole(token, serviceId);
640
+ return roles.includes(effectiveRole || '');
641
+ }
642
+ /**
643
+ * 공개 경로인지 확인하는 함수
644
+ * @param pathname 경로명
645
+ * @param publicPaths 공개 경로 배열
646
+ * @returns 공개 경로 여부
647
+ */
648
+ function isPublicPath(pathname, publicPaths) {
649
+ return publicPaths.some(path => pathname === path || pathname.startsWith(path));
650
+ }
651
+ /**
652
+ * API 경로인지 확인하는 함수
653
+ * @param pathname 경로명
654
+ * @returns API 경로 여부
655
+ */
656
+ function isApiPath(pathname) {
657
+ return pathname.startsWith('/api/');
658
+ }
659
+ /**
660
+ * 보호된 API 경로인지 확인하는 함수
661
+ * @param pathname 경로명
662
+ * @param exemptPaths 제외할 경로 배열
663
+ * @returns 보호된 API 경로 여부
664
+ */
665
+ function isProtectedApiPath(pathname, exemptPaths = []) {
666
+ if (!isApiPath(pathname))
667
+ return false;
668
+ return !exemptPaths.some(path => pathname.startsWith(path));
669
+ }
670
+ /**
671
+ * NextAuth 토큰과 자체 토큰을 모두 확인하는 통합 인증 체크 함수
672
+ * @param req NextRequest 객체
673
+ * @param secret JWT 서명에 사용할 secret key
674
+ * @param options 옵션
675
+ * @returns 인증 결과
676
+ */
677
+ async function checkAuthentication(req, secret, options) {
678
+ const { cookiePrefix = 'checkon', getNextAuthToken, } = options || {};
679
+ // 1. NextAuth 토큰 확인
680
+ let nextAuthToken = null;
681
+ if (getNextAuthToken) {
682
+ nextAuthToken = await getNextAuthToken(req);
683
+ }
684
+ if (nextAuthToken && isValidToken(nextAuthToken)) {
685
+ return {
686
+ isAuthenticated: true,
687
+ token: nextAuthToken,
688
+ };
689
+ }
690
+ // 2. 자체 토큰 확인
691
+ const authCheck = await verifyAndRefreshToken(req, secret, options);
692
+ if (authCheck.response) {
693
+ return {
694
+ isAuthenticated: true,
695
+ response: authCheck.response,
696
+ payload: authCheck.payload,
697
+ };
698
+ }
699
+ if (authCheck.isValid && authCheck.payload) {
700
+ return {
701
+ isAuthenticated: true,
702
+ payload: authCheck.payload,
703
+ };
704
+ }
705
+ return {
706
+ isAuthenticated: false,
707
+ error: authCheck.error || 'NO_TOKEN',
708
+ };
709
+ }