nestjs-tenant-shield 0.1.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/LICENSE +21 -0
- package/README.md +612 -0
- package/dist/cache/cache.registry.d.ts +19 -0
- package/dist/cache/cache.registry.d.ts.map +1 -0
- package/dist/cache/cache.registry.js +51 -0
- package/dist/cache/cache.registry.js.map +1 -0
- package/dist/cache/cache.service.d.ts +44 -0
- package/dist/cache/cache.service.d.ts.map +1 -0
- package/dist/cache/cache.service.js +64 -0
- package/dist/cache/cache.service.js.map +1 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +19 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/constants/index.d.ts +64 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +67 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/context/get-current-tenant-id.d.ts +30 -0
- package/dist/context/get-current-tenant-id.d.ts.map +1 -0
- package/dist/context/get-current-tenant-id.js +40 -0
- package/dist/context/get-current-tenant-id.js.map +1 -0
- package/dist/context/index.d.ts +7 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +23 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/run-with-tenant.d.ts +84 -0
- package/dist/context/run-with-tenant.d.ts.map +1 -0
- package/dist/context/run-with-tenant.js +95 -0
- package/dist/context/run-with-tenant.js.map +1 -0
- package/dist/context/tenant-context.storage.d.ts +43 -0
- package/dist/context/tenant-context.storage.d.ts.map +1 -0
- package/dist/context/tenant-context.storage.js +45 -0
- package/dist/context/tenant-context.storage.js.map +1 -0
- package/dist/decorators/cacheable.decorator.d.ts +27 -0
- package/dist/decorators/cacheable.decorator.d.ts.map +1 -0
- package/dist/decorators/cacheable.decorator.js +108 -0
- package/dist/decorators/cacheable.decorator.js.map +1 -0
- package/dist/decorators/index.d.ts +13 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +29 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/require-tenant.decorator.d.ts +41 -0
- package/dist/decorators/require-tenant.decorator.d.ts.map +1 -0
- package/dist/decorators/require-tenant.decorator.js +125 -0
- package/dist/decorators/require-tenant.decorator.js.map +1 -0
- package/dist/decorators/system-action.decorator.d.ts +39 -0
- package/dist/decorators/system-action.decorator.d.ts.map +1 -0
- package/dist/decorators/system-action.decorator.js +50 -0
- package/dist/decorators/system-action.decorator.js.map +1 -0
- package/dist/decorators/tenant-context.decorator.d.ts +33 -0
- package/dist/decorators/tenant-context.decorator.d.ts.map +1 -0
- package/dist/decorators/tenant-context.decorator.js +54 -0
- package/dist/decorators/tenant-context.decorator.js.map +1 -0
- package/dist/errors/cross-tenant-access.error.d.ts +29 -0
- package/dist/errors/cross-tenant-access.error.d.ts.map +1 -0
- package/dist/errors/cross-tenant-access.error.js +37 -0
- package/dist/errors/cross-tenant-access.error.js.map +1 -0
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +26 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/invalid-tenant-source.error.d.ts +20 -0
- package/dist/errors/invalid-tenant-source.error.d.ts.map +1 -0
- package/dist/errors/invalid-tenant-source.error.js +28 -0
- package/dist/errors/invalid-tenant-source.error.js.map +1 -0
- package/dist/errors/missing-tenant-context.error.d.ts +22 -0
- package/dist/errors/missing-tenant-context.error.d.ts.map +1 -0
- package/dist/errors/missing-tenant-context.error.js +32 -0
- package/dist/errors/missing-tenant-context.error.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/cacheable-options.interface.d.ts +37 -0
- package/dist/interfaces/cacheable-options.interface.d.ts.map +1 -0
- package/dist/interfaces/cacheable-options.interface.js +3 -0
- package/dist/interfaces/cacheable-options.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +13 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +30 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interfaces/require-tenant-options.interface.d.ts +23 -0
- package/dist/interfaces/require-tenant-options.interface.d.ts.map +1 -0
- package/dist/interfaces/require-tenant-options.interface.js +3 -0
- package/dist/interfaces/require-tenant-options.interface.js.map +1 -0
- package/dist/interfaces/tenant-context-options.interface.d.ts +19 -0
- package/dist/interfaces/tenant-context-options.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-context-options.interface.js +3 -0
- package/dist/interfaces/tenant-context-options.interface.js.map +1 -0
- package/dist/interfaces/tenant-context.interface.d.ts +26 -0
- package/dist/interfaces/tenant-context.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-context.interface.js +3 -0
- package/dist/interfaces/tenant-context.interface.js.map +1 -0
- package/dist/interfaces/tenant-shield-options.interface.d.ts +141 -0
- package/dist/interfaces/tenant-shield-options.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-shield-options.interface.js +11 -0
- package/dist/interfaces/tenant-shield-options.interface.js.map +1 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/tenant-context.middleware.d.ts +30 -0
- package/dist/middleware/tenant-context.middleware.d.ts.map +1 -0
- package/dist/middleware/tenant-context.middleware.js +68 -0
- package/dist/middleware/tenant-context.middleware.js.map +1 -0
- package/dist/options/index.d.ts +2 -0
- package/dist/options/index.d.ts.map +1 -0
- package/dist/options/index.js +18 -0
- package/dist/options/index.js.map +1 -0
- package/dist/options/options.registry.d.ts +8 -0
- package/dist/options/options.registry.d.ts.map +1 -0
- package/dist/options/options.registry.js +36 -0
- package/dist/options/options.registry.js.map +1 -0
- package/dist/resolvers/custom.resolver.d.ts +29 -0
- package/dist/resolvers/custom.resolver.d.ts.map +1 -0
- package/dist/resolvers/custom.resolver.js +47 -0
- package/dist/resolvers/custom.resolver.js.map +1 -0
- package/dist/resolvers/header.resolver.d.ts +22 -0
- package/dist/resolvers/header.resolver.d.ts.map +1 -0
- package/dist/resolvers/header.resolver.js +39 -0
- package/dist/resolvers/header.resolver.js.map +1 -0
- package/dist/resolvers/index.d.ts +13 -0
- package/dist/resolvers/index.d.ts.map +1 -0
- package/dist/resolvers/index.js +29 -0
- package/dist/resolvers/index.js.map +1 -0
- package/dist/resolvers/jwt.resolver.d.ts +35 -0
- package/dist/resolvers/jwt.resolver.d.ts.map +1 -0
- package/dist/resolvers/jwt.resolver.js +51 -0
- package/dist/resolvers/jwt.resolver.js.map +1 -0
- package/dist/resolvers/resolver.factory.d.ts +12 -0
- package/dist/resolvers/resolver.factory.d.ts.map +1 -0
- package/dist/resolvers/resolver.factory.js +43 -0
- package/dist/resolvers/resolver.factory.js.map +1 -0
- package/dist/resolvers/subdomain.resolver.d.ts +37 -0
- package/dist/resolvers/subdomain.resolver.d.ts.map +1 -0
- package/dist/resolvers/subdomain.resolver.js +57 -0
- package/dist/resolvers/subdomain.resolver.js.map +1 -0
- package/dist/resolvers/tenant-resolver.interface.d.ts +22 -0
- package/dist/resolvers/tenant-resolver.interface.d.ts.map +1 -0
- package/dist/resolvers/tenant-resolver.interface.js +3 -0
- package/dist/resolvers/tenant-resolver.interface.js.map +1 -0
- package/dist/tenant-shield.module.d.ts +88 -0
- package/dist/tenant-shield.module.d.ts.map +1 -0
- package/dist/tenant-shield.module.js +263 -0
- package/dist/tenant-shield.module.js.map +1 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/test-helpers.d.ts +52 -0
- package/dist/testing/test-helpers.d.ts.map +1 -0
- package/dist/testing/test-helpers.js +72 -0
- package/dist/testing/test-helpers.js.map +1 -0
- package/dist/typeorm/index.d.ts +10 -0
- package/dist/typeorm/index.d.ts.map +1 -0
- package/dist/typeorm/index.js +26 -0
- package/dist/typeorm/index.js.map +1 -0
- package/dist/typeorm/raw-sql.helper.d.ts +35 -0
- package/dist/typeorm/raw-sql.helper.d.ts.map +1 -0
- package/dist/typeorm/raw-sql.helper.js +24 -0
- package/dist/typeorm/raw-sql.helper.js.map +1 -0
- package/dist/typeorm/tenant.subscriber.d.ts +61 -0
- package/dist/typeorm/tenant.subscriber.d.ts.map +1 -0
- package/dist/typeorm/tenant.subscriber.js +487 -0
- package/dist/typeorm/tenant.subscriber.js.map +1 -0
- package/package.json +109 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────
|
|
3
|
+
* TenantShieldModule.forRoot()에 넘기는 전역 설정 인터페이스.
|
|
4
|
+
*
|
|
5
|
+
* 라이브러리 사용자가 앱 시작 시 단 한 번 호출하며, 이 옵션이
|
|
6
|
+
* 모든 데코레이터/미들웨어/Subscriber의 동작을 결정합니다.
|
|
7
|
+
* ─────────────────────────────────────────────────────────────
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* tenant ID를 요청에서 추출하는 방식.
|
|
11
|
+
* 보통 SaaS의 인증 방식과 매칭됩니다.
|
|
12
|
+
*
|
|
13
|
+
* - 'header' : 모바일/내부 API에서 흔함. 예) X-Tenant-Id: academy-A
|
|
14
|
+
* - 'jwt' : 표준적인 토큰 기반 인증.
|
|
15
|
+
* - 'subdomain' : academy-a.yourapp.com 같은 멀티 도메인 SaaS.
|
|
16
|
+
* - 'custom' : 위 셋에 안 맞는 특수한 경우. 사용자가 함수를 직접 제공.
|
|
17
|
+
*/
|
|
18
|
+
export type TenantSource = 'header' | 'jwt' | 'subdomain' | 'custom';
|
|
19
|
+
/**
|
|
20
|
+
* 멀티테넌시 격리 전략.
|
|
21
|
+
*
|
|
22
|
+
* v0.1에서는 'discriminator'만 구현됩니다.
|
|
23
|
+
* 나머지는 v0.2+에서 추가됩니다 (PRD §7 범위 제외 참조).
|
|
24
|
+
*/
|
|
25
|
+
export type TenantStrategy = 'discriminator' | 'schema' | 'database';
|
|
26
|
+
/**
|
|
27
|
+
* forRoot()에 전달하는 옵션.
|
|
28
|
+
*/
|
|
29
|
+
export interface TenantShieldOptions {
|
|
30
|
+
/**
|
|
31
|
+
* 멀티테넌시 패턴.
|
|
32
|
+
* - 'discriminator': 같은 테이블에 tenant_id 컬럼으로 구분 (v0.1)
|
|
33
|
+
* - 'schema' : Postgres schema 분리 (v0.2)
|
|
34
|
+
* - 'database' : DB-per-tenant (v0.2)
|
|
35
|
+
*/
|
|
36
|
+
strategy: TenantStrategy;
|
|
37
|
+
/**
|
|
38
|
+
* Entity에서 tenant를 식별하는 컬럼/필드 이름.
|
|
39
|
+
* 회사마다 컨벤션이 다르므로 사용자가 지정합니다.
|
|
40
|
+
* 예: 'tenantId' (일반), 'spaceId' (슈퍼러닝), 'orgId' (Slack 스타일)
|
|
41
|
+
*/
|
|
42
|
+
tenantIdField: string;
|
|
43
|
+
/**
|
|
44
|
+
* 요청에서 tenant ID를 어떻게 추출할지 지정.
|
|
45
|
+
* 아래 source 관련 옵션과 함께 사용.
|
|
46
|
+
*/
|
|
47
|
+
tenantSource: TenantSource;
|
|
48
|
+
/**
|
|
49
|
+
* tenantSource가 'header'일 때 사용할 헤더 이름.
|
|
50
|
+
* 기본값: 'x-tenant-id'
|
|
51
|
+
*/
|
|
52
|
+
headerName?: string;
|
|
53
|
+
/**
|
|
54
|
+
* tenantSource가 'jwt'일 때 사용할 JWT claim 이름.
|
|
55
|
+
* 기본값: 'tenant_id'
|
|
56
|
+
*
|
|
57
|
+
* 주의: JWT 디코딩 자체는 사용자의 인증 미들웨어 책임.
|
|
58
|
+
* 이 라이브러리는 request.user 또는 request.jwt에서 값만 꺼냅니다.
|
|
59
|
+
*/
|
|
60
|
+
jwtClaim?: string;
|
|
61
|
+
/**
|
|
62
|
+
* tenantSource가 'subdomain'일 때 사용할 도메인 패턴.
|
|
63
|
+
* 예: '*.yourapp.com' → 'academy-a.yourapp.com'에서 'academy-a' 추출.
|
|
64
|
+
*/
|
|
65
|
+
subdomainPattern?: string;
|
|
66
|
+
/**
|
|
67
|
+
* tenantSource가 'custom'일 때 사용자가 제공하는 추출 함수.
|
|
68
|
+
* null을 반환하면 tenant 컨텍스트가 설정되지 않은 것으로 간주.
|
|
69
|
+
*
|
|
70
|
+
* 예시:
|
|
71
|
+
* customResolver: (req) => req.user?.organizationId ?? null
|
|
72
|
+
*/
|
|
73
|
+
customResolver?: (request: unknown) => string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Strict mode 활성화 여부 (기본 true, 강력 권장).
|
|
76
|
+
*
|
|
77
|
+
* true : tenant 컨텍스트 없거나 cross-tenant 감지 시 즉시 throw.
|
|
78
|
+
* false : 경고 로그만 남기고 진행. 마이그레이션 중에만 임시 사용.
|
|
79
|
+
*
|
|
80
|
+
* 프로덕션에서는 절대 false로 두지 마세요. 데이터 누출 사고의 첫 단추.
|
|
81
|
+
*/
|
|
82
|
+
strictMode?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* 시스템 작업(@SystemAction) 허용 여부 (기본 false).
|
|
85
|
+
*
|
|
86
|
+
* true로 켜면 cron, 마이그레이션, 모니터링 같은 작업이
|
|
87
|
+
* tenant 컨텍스트 없이 실행될 수 있습니다.
|
|
88
|
+
*
|
|
89
|
+
* 켜더라도 메서드에 @SystemAction()을 명시적으로 붙여야 합니다.
|
|
90
|
+
*/
|
|
91
|
+
allowSystemActions?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* tenant 보호를 적용할 entity 클래스 목록.
|
|
94
|
+
*
|
|
95
|
+
* 미지정 시 라이브러리가 자동으로 tenantIdField를 가진 모든 entity를 감시.
|
|
96
|
+
* 명시하면 화이트리스트로 동작 (성능 + 안전성 둘 다 향상).
|
|
97
|
+
*/
|
|
98
|
+
entities?: Function[];
|
|
99
|
+
/**
|
|
100
|
+
* 캐시 백엔드 교체 슬롯.
|
|
101
|
+
*
|
|
102
|
+
* 미지정 시 InMemoryTenantAwareCacheService(단일 프로세스용)가 사용됩니다.
|
|
103
|
+
* 프로덕션에서는 Redis 등 외부 캐시 어댑터로 교체 권장.
|
|
104
|
+
*
|
|
105
|
+
* 세 가지 형태로 제공 가능 (NestJS provider와 동일한 패턴):
|
|
106
|
+
* - `useClass` : 클래스 자체. NestJS DI가 인스턴스 생성.
|
|
107
|
+
* - `useValue` : 이미 만들어진 인스턴스. 외부에서 만든 싱글톤.
|
|
108
|
+
* - `useFactory` : 비동기 셋업이 필요한 경우 (예: Redis client 초기화).
|
|
109
|
+
*
|
|
110
|
+
* 예시 (Redis 어댑터, 자세한 구현은 examples/redis-cache/ 참고):
|
|
111
|
+
*
|
|
112
|
+
* cache: {
|
|
113
|
+
* useFactory: (config: ConfigService) => new RedisCacheAdapter(config.get('REDIS_URL')),
|
|
114
|
+
* inject: [ConfigService],
|
|
115
|
+
* }
|
|
116
|
+
*/
|
|
117
|
+
cache?: TenantShieldCacheProvider;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* cache 슬롯에 들어갈 수 있는 세 가지 형태.
|
|
121
|
+
*
|
|
122
|
+
* 셋 중 정확히 하나만 채우는 것을 권장 (둘 이상 있으면 useFactory > useValue > useClass 순으로 우선).
|
|
123
|
+
*/
|
|
124
|
+
export interface TenantShieldCacheProvider {
|
|
125
|
+
useClass?: new (...args: any[]) => unknown;
|
|
126
|
+
useValue?: unknown;
|
|
127
|
+
useFactory?: (...args: any[]) => unknown | Promise<unknown>;
|
|
128
|
+
/** useFactory의 파라미터로 주입될 토큰들. */
|
|
129
|
+
inject?: any[];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* forRootAsync()용 옵션. (v0.1.x 패치에서 추가 예정)
|
|
133
|
+
*
|
|
134
|
+
* ConfigService 같은 비동기 의존성을 통해 옵션을 만들고 싶을 때 사용.
|
|
135
|
+
*/
|
|
136
|
+
export interface TenantShieldAsyncOptions {
|
|
137
|
+
imports?: any[];
|
|
138
|
+
useFactory: (...args: any[]) => Promise<TenantShieldOptions> | TenantShieldOptions;
|
|
139
|
+
inject?: any[];
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=tenant-shield-options.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-shield-options.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/tenant-shield-options.interface.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,KAAK,GAAG,WAAW,GAAG,QAAQ,CAAC;AAErE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,QAAQ,GAAG,UAAU,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,QAAQ,EAAE,cAAc,CAAC;IAEzB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,YAAY,EAAE,YAAY,CAAC;IAE3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IAErD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IAEtB;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,EAAE,yBAAyB,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,iCAAiC;IACjC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;IAChB,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,GAAG,mBAAmB,CAAC;IACnF,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;CAChB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ─────────────────────────────────────────────────────────────
|
|
4
|
+
* TenantShieldModule.forRoot()에 넘기는 전역 설정 인터페이스.
|
|
5
|
+
*
|
|
6
|
+
* 라이브러리 사용자가 앱 시작 시 단 한 번 호출하며, 이 옵션이
|
|
7
|
+
* 모든 데코레이터/미들웨어/Subscriber의 동작을 결정합니다.
|
|
8
|
+
* ─────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
//# sourceMappingURL=tenant-shield-options.interface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-shield-options.interface.js","sourceRoot":"","sources":["../../src/interfaces/tenant-shield-options.interface.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./tenant-context.middleware"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,8DAA4C"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { TenantShieldOptions } from '../interfaces/tenant-shield-options.interface';
|
|
3
|
+
/**
|
|
4
|
+
* ─────────────────────────────────────────────────────────────
|
|
5
|
+
* 모든 HTTP 요청의 진입점에서 동작하는 미들웨어.
|
|
6
|
+
*
|
|
7
|
+
* 역할:
|
|
8
|
+
* 1) Resolver를 통해 요청에서 tenant ID 추출
|
|
9
|
+
* 2) AsyncLocalStorage.run()으로 컨텍스트를 만들어 next() 호출
|
|
10
|
+
* 3) 이후 컨트롤러/서비스/Subscriber 어디서든 getCurrentTenantId()로 조회 가능
|
|
11
|
+
*
|
|
12
|
+
* 이 미들웨어가 동작해야 비로소 라이브러리의 모든 기능이 켜집니다.
|
|
13
|
+
* TenantShieldModule이 자동으로 모든 라우트('*')에 적용해 줍니다.
|
|
14
|
+
* ─────────────────────────────────────────────────────────────
|
|
15
|
+
*/
|
|
16
|
+
export declare class TenantContextMiddleware implements NestMiddleware {
|
|
17
|
+
private readonly options;
|
|
18
|
+
/**
|
|
19
|
+
* forRoot에서 만든 resolver를 한 번만 생성해 두고 재사용.
|
|
20
|
+
* (요청마다 새로 만들면 오버헤드)
|
|
21
|
+
*/
|
|
22
|
+
private readonly resolver;
|
|
23
|
+
constructor(options: TenantShieldOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Express 시그니처에 맞춘 미들웨어 함수.
|
|
26
|
+
* NestJS는 내부적으로 Express/Fastify 어댑터로 변환해 호출.
|
|
27
|
+
*/
|
|
28
|
+
use(req: unknown, _res: unknown, next: (err?: unknown) => void): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=tenant-context.middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-context.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/tenant-context.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AAMpF;;;;;;;;;;;;GAYG;AACH,qBACa,uBAAwB,YAAW,cAAc;IAU1D,OAAO,CAAC,QAAQ,CAAC,OAAO;IAT1B;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;gBAKvB,OAAO,EAAE,mBAAmB;IAO/C;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;CAuBtE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.TenantContextMiddleware = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const constants_1 = require("../constants");
|
|
18
|
+
const resolver_factory_1 = require("../resolvers/resolver.factory");
|
|
19
|
+
const tenant_context_storage_1 = require("../context/tenant-context.storage");
|
|
20
|
+
const missing_tenant_context_error_1 = require("../errors/missing-tenant-context.error");
|
|
21
|
+
/**
|
|
22
|
+
* ─────────────────────────────────────────────────────────────
|
|
23
|
+
* 모든 HTTP 요청의 진입점에서 동작하는 미들웨어.
|
|
24
|
+
*
|
|
25
|
+
* 역할:
|
|
26
|
+
* 1) Resolver를 통해 요청에서 tenant ID 추출
|
|
27
|
+
* 2) AsyncLocalStorage.run()으로 컨텍스트를 만들어 next() 호출
|
|
28
|
+
* 3) 이후 컨트롤러/서비스/Subscriber 어디서든 getCurrentTenantId()로 조회 가능
|
|
29
|
+
*
|
|
30
|
+
* 이 미들웨어가 동작해야 비로소 라이브러리의 모든 기능이 켜집니다.
|
|
31
|
+
* TenantShieldModule이 자동으로 모든 라우트('*')에 적용해 줍니다.
|
|
32
|
+
* ─────────────────────────────────────────────────────────────
|
|
33
|
+
*/
|
|
34
|
+
let TenantContextMiddleware = class TenantContextMiddleware {
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.options = options;
|
|
37
|
+
// 부팅 시 옵션을 보고 적절한 resolver 인스턴스 생성.
|
|
38
|
+
// 잘못된 설정이면 여기서 InvalidTenantSourceError가 던져져 앱 부팅 실패.
|
|
39
|
+
this.resolver = (0, resolver_factory_1.createTenantResolver)(options);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Express 시그니처에 맞춘 미들웨어 함수.
|
|
43
|
+
* NestJS는 내부적으로 Express/Fastify 어댑터로 변환해 호출.
|
|
44
|
+
*/
|
|
45
|
+
use(req, _res, next) {
|
|
46
|
+
// 1) 요청에서 tenant ID 추출 시도
|
|
47
|
+
const tenantId = this.resolver.resolve(req);
|
|
48
|
+
// 2) strict mode면 tenant 없을 때 즉시 차단
|
|
49
|
+
// (단, allowSystemActions가 켜져있으면 시스템 작업 가능성이 있으므로
|
|
50
|
+
// 일단 통과시키고 메서드 단의 @RequireTenant가 최종 판단하도록 위임)
|
|
51
|
+
const strictMode = this.options.strictMode !== false; // 기본 true
|
|
52
|
+
if (!tenantId && strictMode && !this.options.allowSystemActions) {
|
|
53
|
+
// 미들웨어 단에서 throw하면 NestJS Exception Filter가 처리.
|
|
54
|
+
return next(new missing_tenant_context_error_1.MissingTenantContextError(`요청에서 tenant ID를 추출하지 못했습니다. source=${this.options.tenantSource}`, this.options.tenantSource));
|
|
55
|
+
}
|
|
56
|
+
// 3) AsyncLocalStorage.run()으로 컨텍스트 활성화.
|
|
57
|
+
// next() 호출이 이 콜백 안에서 일어나야, 이후 모든 비동기 체인이
|
|
58
|
+
// 동일한 store를 공유합니다.
|
|
59
|
+
tenant_context_storage_1.tenantContextStorage.run({ tenantId, isSystemAction: false }, () => next());
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.TenantContextMiddleware = TenantContextMiddleware;
|
|
63
|
+
exports.TenantContextMiddleware = TenantContextMiddleware = __decorate([
|
|
64
|
+
(0, common_1.Injectable)(),
|
|
65
|
+
__param(0, (0, common_1.Inject)(constants_1.TENANT_SHIELD_OPTIONS)),
|
|
66
|
+
__metadata("design:paramtypes", [Object])
|
|
67
|
+
], TenantContextMiddleware);
|
|
68
|
+
//# sourceMappingURL=tenant-context.middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-context.middleware.js","sourceRoot":"","sources":["../../src/middleware/tenant-context.middleware.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoE;AACpE,4CAAqD;AAGrD,oEAAqE;AACrE,8EAAyE;AACzE,yFAAmF;AAEnF;;;;;;;;;;;;GAYG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAOlC,YAGmB,OAA4B;QAA5B,YAAO,GAAP,OAAO,CAAqB;QAE7C,oCAAoC;QACpC,sDAAsD;QACtD,IAAI,CAAC,QAAQ,GAAG,IAAA,uCAAoB,EAAC,OAAO,CAAC,CAAC;IAChD,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,GAAY,EAAE,IAAa,EAAE,IAA6B;QAC5D,0BAA0B;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE5C,oCAAoC;QACpC,oDAAoD;QACpD,mDAAmD;QACnD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,UAAU;QAChE,IAAI,CAAC,QAAQ,IAAI,UAAU,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC;YAChE,gDAAgD;YAChD,OAAO,IAAI,CACT,IAAI,wDAAyB,CAC3B,sCAAsC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EACjE,IAAI,CAAC,OAAO,CAAC,YAAY,CAC1B,CACF,CAAC;QACJ,CAAC;QAED,yCAAyC;QACzC,6CAA6C;QAC7C,uBAAuB;QACvB,6CAAoB,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9E,CAAC;CACF,CAAA;AA5CY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,mBAAU,GAAE;IAUR,WAAA,IAAA,eAAM,EAAC,iCAAqB,CAAC,CAAA;;GATrB,uBAAuB,CA4CnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/options/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./options.registry"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/options/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,qDAAmC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TenantShieldOptions } from '../interfaces/tenant-shield-options.interface';
|
|
2
|
+
/** 모듈 bootstrap 단계에서 1회 호출. */
|
|
3
|
+
export declare function setGlobalOptions(options: TenantShieldOptions): void;
|
|
4
|
+
/** wrapMethod가 런타임에 allowSystemActions 등을 읽기 위해 호출. */
|
|
5
|
+
export declare function getGlobalOptions(): TenantShieldOptions | undefined;
|
|
6
|
+
/** 테스트 격리용. 각 테스트 케이스 후 호출해서 오염 방지. */
|
|
7
|
+
export declare function resetGlobalOptions(): void;
|
|
8
|
+
//# sourceMappingURL=options.registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.registry.d.ts","sourceRoot":"","sources":["../../src/options/options.registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AAsBpF,+BAA+B;AAC/B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAEnE;AAED,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,mBAAmB,GAAG,SAAS,CAElE;AAED,uCAAuC;AACvC,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setGlobalOptions = setGlobalOptions;
|
|
4
|
+
exports.getGlobalOptions = getGlobalOptions;
|
|
5
|
+
exports.resetGlobalOptions = resetGlobalOptions;
|
|
6
|
+
/**
|
|
7
|
+
* ─────────────────────────────────────────────────────────────
|
|
8
|
+
* 글로벌 옵션 registry — @RequireTenant wrapMethod의 DI 우회로.
|
|
9
|
+
*
|
|
10
|
+
* 배경:
|
|
11
|
+
* wrapMethod는 메서드 디스크립터를 decoration 시점에 감싸기 때문에
|
|
12
|
+
* NestJS DI 컨테이너에서 forRoot 옵션을 꺼낼 방법이 없습니다.
|
|
13
|
+
*
|
|
14
|
+
* 해결: 모듈이 onApplicationBootstrap 시점에 옵션을 이 registry에 등록.
|
|
15
|
+
* wrapMethod 내부의 wrapped function은 런타임에 registry에서 읽어옵니다.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ 한계:
|
|
18
|
+
* - 한 프로세스에 하나의 forRoot 옵션만 가능 (대부분 SaaS에서는 OK)
|
|
19
|
+
* - 테스트 격리를 위해 resetGlobalOptions() 필요
|
|
20
|
+
*
|
|
21
|
+
* ─────────────────────────────────────────────────────────────
|
|
22
|
+
*/
|
|
23
|
+
let globalOptions;
|
|
24
|
+
/** 모듈 bootstrap 단계에서 1회 호출. */
|
|
25
|
+
function setGlobalOptions(options) {
|
|
26
|
+
globalOptions = options;
|
|
27
|
+
}
|
|
28
|
+
/** wrapMethod가 런타임에 allowSystemActions 등을 읽기 위해 호출. */
|
|
29
|
+
function getGlobalOptions() {
|
|
30
|
+
return globalOptions;
|
|
31
|
+
}
|
|
32
|
+
/** 테스트 격리용. 각 테스트 케이스 후 호출해서 오염 방지. */
|
|
33
|
+
function resetGlobalOptions() {
|
|
34
|
+
globalOptions = undefined;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=options.registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.registry.js","sourceRoot":"","sources":["../../src/options/options.registry.ts"],"names":[],"mappings":";;AAuBA,4CAEC;AAGD,4CAEC;AAGD,gDAEC;AAjCD;;;;;;;;;;;;;;;;GAgBG;AAEH,IAAI,aAA8C,CAAC;AAEnD,+BAA+B;AAC/B,SAAgB,gBAAgB,CAAC,OAA4B;IAC3D,aAAa,GAAG,OAAO,CAAC;AAC1B,CAAC;AAED,uDAAuD;AACvD,SAAgB,gBAAgB;IAC9B,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,uCAAuC;AACvC,SAAgB,kBAAkB;IAChC,aAAa,GAAG,SAAS,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TenantResolver } from './tenant-resolver.interface';
|
|
2
|
+
/**
|
|
3
|
+
* 사용자가 직접 제공한 함수를 그대로 호출해 tenant ID를 얻는 resolver.
|
|
4
|
+
*
|
|
5
|
+
* Header/JWT/Subdomain 셋 어디에도 안 맞는 특수한 경우를 위한 탈출구입니다.
|
|
6
|
+
*
|
|
7
|
+
* ─────────────────────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* [예시 — multi-source 인증]
|
|
10
|
+
*
|
|
11
|
+
* customResolver: (req) => {
|
|
12
|
+
* // 모바일 앱은 헤더로, 웹은 JWT로, 외부 통합은 API 키로 보낼 때
|
|
13
|
+
* return req.user?.organizationId
|
|
14
|
+
* ?? req.headers['x-tenant-id']
|
|
15
|
+
* ?? req.apiKey?.tenantId
|
|
16
|
+
* ?? null;
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* ─────────────────────────────────────────────────────────────
|
|
20
|
+
*/
|
|
21
|
+
export declare class CustomTenantResolver implements TenantResolver {
|
|
22
|
+
private readonly fn;
|
|
23
|
+
/**
|
|
24
|
+
* @param fn - 사용자가 forRoot에서 제공한 customResolver 함수.
|
|
25
|
+
*/
|
|
26
|
+
constructor(fn: (request: unknown) => string | null);
|
|
27
|
+
resolve(request: unknown): string | null;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=custom.resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom.resolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/custom.resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IAI7C,OAAO,CAAC,QAAQ,CAAC,EAAE;IAH/B;;OAEG;gBAC0B,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI;IAEpE,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI;CAazC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CustomTenantResolver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 사용자가 직접 제공한 함수를 그대로 호출해 tenant ID를 얻는 resolver.
|
|
6
|
+
*
|
|
7
|
+
* Header/JWT/Subdomain 셋 어디에도 안 맞는 특수한 경우를 위한 탈출구입니다.
|
|
8
|
+
*
|
|
9
|
+
* ─────────────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* [예시 — multi-source 인증]
|
|
12
|
+
*
|
|
13
|
+
* customResolver: (req) => {
|
|
14
|
+
* // 모바일 앱은 헤더로, 웹은 JWT로, 외부 통합은 API 키로 보낼 때
|
|
15
|
+
* return req.user?.organizationId
|
|
16
|
+
* ?? req.headers['x-tenant-id']
|
|
17
|
+
* ?? req.apiKey?.tenantId
|
|
18
|
+
* ?? null;
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* ─────────────────────────────────────────────────────────────
|
|
22
|
+
*/
|
|
23
|
+
class CustomTenantResolver {
|
|
24
|
+
/**
|
|
25
|
+
* @param fn - 사용자가 forRoot에서 제공한 customResolver 함수.
|
|
26
|
+
*/
|
|
27
|
+
constructor(fn) {
|
|
28
|
+
this.fn = fn;
|
|
29
|
+
}
|
|
30
|
+
resolve(request) {
|
|
31
|
+
// 사용자 함수 호출 후 결과 검증.
|
|
32
|
+
// 빈 문자열, 잘못된 타입은 null로 정규화.
|
|
33
|
+
try {
|
|
34
|
+
const result = this.fn(request);
|
|
35
|
+
if (typeof result !== 'string' || result.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// 사용자 함수가 throw해도 라이브러리 전체가 죽으면 안 됨.
|
|
41
|
+
// 추출 실패로 간주하고 미들웨어가 strict mode 정책에 따라 처리하도록 위임.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.CustomTenantResolver = CustomTenantResolver;
|
|
47
|
+
//# sourceMappingURL=custom.resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom.resolver.js","sourceRoot":"","sources":["../../src/resolvers/custom.resolver.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,oBAAoB;IAC/B;;OAEG;IACH,YAA6B,EAAuC;QAAvC,OAAE,GAAF,EAAE,CAAqC;IAAG,CAAC;IAExE,OAAO,CAAC,OAAgB;QACtB,qBAAqB;QACrB,4BAA4B;QAC5B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;YAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACnE,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,qCAAqC;YACrC,iDAAiD;YACjD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAnBD,oDAmBC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TenantResolver } from './tenant-resolver.interface';
|
|
2
|
+
/**
|
|
3
|
+
* HTTP 헤더에서 tenant ID를 추출하는 resolver.
|
|
4
|
+
*
|
|
5
|
+
* 가장 단순하고 흔한 방식. 클라이언트(웹/모바일/API)가
|
|
6
|
+
* 매 요청마다 X-Tenant-Id 같은 헤더를 보냅니다.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ 헤더 값은 클라이언트가 자유롭게 위조할 수 있습니다.
|
|
9
|
+
* 따라서 반드시 인증 미들웨어가 "이 사용자가 진짜 이 tenant 소속인지"
|
|
10
|
+
* 검증한 뒤에 이 resolver를 사용해야 합니다.
|
|
11
|
+
* (이 라이브러리는 격리만 담당하고, 인증 자체는 책임지지 않음)
|
|
12
|
+
*/
|
|
13
|
+
export declare class HeaderTenantResolver implements TenantResolver {
|
|
14
|
+
private readonly headerName;
|
|
15
|
+
/**
|
|
16
|
+
* @param headerName - 어떤 헤더 이름을 읽을지. 기본 'x-tenant-id'
|
|
17
|
+
* (HTTP 헤더 이름은 대소문자 무시이지만, 노드는 보통 소문자로 정규화)
|
|
18
|
+
*/
|
|
19
|
+
constructor(headerName?: string);
|
|
20
|
+
resolve(request: unknown): string | null;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=header.resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header.resolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/header.resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IAK7C,OAAO,CAAC,QAAQ,CAAC,UAAU;IAJvC;;;OAGG;gBAC0B,UAAU,GAAE,MAAsB;IAE/D,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI;CAgBzC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HeaderTenantResolver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* HTTP 헤더에서 tenant ID를 추출하는 resolver.
|
|
6
|
+
*
|
|
7
|
+
* 가장 단순하고 흔한 방식. 클라이언트(웹/모바일/API)가
|
|
8
|
+
* 매 요청마다 X-Tenant-Id 같은 헤더를 보냅니다.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ 헤더 값은 클라이언트가 자유롭게 위조할 수 있습니다.
|
|
11
|
+
* 따라서 반드시 인증 미들웨어가 "이 사용자가 진짜 이 tenant 소속인지"
|
|
12
|
+
* 검증한 뒤에 이 resolver를 사용해야 합니다.
|
|
13
|
+
* (이 라이브러리는 격리만 담당하고, 인증 자체는 책임지지 않음)
|
|
14
|
+
*/
|
|
15
|
+
class HeaderTenantResolver {
|
|
16
|
+
/**
|
|
17
|
+
* @param headerName - 어떤 헤더 이름을 읽을지. 기본 'x-tenant-id'
|
|
18
|
+
* (HTTP 헤더 이름은 대소문자 무시이지만, 노드는 보통 소문자로 정규화)
|
|
19
|
+
*/
|
|
20
|
+
constructor(headerName = 'x-tenant-id') {
|
|
21
|
+
this.headerName = headerName;
|
|
22
|
+
}
|
|
23
|
+
resolve(request) {
|
|
24
|
+
// Express/Fastify 모두 req.headers를 통해 헤더 dict에 접근 가능.
|
|
25
|
+
// 타입은 라이브러리 호환성을 위해 unknown으로 받고 안에서 좁힙니다.
|
|
26
|
+
const req = request;
|
|
27
|
+
// 헤더가 아예 없으면 null 반환.
|
|
28
|
+
const raw = req.headers?.[this.headerName.toLowerCase()];
|
|
29
|
+
if (!raw)
|
|
30
|
+
return null;
|
|
31
|
+
// 헤더는 같은 키로 여러 값이 올 수 있어 배열일 수 있음 → 첫 번째 값 사용.
|
|
32
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
33
|
+
// 빈 문자열도 null로 간주 (의미 있는 tenant ID로 보지 않음).
|
|
34
|
+
const trimmed = value?.trim();
|
|
35
|
+
return trimmed ? trimmed : null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.HeaderTenantResolver = HeaderTenantResolver;
|
|
39
|
+
//# sourceMappingURL=header.resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header.resolver.js","sourceRoot":"","sources":["../../src/resolvers/header.resolver.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;GAUG;AACH,MAAa,oBAAoB;IAC/B;;;OAGG;IACH,YAA6B,aAAqB,aAAa;QAAlC,eAAU,GAAV,UAAU,CAAwB;IAAG,CAAC;IAEnE,OAAO,CAAC,OAAgB;QACtB,qDAAqD;QACrD,2CAA2C;QAC3C,MAAM,GAAG,GAAG,OAAsE,CAAC;QAEnF,sBAAsB;QACtB,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QAEtB,+CAA+C;QAC/C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAEhD,4CAA4C;QAC5C,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;QAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAClC,CAAC;CACF;AAvBD,oDAuBC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver 관련 export.
|
|
3
|
+
*
|
|
4
|
+
* 일반 사용자는 직접 쓸 일이 거의 없습니다.
|
|
5
|
+
* 내부 모듈(미들웨어)이 factory로 인스턴스를 만들고 사용합니다.
|
|
6
|
+
*/
|
|
7
|
+
export * from './tenant-resolver.interface';
|
|
8
|
+
export * from './header.resolver';
|
|
9
|
+
export * from './jwt.resolver';
|
|
10
|
+
export * from './subdomain.resolver';
|
|
11
|
+
export * from './custom.resolver';
|
|
12
|
+
export * from './resolver.factory';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resolvers/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* Resolver 관련 export.
|
|
19
|
+
*
|
|
20
|
+
* 일반 사용자는 직접 쓸 일이 거의 없습니다.
|
|
21
|
+
* 내부 모듈(미들웨어)이 factory로 인스턴스를 만들고 사용합니다.
|
|
22
|
+
*/
|
|
23
|
+
__exportStar(require("./tenant-resolver.interface"), exports);
|
|
24
|
+
__exportStar(require("./header.resolver"), exports);
|
|
25
|
+
__exportStar(require("./jwt.resolver"), exports);
|
|
26
|
+
__exportStar(require("./subdomain.resolver"), exports);
|
|
27
|
+
__exportStar(require("./custom.resolver"), exports);
|
|
28
|
+
__exportStar(require("./resolver.factory"), exports);
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resolvers/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;;;GAKG;AACH,8DAA4C;AAC5C,oDAAkC;AAClC,iDAA+B;AAC/B,uDAAqC;AACrC,oDAAkC;AAClC,qDAAmC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TenantResolver } from './tenant-resolver.interface';
|
|
2
|
+
/**
|
|
3
|
+
* JWT 페이로드의 claim에서 tenant ID를 추출하는 resolver.
|
|
4
|
+
*
|
|
5
|
+
* 동작 전제:
|
|
6
|
+
* 사용자의 인증 미들웨어(예: Passport, @nestjs/jwt)가 이미
|
|
7
|
+
* JWT를 검증하고 페이로드를 request.user 또는 request.jwt에
|
|
8
|
+
* 넣어 둔 상태여야 합니다.
|
|
9
|
+
*
|
|
10
|
+
* 이 resolver는 그 객체에서 특정 키만 읽어옵니다.
|
|
11
|
+
* 즉, JWT 자체 검증은 이 라이브러리의 책임이 아님.
|
|
12
|
+
*
|
|
13
|
+
* ─────────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* [예시 JWT 페이로드]
|
|
16
|
+
*
|
|
17
|
+
* {
|
|
18
|
+
* "sub": "user-123",
|
|
19
|
+
* "tenant_id": "academy-A",
|
|
20
|
+
* "exp": 1700000000
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* → jwtClaim: 'tenant_id'로 설정하면 'academy-A'를 추출.
|
|
24
|
+
*
|
|
25
|
+
* ─────────────────────────────────────────────────────────────
|
|
26
|
+
*/
|
|
27
|
+
export declare class JwtTenantResolver implements TenantResolver {
|
|
28
|
+
private readonly claimName;
|
|
29
|
+
/**
|
|
30
|
+
* @param claimName - JWT 페이로드에서 읽을 키 이름. 기본 'tenant_id'.
|
|
31
|
+
*/
|
|
32
|
+
constructor(claimName?: string);
|
|
33
|
+
resolve(request: unknown): string | null;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=jwt.resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.resolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/jwt.resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IAI1C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAHtC;;OAEG;gBAC0B,SAAS,GAAE,MAAoB;IAE5D,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI;CAkBzC"}
|