@thinkingcat/auth-utils 1.0.5 → 1.0.7

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.d.ts CHANGED
@@ -291,6 +291,29 @@ export interface RoleAccessConfig {
291
291
  allowedRoles?: string[];
292
292
  };
293
293
  }
294
+ /**
295
+ * 미들웨어 설정 인터페이스
296
+ */
297
+ export interface MiddlewareConfig {
298
+ /** 공개 접근 가능한 경로 배열 */
299
+ publicPaths?: string[];
300
+ /** 구독이 필요한 경로 배열 */
301
+ subscriptionRequiredPaths?: string[];
302
+ /** 구독 상태 확인을 제외할 API 경로 배열 */
303
+ subscriptionExemptApiPaths?: string[];
304
+ /** 인증 관련 API 경로 배열 */
305
+ authApiPaths?: string[];
306
+ /** 역할별 대시보드 경로 객체 */
307
+ rolePaths?: Record<string, string>;
308
+ /** 역할 기반 접근 제어 설정 */
309
+ roleAccessConfig?: RoleAccessConfig;
310
+ /** 서비스 ID */
311
+ serviceId?: string;
312
+ /** 시스템 관리자 역할명 (기본값: 'SYSTEM_ADMIN') */
313
+ systemAdminRole?: string;
314
+ /** 에러 페이지 경로 (기본값: '/error') */
315
+ errorPath?: string;
316
+ }
294
317
  export declare function checkRoleAccess(pathname: string, role: string, roleConfig: RoleAccessConfig): {
295
318
  allowed: boolean;
296
319
  message?: string;
@@ -404,3 +427,18 @@ export declare function verifyAndRefreshTokenWithNextAuth(req: NextRequest, next
404
427
  error?: string;
405
428
  payload?: JWTPayload;
406
429
  }>;
430
+ /**
431
+ * 기본 미들웨어 설정을 생성하는 함수
432
+ * @param config 커스텀 설정 (선택사항)
433
+ * @returns 미들웨어 설정 객체
434
+ */
435
+ export declare function createMiddlewareConfig(config?: Partial<MiddlewareConfig>): Required<Omit<MiddlewareConfig, 'serviceId'>> & Pick<MiddlewareConfig, 'serviceId'>;
436
+ /**
437
+ * 통합 미들웨어 핸들러 함수
438
+ * 모든 인증, 권한, 구독 체크를 포함한 완전한 미들웨어 로직
439
+ * @param req NextRequest 객체
440
+ * @param config 미들웨어 설정
441
+ * @param getNextAuthToken NextAuth 토큰을 가져오는 함수
442
+ * @returns NextResponse 또는 null (다음 미들웨어로 진행)
443
+ */
444
+ export declare function handleMiddleware(req: NextRequest, config: Required<Omit<MiddlewareConfig, 'serviceId'>> & Pick<MiddlewareConfig, 'serviceId'>, getNextAuthToken: (req: NextRequest) => Promise<JWT | null>): Promise<NextResponse | null>;
package/dist/index.js CHANGED
@@ -28,6 +28,8 @@ exports.isApiPath = isApiPath;
28
28
  exports.isProtectedApiPath = isProtectedApiPath;
29
29
  exports.checkAuthentication = checkAuthentication;
30
30
  exports.verifyAndRefreshTokenWithNextAuth = verifyAndRefreshTokenWithNextAuth;
31
+ exports.createMiddlewareConfig = createMiddlewareConfig;
32
+ exports.handleMiddleware = handleMiddleware;
31
33
  const jwt_1 = require("next-auth/jwt");
32
34
  const jose_1 = require("jose");
33
35
  const server_1 = require("next/server");
@@ -726,3 +728,220 @@ async function verifyAndRefreshTokenWithNextAuth(req, nextAuthToken, secret, opt
726
728
  const authCheck = await verifyAndRefreshToken(req, secret, options);
727
729
  return authCheck;
728
730
  }
731
+ /**
732
+ * 기본 미들웨어 설정을 생성하는 함수
733
+ * @param config 커스텀 설정 (선택사항)
734
+ * @returns 미들웨어 설정 객체
735
+ */
736
+ function createMiddlewareConfig(config) {
737
+ const defaultConfig = {
738
+ publicPaths: [
739
+ '/robots.txt',
740
+ '/sitemap.xml',
741
+ '/ads.txt',
742
+ '/images',
743
+ '/login',
744
+ '/register',
745
+ '/forgot-password',
746
+ '/reset-password',
747
+ '/about',
748
+ '/pricing',
749
+ '/support',
750
+ '/terms',
751
+ '/privacy',
752
+ '/refund',
753
+ '/error',
754
+ ],
755
+ subscriptionRequiredPaths: [
756
+ '/admin',
757
+ '/teacher',
758
+ '/student',
759
+ '/personal',
760
+ ],
761
+ subscriptionExemptApiPaths: [
762
+ '/api/auth',
763
+ '/api/sso',
764
+ '/api/admin/subscription',
765
+ '/api/admin/payment-methods',
766
+ ],
767
+ authApiPaths: [
768
+ '/api/auth/send-verification',
769
+ '/api/auth/verify-code',
770
+ '/api/auth/verify-user',
771
+ '/api/auth/select-academy',
772
+ ],
773
+ rolePaths: {
774
+ SYSTEM_ADMIN: '/system',
775
+ ADMIN: '/admin',
776
+ TEACHER: '/teacher',
777
+ STUDENT: '/student',
778
+ },
779
+ roleAccessConfig: {},
780
+ systemAdminRole: 'SYSTEM_ADMIN',
781
+ errorPath: '/error',
782
+ serviceId: config?.serviceId,
783
+ };
784
+ // 커스텀 설정으로 병합
785
+ return {
786
+ ...defaultConfig,
787
+ ...config,
788
+ publicPaths: config?.publicPaths || defaultConfig.publicPaths,
789
+ subscriptionRequiredPaths: config?.subscriptionRequiredPaths || defaultConfig.subscriptionRequiredPaths,
790
+ subscriptionExemptApiPaths: config?.subscriptionExemptApiPaths || defaultConfig.subscriptionExemptApiPaths,
791
+ authApiPaths: config?.authApiPaths || defaultConfig.authApiPaths,
792
+ rolePaths: config?.rolePaths || defaultConfig.rolePaths,
793
+ roleAccessConfig: config?.roleAccessConfig || defaultConfig.roleAccessConfig,
794
+ };
795
+ }
796
+ /**
797
+ * 통합 미들웨어 핸들러 함수
798
+ * 모든 인증, 권한, 구독 체크를 포함한 완전한 미들웨어 로직
799
+ * @param req NextRequest 객체
800
+ * @param config 미들웨어 설정
801
+ * @param getNextAuthToken NextAuth 토큰을 가져오는 함수
802
+ * @returns NextResponse 또는 null (다음 미들웨어로 진행)
803
+ */
804
+ async function handleMiddleware(req, config, getNextAuthToken) {
805
+ try {
806
+ const pathname = req.nextUrl.pathname;
807
+ const secret = process.env.NEXTAUTH_SECRET;
808
+ const isProduction = process.env.NODE_ENV === 'production';
809
+ const cookieDomain = process.env.COOKIE_DOMAIN;
810
+ const serviceId = config.serviceId || 'checkon';
811
+ const cookiePrefix = serviceId;
812
+ const token = await getNextAuthToken(req);
813
+ const effectiveRole = getEffectiveRole(token, serviceId);
814
+ // 1. API 요청 처리
815
+ if (pathname.startsWith('/api/')) {
816
+ if (config.authApiPaths.includes(pathname)) {
817
+ return server_1.NextResponse.next();
818
+ }
819
+ if (config.subscriptionExemptApiPaths.some((path) => pathname.startsWith(path))) {
820
+ return server_1.NextResponse.next();
821
+ }
822
+ const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
823
+ cookiePrefix,
824
+ isProduction,
825
+ cookieDomain,
826
+ text: serviceId,
827
+ });
828
+ if (authCheck.response) {
829
+ return authCheck.response;
830
+ }
831
+ if (!authCheck.isValid) {
832
+ const response = redirectToError(req, 'UNAUTHORIZED', '인증이 필요합니다.', config.errorPath);
833
+ return clearAuthCookies(response, cookiePrefix);
834
+ }
835
+ return server_1.NextResponse.next();
836
+ }
837
+ // 2. 루트 경로 처리 - SSO 토큰 처리 (인증 체크보다 먼저!)
838
+ if (pathname === '/') {
839
+ const tokenParam = req.nextUrl.searchParams.get('token');
840
+ if (tokenParam) {
841
+ try {
842
+ // 1. 토큰 검증
843
+ const tokenResult = await verifyToken(tokenParam, secret);
844
+ if (!tokenResult) {
845
+ throw new Error('Invalid token');
846
+ }
847
+ const { payload } = tokenResult;
848
+ // 2. 역할 추출
849
+ const defaultRole = Object.keys(config.rolePaths)[0] || 'ADMIN';
850
+ const tokenRole = extractRoleFromPayload(payload, serviceId, defaultRole);
851
+ // 3. Refresh token 가져오기 (서버 간 통신)
852
+ const userId = payload.sub || payload.userId || '';
853
+ const refreshToken = await getRefreshTokenFromSSO(userId, tokenParam) || '';
854
+ // 4. 자체 토큰 생성 및 쿠키 설정
855
+ const redirectPath = config.rolePaths[tokenRole] || config.rolePaths[defaultRole] || '/admin';
856
+ const response = await createAuthResponse(tokenParam, secret, {
857
+ refreshToken: refreshToken || undefined,
858
+ redirectPath,
859
+ text: serviceId,
860
+ cookiePrefix,
861
+ isProduction,
862
+ cookieDomain,
863
+ });
864
+ return response;
865
+ }
866
+ catch {
867
+ // 토큰 검증 실패 시 SSO 로그인으로 리다이렉트
868
+ return redirectToSSOLogin(req, serviceId);
869
+ }
870
+ }
871
+ // 토큰이 없고 이미 인증된 경우 역할별 대시보드로 리다이렉트
872
+ if (token && effectiveRole) {
873
+ return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
874
+ }
875
+ // 인증되지 않은 경우 SSO 로그인 페이지로 리다이렉트
876
+ return redirectToSSOLogin(req, serviceId);
877
+ }
878
+ // 3. 공개 경로 처리
879
+ if (config.publicPaths.some((path) => pathname === path || pathname.startsWith(path))) {
880
+ if (pathname === '/error' || pathname === '/verification') {
881
+ return server_1.NextResponse.next();
882
+ }
883
+ if (token && effectiveRole) {
884
+ return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
885
+ }
886
+ return server_1.NextResponse.next();
887
+ }
888
+ // 4. 인증 체크
889
+ const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
890
+ cookiePrefix,
891
+ isProduction,
892
+ cookieDomain,
893
+ text: serviceId,
894
+ });
895
+ if (authCheck.response) {
896
+ return authCheck.response;
897
+ }
898
+ if (!authCheck.isValid) {
899
+ return redirectToSSOLogin(req, serviceId);
900
+ }
901
+ // 5. 토큰 확인 및 변환
902
+ let finalToken = token || await getNextAuthToken(req);
903
+ // verifyAndRefreshToken이 성공했는데 NextAuth 토큰이 없으면, 자체 토큰을 사용
904
+ if (!finalToken && authCheck.isValid) {
905
+ const accessToken = req.cookies.get(`${cookiePrefix}_access_token`)?.value;
906
+ if (accessToken) {
907
+ const tokenResult = await verifyToken(accessToken, secret);
908
+ if (tokenResult) {
909
+ const { payload } = tokenResult;
910
+ finalToken = createNextAuthJWT(payload, true); // academies 포함
911
+ }
912
+ }
913
+ }
914
+ if (!finalToken) {
915
+ return redirectToSSOLogin(req, serviceId);
916
+ }
917
+ // 6. 토큰 에러 체크
918
+ if (finalToken.error === "RefreshAccessTokenError") {
919
+ return redirectToSSOLogin(req, serviceId);
920
+ }
921
+ // 7. 토큰 유효성 체크
922
+ if (!finalToken.role || !finalToken.email) {
923
+ return redirectToSSOLogin(req, serviceId);
924
+ }
925
+ // 8. 역할 기반 접근 제어
926
+ const finalEffectiveRole = finalToken.role || getEffectiveRole(finalToken, serviceId) || effectiveRole || '';
927
+ if (config.roleAccessConfig && Object.keys(config.roleAccessConfig).length > 0 && finalEffectiveRole) {
928
+ const roleCheck = checkRoleAccess(pathname, finalEffectiveRole, config.roleAccessConfig);
929
+ if (!roleCheck.allowed) {
930
+ return redirectToError(req, 'ACCESS_DENIED', roleCheck.message || '접근 권한이 없습니다.', config.errorPath);
931
+ }
932
+ }
933
+ // 9. 구독 상태 확인 (시스템 관리자 제외)
934
+ if (finalEffectiveRole && requiresSubscription(pathname, finalEffectiveRole, config.subscriptionRequiredPaths, config.systemAdminRole)) {
935
+ const services = finalToken.services || [];
936
+ const subscriptionCheck = validateServiceSubscription(services, serviceId);
937
+ if (!subscriptionCheck.isValid) {
938
+ return server_1.NextResponse.redirect(subscriptionCheck.redirectUrl);
939
+ }
940
+ }
941
+ return null; // 다음 미들웨어로 진행
942
+ }
943
+ catch (error) {
944
+ console.error('Middleware error:', error);
945
+ return redirectToError(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
946
+ }
947
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkingcat/auth-utils",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Authentication utilities for ThinkingCat SSO services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",