@thinkingcat/auth-utils 1.0.6 → 1.0.9

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 CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.verifyToken = verifyToken;
4
37
  exports.extractRoleFromPayload = extractRoleFromPayload;
@@ -16,6 +49,7 @@ exports.redirectToError = redirectToError;
16
49
  exports.clearAuthCookies = clearAuthCookies;
17
50
  exports.getEffectiveRole = getEffectiveRole;
18
51
  exports.requiresSubscription = requiresSubscription;
52
+ exports.checkLicenseKey = checkLicenseKey;
19
53
  exports.checkRoleAccess = checkRoleAccess;
20
54
  exports.redirectToSSOLogin = redirectToSSOLogin;
21
55
  exports.redirectToRoleDashboard = redirectToRoleDashboard;
@@ -29,9 +63,11 @@ exports.isProtectedApiPath = isProtectedApiPath;
29
63
  exports.checkAuthentication = checkAuthentication;
30
64
  exports.verifyAndRefreshTokenWithNextAuth = verifyAndRefreshTokenWithNextAuth;
31
65
  exports.createMiddlewareConfig = createMiddlewareConfig;
66
+ exports.handleMiddleware = handleMiddleware;
32
67
  const jwt_1 = require("next-auth/jwt");
33
68
  const jose_1 = require("jose");
34
69
  const server_1 = require("next/server");
70
+ const crypto_1 = require("crypto");
35
71
  /**
36
72
  * 토큰 검증 및 디코딩
37
73
  * @param accessToken JWT access token
@@ -54,11 +90,11 @@ async function verifyToken(accessToken, secret) {
54
90
  /**
55
91
  * payload에서 역할 추출 (서비스별)
56
92
  * @param payload JWT payload
57
- * @param serviceId 서비스 ID (기본값: 'checkon')
93
+ * @param serviceId 서비스 ID (필수)
58
94
  * @param defaultRole 기본 역할 (기본값: 'ADMIN')
59
95
  * @returns 추출된 역할
60
96
  */
61
- function extractRoleFromPayload(payload, serviceId = 'checkon', defaultRole = 'ADMIN') {
97
+ function extractRoleFromPayload(payload, serviceId, defaultRole = 'ADMIN') {
62
98
  const services = payload.services || [];
63
99
  const service = services.find((s) => s.serviceId === serviceId);
64
100
  return service?.role || payload.role || defaultRole;
@@ -66,18 +102,19 @@ function extractRoleFromPayload(payload, serviceId = 'checkon', defaultRole = 'A
66
102
  /**
67
103
  * payload에서 NextAuth JWT 객체 생성
68
104
  * @param payload JWT payload
105
+ * @param serviceId 서비스 ID (필수)
69
106
  * @param includeAcademies academies 정보 포함 여부
70
107
  * @returns NextAuth JWT 객체
71
108
  */
72
- function createNextAuthJWT(payload, includeAcademies = false) {
109
+ function createNextAuthJWT(payload, serviceId, includeAcademies = false) {
73
110
  const services = payload.services || [];
74
- const checkonService = services.find((s) => s.serviceId === 'checkon');
75
- const effectiveRole = checkonService?.role || payload.role || 'ADMIN';
111
+ const service = services.find((s) => s.serviceId === serviceId);
112
+ const effectiveRole = service?.role || payload.role || 'ADMIN';
76
113
  const jwt = {
77
114
  id: (payload.id || payload.sub),
78
115
  email: payload.email,
79
116
  name: payload.name,
80
- role: effectiveRole, // Role enum 타입
117
+ role: effectiveRole, // Role enum 타입 (string으로 캐스팅)
81
118
  services: payload.services,
82
119
  academies: includeAcademies
83
120
  ? payload.academies || []
@@ -141,7 +178,10 @@ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options)
141
178
  if (typeof optionsOrRefreshToken === 'string') {
142
179
  // 기존 방식: refreshToken이 문자열로 전달된 경우
143
180
  refreshTokenValue = optionsOrRefreshToken;
144
- const { cookiePrefix: prefix = 'checkon', isProduction: prod = false, } = options || {};
181
+ const { cookiePrefix: prefix, isProduction: prod = false, } = options || {};
182
+ if (!prefix) {
183
+ throw new Error('cookiePrefix is required');
184
+ }
145
185
  cookiePrefix = prefix;
146
186
  isProduction = prod;
147
187
  }
@@ -149,7 +189,10 @@ function setCustomTokens(response, accessToken, optionsOrRefreshToken, options)
149
189
  // 새로운 방식: options 객체로 전달된 경우
150
190
  const opts = optionsOrRefreshToken || {};
151
191
  refreshTokenValue = opts.refreshToken;
152
- cookiePrefix = opts.cookiePrefix || 'checkon';
192
+ if (!opts.cookiePrefix) {
193
+ throw new Error('cookiePrefix is required');
194
+ }
195
+ cookiePrefix = opts.cookiePrefix;
153
196
  isProduction = opts.isProduction || false;
154
197
  }
155
198
  // access_token 설정
@@ -203,10 +246,10 @@ function setNextAuthToken(response, sessionToken, options = {}) {
203
246
  /**
204
247
  * 리다이렉트용 HTML 생성
205
248
  * @param redirectPath 리다이렉트할 경로
206
- * @param text 표시할 텍스트 (기본값: 'checkon')
249
+ * @param text 표시할 텍스트 (필수)
207
250
  * @returns HTML 문자열
208
251
  */
209
- function createRedirectHTML(redirectPath, text = 'checkon') {
252
+ function createRedirectHTML(redirectPath, text) {
210
253
  return `
211
254
  <!DOCTYPE html>
212
255
  <html>
@@ -281,14 +324,15 @@ function createRedirectHTML(redirectPath, text = 'checkon') {
281
324
  * @param options 추가 옵션
282
325
  * @param options.refreshToken refresh token (선택)
283
326
  * @param options.redirectPath 리다이렉트할 경로 (기본값: 페이지 리로드)
284
- * @param options.text 리다이렉트 HTML에 표시할 텍스트 (기본값: 'checkon')
285
- * @param options.cookiePrefix 쿠키 이름 접두사 (기본값: 'checkon')
327
+ * @param options.text 리다이렉트 HTML에 표시할 텍스트 (선택사항)
328
+ * @param options.cookiePrefix 쿠키 이름 접두사 (필수)
286
329
  * @param options.isProduction 프로덕션 환경 여부 (기본값: false)
287
330
  * @param options.cookieDomain 쿠키 도메인 (선택)
288
331
  * @returns NextResponse 객체
289
332
  */
290
- async function createAuthResponse(accessToken, secret, options = {}) {
291
- const { refreshToken, redirectPath, text = 'checkon', cookiePrefix = 'checkon', isProduction = false, cookieDomain, } = options;
333
+ async function createAuthResponse(accessToken, secret, options) {
334
+ checkLicenseKey(options.licenseKey);
335
+ const { refreshToken, redirectPath, text, cookiePrefix, isProduction = false, cookieDomain, serviceId, } = options;
292
336
  // 1. 토큰 검증
293
337
  const tokenResult = await verifyToken(accessToken, secret);
294
338
  if (!tokenResult) {
@@ -296,14 +340,15 @@ async function createAuthResponse(accessToken, secret, options = {}) {
296
340
  }
297
341
  const { payload } = tokenResult;
298
342
  // 2. 역할 추출
299
- const role = extractRoleFromPayload(payload);
343
+ const role = extractRoleFromPayload(payload, serviceId);
300
344
  // 3. NextAuth JWT 생성 및 인코딩
301
- const jwt = createNextAuthJWT(payload);
345
+ const jwt = createNextAuthJWT(payload, serviceId);
302
346
  const sessionToken = await encodeNextAuthToken(jwt, secret);
303
347
  // 4. HTML 생성
348
+ const displayText = text || serviceId;
304
349
  const html = redirectPath
305
- ? createRedirectHTML(redirectPath, text)
306
- : createRedirectHTML('', text).replace("window.location.href = ''", "window.location.reload()");
350
+ ? createRedirectHTML(redirectPath, displayText)
351
+ : createRedirectHTML('', displayText).replace("window.location.href = ''", "window.location.reload()");
307
352
  // 5. Response 생성
308
353
  const response = new server_1.NextResponse(html, {
309
354
  status: 200,
@@ -338,21 +383,22 @@ async function createAuthResponse(accessToken, secret, options = {}) {
338
383
  * 서비스 구독 유효성 확인 함수
339
384
  * @param services 서비스 정보 배열
340
385
  * @param serviceId 확인할 서비스 ID
386
+ * @param ssoBaseURL SSO 서버 기본 URL (필수)
341
387
  * @returns 구독 유효성 결과
342
388
  */
343
- function validateServiceSubscription(services, serviceId) {
389
+ function validateServiceSubscription(services, serviceId, ssoBaseURL) {
344
390
  const filteredServices = services.filter(service => service.serviceId === serviceId);
345
391
  if (filteredServices.length === 0) {
346
392
  return {
347
393
  isValid: false,
348
- redirectUrl: `${process.env.SSO_AUTH_SERVER_URL || 'http://localhost:3000'}/services/${serviceId}?type=subscription_required`
394
+ redirectUrl: `${ssoBaseURL}/services/${serviceId}?type=subscription_required`
349
395
  };
350
396
  }
351
397
  const service = filteredServices[0];
352
398
  if (service.status !== "ACTIVE") {
353
399
  return {
354
400
  isValid: false,
355
- redirectUrl: `${process.env.SSO_AUTH_SERVER_URL || 'http://localhost:3000'}/services/${service.serviceId}?type=subscription_required`,
401
+ redirectUrl: `${ssoBaseURL}/services/${service.serviceId}?type=subscription_required`,
356
402
  service
357
403
  };
358
404
  }
@@ -365,13 +411,12 @@ function validateServiceSubscription(services, serviceId) {
365
411
  * SSO 서버에서 refresh token을 사용하여 새로운 access token을 발급받는 함수
366
412
  * @param refreshToken refresh token
367
413
  * @param options 옵션
368
- * @param options.ssoBaseURL SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
414
+ * @param options.ssoBaseURL SSO 서버 기본 URL (필수)
369
415
  * @param options.authServiceKey 인증 서비스 키 (기본값: 환경 변수)
370
416
  * @returns SSO refresh token 응답
371
417
  */
372
418
  async function refreshSSOToken(refreshToken, options) {
373
- const ssoBaseURL = options?.ssoBaseURL || process.env.NEXT_PUBLIC_SSO_BASE_URL || 'https://sso.thinkingcatworks.com';
374
- const authServiceKey = options?.authServiceKey || process.env.AUTH_SERVICE_SECRET_KEY;
419
+ const { ssoBaseURL, authServiceKey } = options;
375
420
  if (!authServiceKey) {
376
421
  throw new Error('AUTH_SERVICE_SECRET_KEY not configured');
377
422
  }
@@ -390,13 +435,12 @@ async function refreshSSOToken(refreshToken, options) {
390
435
  * @param userId 사용자 ID
391
436
  * @param accessToken access token
392
437
  * @param options 옵션
393
- * @param options.ssoBaseURL SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
438
+ * @param options.ssoBaseURL SSO 서버 기본 URL (필수)
394
439
  * @param options.authServiceKey 인증 서비스 키 (기본값: 환경 변수)
395
440
  * @returns refresh token 또는 null
396
441
  */
397
442
  async function getRefreshTokenFromSSO(userId, accessToken, options) {
398
- const ssoBaseURL = options?.ssoBaseURL || process.env.NEXT_PUBLIC_SSO_BASE_URL || 'https://sso.thinkingcatworks.com';
399
- const authServiceKey = options?.authServiceKey || process.env.AUTH_SERVICE_SECRET_KEY;
443
+ const { ssoBaseURL, authServiceKey } = options;
400
444
  if (!authServiceKey) {
401
445
  return null;
402
446
  }
@@ -422,56 +466,59 @@ async function getRefreshTokenFromSSO(userId, accessToken, options) {
422
466
  }
423
467
  return null;
424
468
  }
425
- /**
426
- * 토큰 검증 및 리프레시 처리 공통 함수
427
- *
428
- * @param req - NextRequest 객체
429
- * @param secret - JWT 서명에 사용할 secret key
430
- * @param options - 옵션
431
- * @param options.cookiePrefix - 쿠키 이름 접두사 (기본값: 'checkon')
432
- * @param options.isProduction - 프로덕션 환경 여부 (기본값: false)
433
- * @param options.cookieDomain - 쿠키 도메인 (선택)
434
- * @param options.text - 리다이렉트 HTML에 표시할 텍스트 (기본값: 'checkon')
435
- * @param options.ssoBaseURL - SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
436
- * @param options.authServiceKey - 인증 서비스 키 (기본값: 환경 변수)
437
- * @returns 검증 결과 및 필요시 리프레시된 응답
438
- */
439
469
  async function verifyAndRefreshToken(req, secret, options) {
440
- const { cookiePrefix = 'checkon', isProduction = false, cookieDomain, text = 'checkon', } = options || {};
470
+ const { cookiePrefix, serviceId, isProduction, cookieDomain, text, ssoBaseURL, authServiceKey, } = options;
441
471
  // 1. access_token 쿠키 확인
442
472
  const accessTokenName = `${cookiePrefix}_access_token`;
443
473
  const accessToken = req.cookies.get(accessTokenName)?.value;
444
474
  if (accessToken) {
445
- const tokenResult = await verifyToken(accessToken, secret);
446
- if (tokenResult) {
447
- return { isValid: true, payload: tokenResult.payload };
475
+ try {
476
+ const secretBytes = new TextEncoder().encode(secret);
477
+ const { payload } = await (0, jose_1.jwtVerify)(accessToken, secretBytes);
478
+ if (payload && typeof payload === 'object' && payload.email) {
479
+ return { isValid: true, payload: payload };
480
+ }
481
+ }
482
+ catch {
483
+ // 토큰 검증 실패
448
484
  }
449
- // 토큰이 만료되었거나 유효하지 않음 (리프레시 시도)
450
485
  }
451
- // 2. 리프레시 토큰으로 갱신 시도
486
+ // 리프레시 토큰으로 갱신 시도
452
487
  const refreshTokenName = `${cookiePrefix}_refresh_token`;
453
488
  const refreshToken = req.cookies.get(refreshTokenName)?.value;
454
489
  if (refreshToken) {
455
490
  try {
491
+ if (!ssoBaseURL || !authServiceKey) {
492
+ return { isValid: false, error: 'SSO_CONFIG_MISSING' };
493
+ }
456
494
  const refreshResult = await refreshSSOToken(refreshToken, {
457
- ssoBaseURL: options?.ssoBaseURL,
458
- authServiceKey: options?.authServiceKey,
495
+ ssoBaseURL,
496
+ authServiceKey,
459
497
  });
460
498
  if (refreshResult.success && refreshResult.accessToken) {
461
- // 토큰 갱신 성공 - createAuthResponse 사용
462
499
  const newRefreshToken = refreshResult.refreshToken || refreshToken;
463
500
  try {
501
+ let payload;
502
+ try {
503
+ const secretBytes = new TextEncoder().encode(secret);
504
+ const { payload: tokenPayload } = await (0, jose_1.jwtVerify)(refreshResult.accessToken, secretBytes);
505
+ if (tokenPayload && typeof tokenPayload === 'object' && tokenPayload.email) {
506
+ payload = tokenPayload;
507
+ }
508
+ }
509
+ catch {
510
+ // 토큰 검증 실패
511
+ }
464
512
  const response = await createAuthResponse(refreshResult.accessToken, secret, {
465
513
  refreshToken: newRefreshToken,
466
- redirectPath: '', // 리다이렉트 없이 현재 페이지 유지
467
- text,
514
+ redirectPath: '',
515
+ text: text || serviceId,
468
516
  cookiePrefix,
469
517
  isProduction,
470
518
  cookieDomain,
519
+ serviceId,
520
+ licenseKey: options.licenseKey,
471
521
  });
472
- // payload 추출을 위해 토큰 검증
473
- const tokenResult = await verifyToken(refreshResult.accessToken, secret);
474
- const payload = tokenResult?.payload;
475
522
  return { isValid: true, response, payload };
476
523
  }
477
524
  catch (error) {
@@ -507,10 +554,10 @@ function redirectToError(req, code, message, errorPath = '/error') {
507
554
  /**
508
555
  * 인증 쿠키를 삭제하는 헬퍼 함수
509
556
  * @param response NextResponse 객체
510
- * @param cookiePrefix 쿠키 이름 접두사 (기본값: 'checkon')
557
+ * @param cookiePrefix 쿠키 이름 접두사 (필수)
511
558
  * @returns NextResponse 객체
512
559
  */
513
- function clearAuthCookies(response, cookiePrefix = 'checkon') {
560
+ function clearAuthCookies(response, cookiePrefix) {
514
561
  response.cookies.delete(`${cookiePrefix}_access_token`);
515
562
  response.cookies.delete(`${cookiePrefix}_refresh_token`);
516
563
  return response;
@@ -518,10 +565,10 @@ function clearAuthCookies(response, cookiePrefix = 'checkon') {
518
565
  /**
519
566
  * JWT에서 서비스별 역할을 추출하는 헬퍼 함수
520
567
  * @param token NextAuth JWT 객체 또는 null
521
- * @param serviceId 서비스 ID (기본값: 'checkon')
568
+ * @param serviceId 서비스 ID (필수)
522
569
  * @returns 추출된 역할 또는 undefined
523
570
  */
524
- function getEffectiveRole(token, serviceId = 'checkon') {
571
+ function getEffectiveRole(token, serviceId) {
525
572
  if (!token)
526
573
  return undefined;
527
574
  // token이 이미 JWT 객체인 경우 role을 직접 사용
@@ -549,6 +596,19 @@ function requiresSubscription(pathname, role, subscriptionRequiredPaths, systemA
549
596
  // 구독이 필요한 경로인지 확인
550
597
  return subscriptionRequiredPaths.some(path => pathname.startsWith(path));
551
598
  }
599
+ // 유효한 라이센스 키 해시 목록
600
+ const VALID_LICENSE_KEY_HASHES = new Set([
601
+ '73bce4f3b64804c255cdab450d759a8b53038f9edb59ae42d9988b08dfd007e2',
602
+ ]);
603
+ function checkLicenseKey(licenseKey) {
604
+ if (!licenseKey || licenseKey.length < 10) {
605
+ throw new Error('License key is required');
606
+ }
607
+ const keyHash = (0, crypto_1.createHash)('sha256').update(licenseKey).digest('hex');
608
+ if (!VALID_LICENSE_KEY_HASHES.has(keyHash)) {
609
+ throw new Error('Invalid license key');
610
+ }
611
+ }
552
612
  function checkRoleAccess(pathname, role, roleConfig) {
553
613
  // 각 역할 설정을 확인
554
614
  for (const [configRole, config] of Object.entries(roleConfig)) {
@@ -569,12 +629,11 @@ function checkRoleAccess(pathname, role, roleConfig) {
569
629
  * SSO 로그인 페이지로 리다이렉트하는 헬퍼 함수
570
630
  * @param req NextRequest 객체
571
631
  * @param serviceId 서비스 ID
572
- * @param ssoBaseURL SSO 서버 기본 URL (기본값: 환경 변수 또는 기본값)
632
+ * @param ssoBaseURL SSO 서버 기본 URL (필수)
573
633
  * @returns NextResponse 리다이렉트 응답
574
634
  */
575
635
  function redirectToSSOLogin(req, serviceId, ssoBaseURL) {
576
- const baseURL = ssoBaseURL || process.env.NEXT_PUBLIC_SSO_BASE_URL || "https://sso.thinkingcatworks.com";
577
- return server_1.NextResponse.redirect(new URL(`${baseURL}/auth/login?serviceId=${serviceId}`, req.url));
636
+ return server_1.NextResponse.redirect(new URL(`${ssoBaseURL}/auth/login?serviceId=${serviceId}`, req.url));
578
637
  }
579
638
  /**
580
639
  * 역할별 대시보드 경로로 리다이렉트하는 헬퍼 함수
@@ -619,10 +678,10 @@ function isValidToken(token) {
619
678
  * 특정 역할을 가지고 있는지 확인하는 함수
620
679
  * @param token NextAuth JWT 객체
621
680
  * @param role 확인할 역할
622
- * @param serviceId 서비스 ID (기본값: 'checkon')
681
+ * @param serviceId 서비스 ID (필수)
623
682
  * @returns 역할 보유 여부
624
683
  */
625
- function hasRole(token, role, serviceId = 'checkon') {
684
+ function hasRole(token, role, serviceId) {
626
685
  if (!token)
627
686
  return false;
628
687
  const effectiveRole = getEffectiveRole(token, serviceId);
@@ -632,10 +691,10 @@ function hasRole(token, role, serviceId = 'checkon') {
632
691
  * 여러 역할 중 하나라도 가지고 있는지 확인하는 함수
633
692
  * @param token NextAuth JWT 객체
634
693
  * @param roles 확인할 역할 배열
635
- * @param serviceId 서비스 ID (기본값: 'checkon')
694
+ * @param serviceId 서비스 ID (필수)
636
695
  * @returns 역할 보유 여부
637
696
  */
638
- function hasAnyRole(token, roles, serviceId = 'checkon') {
697
+ function hasAnyRole(token, roles, serviceId) {
639
698
  if (!token)
640
699
  return false;
641
700
  const effectiveRole = getEffectiveRole(token, serviceId);
@@ -677,8 +736,7 @@ function isProtectedApiPath(pathname, exemptPaths = []) {
677
736
  * @returns 인증 결과
678
737
  */
679
738
  async function checkAuthentication(req, secret, options) {
680
- const { cookiePrefix = 'checkon', getNextAuthToken, } = options || {};
681
- // 1. NextAuth 토큰 확인
739
+ const { cookiePrefix, serviceId, getNextAuthToken, } = options;
682
740
  let nextAuthToken = null;
683
741
  if (getNextAuthToken) {
684
742
  nextAuthToken = await getNextAuthToken(req);
@@ -689,7 +747,6 @@ async function checkAuthentication(req, secret, options) {
689
747
  token: nextAuthToken,
690
748
  };
691
749
  }
692
- // 2. 자체 토큰 확인
693
750
  const authCheck = await verifyAndRefreshToken(req, secret, options);
694
751
  if (authCheck.response) {
695
752
  return {
@@ -719,76 +776,276 @@ async function checkAuthentication(req, secret, options) {
719
776
  * @returns 인증 결과
720
777
  */
721
778
  async function verifyAndRefreshTokenWithNextAuth(req, nextAuthToken, secret, options) {
722
- // 1. NextAuth 토큰이 있고 유효하면 통과
723
779
  if (nextAuthToken && isValidToken(nextAuthToken)) {
724
780
  return { isValid: true };
725
781
  }
726
- // 2. 자체 토큰 확인 및 리프레시
727
782
  const authCheck = await verifyAndRefreshToken(req, secret, options);
728
783
  return authCheck;
729
784
  }
730
785
  /**
731
786
  * 기본 미들웨어 설정을 생성하는 함수
732
- * @param config 커스텀 설정 (선택사항)
787
+ * @param config 커스텀 설정 (필수: serviceId 포함)
788
+ * @param defaults 기본 설정값 (선택사항, 제공하지 않으면 최소 기본값 사용)
733
789
  * @returns 미들웨어 설정 객체
734
790
  */
735
- function createMiddlewareConfig(config) {
736
- const defaultConfig = {
737
- publicPaths: [
738
- '/robots.txt',
739
- '/sitemap.xml',
740
- '/ads.txt',
741
- '/images',
742
- '/login',
743
- '/register',
744
- '/forgot-password',
745
- '/reset-password',
746
- '/about',
747
- '/pricing',
748
- '/support',
749
- '/terms',
750
- '/privacy',
751
- '/refund',
752
- '/error',
753
- ],
754
- subscriptionRequiredPaths: [
755
- '/admin',
756
- '/teacher',
757
- '/student',
758
- '/personal',
759
- ],
760
- subscriptionExemptApiPaths: [
761
- '/api/auth',
762
- '/api/sso',
763
- '/api/admin/subscription',
764
- '/api/admin/payment-methods',
765
- ],
766
- authApiPaths: [
767
- '/api/auth/send-verification',
768
- '/api/auth/verify-code',
769
- '/api/auth/verify-user',
770
- '/api/auth/select-academy',
771
- ],
772
- rolePaths: {
773
- SYSTEM_ADMIN: '/system',
774
- ADMIN: '/admin',
775
- TEACHER: '/teacher',
776
- STUDENT: '/student',
777
- },
778
- roleAccessConfig: {},
779
- systemAdminRole: 'SYSTEM_ADMIN',
780
- errorPath: '/error',
781
- serviceId: config?.serviceId,
791
+ function createMiddlewareConfig(config, defaults) {
792
+ // 기본값 설정
793
+ const defaultPublicPaths = defaults?.publicPaths || [
794
+ '/robots.txt',
795
+ '/sitemap.xml',
796
+ '/ads.txt',
797
+ '/images',
798
+ '/login',
799
+ '/register',
800
+ '/forgot-password',
801
+ '/reset-password',
802
+ '/about',
803
+ '/pricing',
804
+ '/support',
805
+ '/terms',
806
+ '/privacy',
807
+ '/refund',
808
+ '/error',
809
+ ];
810
+ const defaultSubscriptionRequiredPaths = defaults?.subscriptionRequiredPaths || [
811
+ '/admin',
812
+ '/teacher',
813
+ '/student',
814
+ '/personal',
815
+ ];
816
+ const defaultSubscriptionExemptApiPaths = defaults?.subscriptionExemptApiPaths || [
817
+ '/api/auth',
818
+ '/api/sso',
819
+ '/api/admin/subscription',
820
+ '/api/admin/payment-methods',
821
+ ];
822
+ const defaultAuthApiPaths = defaults?.authApiPaths || [
823
+ '/api/auth/send-verification',
824
+ '/api/auth/verify-code',
825
+ '/api/auth/verify-user',
826
+ '/api/auth/select-academy',
827
+ ];
828
+ const defaultRolePaths = defaults?.rolePaths || {
829
+ SYSTEM_ADMIN: '/system',
830
+ ADMIN: '/admin',
831
+ TEACHER: '/teacher',
832
+ STUDENT: '/student',
782
833
  };
834
+ const defaultSystemAdminRole = defaults?.systemAdminRole || 'SYSTEM_ADMIN';
835
+ const defaultErrorPath = defaults?.errorPath || '/error';
783
836
  // 커스텀 설정으로 병합
784
837
  return {
785
- ...defaultConfig,
786
- ...config,
787
- publicPaths: config?.publicPaths || defaultConfig.publicPaths,
788
- subscriptionRequiredPaths: config?.subscriptionRequiredPaths || defaultConfig.subscriptionRequiredPaths,
789
- subscriptionExemptApiPaths: config?.subscriptionExemptApiPaths || defaultConfig.subscriptionExemptApiPaths,
790
- authApiPaths: config?.authApiPaths || defaultConfig.authApiPaths,
791
- rolePaths: config?.rolePaths || defaultConfig.rolePaths,
792
- roleAccessConfig: config?.roleAccessConfig || defaultConfig.roleAccessConfig,
838
+ publicPaths: config.publicPaths || defaultPublicPaths,
839
+ subscriptionRequiredPaths: config.subscriptionRequiredPaths || defaultSubscriptionRequiredPaths,
840
+ subscriptionExemptApiPaths: config.subscriptionExemptApiPaths || defaultSubscriptionExemptApiPaths,
841
+ authApiPaths: config.authApiPaths || defaultAuthApiPaths,
842
+ rolePaths: config.rolePaths || defaultRolePaths,
843
+ roleAccessConfig: config.roleAccessConfig || {},
844
+ systemAdminRole: config.systemAdminRole || defaultSystemAdminRole,
845
+ errorPath: config.errorPath || defaultErrorPath,
846
+ serviceId: config.serviceId,
793
847
  };
794
848
  }
849
+ /**
850
+ * 통합 미들웨어 핸들러 함수
851
+ * 모든 인증, 권한, 구독 체크를 포함한 완전한 미들웨어 로직
852
+ * @param req NextRequest 객체
853
+ * @param config 미들웨어 설정
854
+ * @param options 미들웨어 실행 옵션 (secret 필수)
855
+ * @returns NextResponse 또는 null (다음 미들웨어로 진행)
856
+ */
857
+ async function handleMiddleware(req, config, options) {
858
+ try {
859
+ const pathname = req.nextUrl.pathname;
860
+ const { secret, isProduction, cookieDomain, getNextAuthToken, licenseKey } = options;
861
+ checkLicenseKey(licenseKey);
862
+ if (!config.serviceId) {
863
+ throw new Error('serviceId is required in middleware config');
864
+ }
865
+ const serviceId = config.serviceId;
866
+ const cookiePrefix = serviceId;
867
+ let token = null;
868
+ if (getNextAuthToken) {
869
+ token = await getNextAuthToken(req);
870
+ }
871
+ else {
872
+ // 기본적으로 secret을 사용하여 토큰 가져오기
873
+ try {
874
+ const { getToken } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
875
+ token = await getToken({ req, secret });
876
+ }
877
+ catch {
878
+ // NextAuth가 없으면 null 유지
879
+ }
880
+ }
881
+ const effectiveRole = getEffectiveRole(token, serviceId);
882
+ // 1. API 요청 처리
883
+ if (pathname.startsWith('/api/')) {
884
+ if (config.authApiPaths.includes(pathname)) {
885
+ return server_1.NextResponse.next();
886
+ }
887
+ if (config.subscriptionExemptApiPaths.some((path) => pathname.startsWith(path))) {
888
+ return server_1.NextResponse.next();
889
+ }
890
+ const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
891
+ cookiePrefix,
892
+ serviceId,
893
+ isProduction,
894
+ cookieDomain,
895
+ text: serviceId,
896
+ ssoBaseURL: options.ssoBaseURL,
897
+ authServiceKey: options.authServiceKey,
898
+ licenseKey: options.licenseKey,
899
+ });
900
+ if (authCheck.response) {
901
+ return authCheck.response;
902
+ }
903
+ if (!authCheck.isValid) {
904
+ const response = redirectToError(req, 'UNAUTHORIZED', '인증이 필요합니다.', config.errorPath);
905
+ return clearAuthCookies(response, cookiePrefix);
906
+ }
907
+ return server_1.NextResponse.next();
908
+ }
909
+ // 2. 루트 경로 처리 - SSO 토큰 처리 (인증 체크보다 먼저!)
910
+ if (pathname === '/') {
911
+ const tokenParam = req.nextUrl.searchParams.get('token');
912
+ if (tokenParam) {
913
+ try {
914
+ // 1. 토큰 검증
915
+ const tokenResult = await verifyToken(tokenParam, secret);
916
+ if (!tokenResult) {
917
+ throw new Error('Invalid token');
918
+ }
919
+ const { payload } = tokenResult;
920
+ // 2. 역할 추출
921
+ const defaultRole = Object.keys(config.rolePaths)[0] || 'ADMIN';
922
+ const tokenRole = extractRoleFromPayload(payload, serviceId, defaultRole);
923
+ // 3. Refresh token 가져오기 (서버 간 통신)
924
+ const userId = payload.sub || payload.userId || '';
925
+ const ssoBaseURL = options.ssoBaseURL;
926
+ const authServiceKey = options.authServiceKey;
927
+ const refreshToken = authServiceKey
928
+ ? await getRefreshTokenFromSSO(userId, tokenParam, { ssoBaseURL, authServiceKey }) || ''
929
+ : '';
930
+ // 4. 자체 토큰 생성 및 쿠키 설정
931
+ const redirectPath = config.rolePaths[tokenRole] || config.rolePaths[defaultRole] || '/admin';
932
+ const response = await createAuthResponse(tokenParam, secret, {
933
+ refreshToken: refreshToken || undefined,
934
+ redirectPath,
935
+ text: serviceId,
936
+ cookiePrefix,
937
+ isProduction,
938
+ cookieDomain,
939
+ serviceId,
940
+ licenseKey: options.licenseKey,
941
+ });
942
+ return response;
943
+ }
944
+ catch {
945
+ // 토큰 검증 실패 시 SSO 로그인으로 리다이렉트
946
+ const ssoBaseURL = options.ssoBaseURL;
947
+ return redirectToSSOLogin(req, serviceId, ssoBaseURL);
948
+ }
949
+ }
950
+ // 토큰이 없고 이미 인증된 경우 역할별 대시보드로 리다이렉트
951
+ if (token && effectiveRole) {
952
+ return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
953
+ }
954
+ // 인증되지 않은 경우 SSO 로그인 페이지로 리다이렉트
955
+ const ssoBaseURL = options.ssoBaseURL;
956
+ return redirectToSSOLogin(req, serviceId, ssoBaseURL);
957
+ }
958
+ // 3. 공개 경로 처리
959
+ if (config.publicPaths.some((path) => pathname === path || pathname.startsWith(path))) {
960
+ if (pathname === '/error' || pathname === '/verification') {
961
+ return server_1.NextResponse.next();
962
+ }
963
+ if (token && effectiveRole) {
964
+ return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
965
+ }
966
+ return server_1.NextResponse.next();
967
+ }
968
+ // 4. 인증 체크
969
+ const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
970
+ cookiePrefix,
971
+ serviceId,
972
+ isProduction,
973
+ cookieDomain,
974
+ text: serviceId,
975
+ ssoBaseURL: options.ssoBaseURL,
976
+ authServiceKey: options.authServiceKey,
977
+ licenseKey: options.licenseKey,
978
+ });
979
+ if (authCheck.response) {
980
+ return authCheck.response;
981
+ }
982
+ if (!authCheck.isValid) {
983
+ const ssoBaseURL = options.ssoBaseURL;
984
+ return redirectToSSOLogin(req, serviceId, ssoBaseURL);
985
+ }
986
+ // 5. 토큰 확인 및 변환
987
+ let finalToken = token;
988
+ if (!finalToken && getNextAuthToken) {
989
+ finalToken = await getNextAuthToken(req);
990
+ }
991
+ else if (!finalToken) {
992
+ try {
993
+ const { getToken } = await Promise.resolve().then(() => __importStar(require('next-auth/jwt')));
994
+ finalToken = await getToken({ req, secret });
995
+ }
996
+ catch {
997
+ // NextAuth가 없으면 null 유지
998
+ }
999
+ }
1000
+ // verifyAndRefreshToken이 성공했는데 NextAuth 토큰이 없으면, 자체 토큰을 사용
1001
+ if (!finalToken && authCheck.isValid) {
1002
+ const accessToken = req.cookies.get(`${cookiePrefix}_access_token`)?.value;
1003
+ if (accessToken) {
1004
+ const tokenResult = await verifyToken(accessToken, secret);
1005
+ if (tokenResult) {
1006
+ const { payload } = tokenResult;
1007
+ finalToken = createNextAuthJWT(payload, serviceId, true); // academies 포함
1008
+ }
1009
+ }
1010
+ }
1011
+ if (!finalToken) {
1012
+ const ssoBaseURL = options.ssoBaseURL;
1013
+ return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1014
+ }
1015
+ // 6. 토큰 에러 체크
1016
+ if (finalToken.error === "RefreshAccessTokenError") {
1017
+ const ssoBaseURL = options.ssoBaseURL;
1018
+ return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1019
+ }
1020
+ // 7. 토큰 유효성 체크
1021
+ if (!finalToken.role || !finalToken.email) {
1022
+ const ssoBaseURL = options.ssoBaseURL;
1023
+ return redirectToSSOLogin(req, serviceId, ssoBaseURL);
1024
+ }
1025
+ // 8. 역할 기반 접근 제어
1026
+ const finalEffectiveRole = finalToken.role || getEffectiveRole(finalToken, serviceId) || effectiveRole || '';
1027
+ if (config.roleAccessConfig && Object.keys(config.roleAccessConfig).length > 0 && finalEffectiveRole) {
1028
+ const roleCheck = checkRoleAccess(pathname, finalEffectiveRole, config.roleAccessConfig);
1029
+ if (!roleCheck.allowed) {
1030
+ return redirectToError(req, 'ACCESS_DENIED', roleCheck.message || '접근 권한이 없습니다.', config.errorPath);
1031
+ }
1032
+ }
1033
+ // 9. 구독 상태 확인 (시스템 관리자 제외)
1034
+ if (finalEffectiveRole && requiresSubscription(pathname, finalEffectiveRole, config.subscriptionRequiredPaths, config.systemAdminRole)) {
1035
+ const services = finalToken.services || [];
1036
+ const ssoBaseURL = options.ssoBaseURL;
1037
+ if (!ssoBaseURL) {
1038
+ throw new Error('ssoBaseURL is required in middleware options');
1039
+ }
1040
+ const subscriptionCheck = validateServiceSubscription(services, serviceId, ssoBaseURL);
1041
+ if (!subscriptionCheck.isValid) {
1042
+ return server_1.NextResponse.redirect(subscriptionCheck.redirectUrl);
1043
+ }
1044
+ }
1045
+ return null; // 다음 미들웨어로 진행
1046
+ }
1047
+ catch (error) {
1048
+ console.error('Middleware error:', error);
1049
+ return redirectToError(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
1050
+ }
1051
+ }