Files
invyone/frontend/lib/tenant/subdomain.ts
T
hjjeong b782bb298f merge: origin/gbpark-node → hjjeong (60 commits, 5 conflicts resolved)
충돌 해결 5개 파일:
- .gitignore: .envrc/.direnv (hjjeong direnv 셋업) + .omc/ (gbpark) 양쪽 보존
- docs/MULTI_TENANCY_ARCHITECTURE.md: *.localhost dev 분기 + *.invyone.com/solution.invyone.com 통합
- frontend/lib/api/client.ts: 1-b *.localhost:8081 dev + 1-c DEV_TENANT_HOST(nip.io):8083 + invyone.com 신 도메인
- frontend/lib/tenant/subdomain.ts: IPv4 차단 + *.invyone.com + DEV_TENANT_HOST + *.localhost 모두 처리
- frontend/app/(auth)/login/page.tsx: B안 채택 — buttons 항상 렌더, className 만 mounted 가드 (next-themes 표준 패턴)

검증:
- backend: ./gradlew compileJava 성공 (Java 21)
- frontend: 머지된 4개 파일 관련 타입 에러 0개

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:51:06 +09:00

73 lines
2.3 KiB
TypeScript

/**
* 테넌트 서브도메인 헬퍼.
* 메인 사이트(solution, www, admin 등 예약어) 와 베이스 도메인은 null 을 리턴해서
* TenantGuard 가 체크를 스킵하게 한다.
*
* 백엔드 SubdomainResolverFilter.extractSubdomain() 와 동일한 규칙을 따라야 함:
* - bare localhost / IP / 베이스 도메인 → null (메타)
* - {sub}.localhost (dev) → 첫 파트 (예약어 제외)
* - {sub}.invyone.com (운영) → 첫 파트 (예약어 제외)
*
* 백엔드 provisioning 의 RESERVED_SUBDOMAINS 와 같은 값을 유지할 것.
*/
const RESERVED_MAIN = new Set([
"solution",
"www",
"admin",
"api",
"app",
"static",
"assets",
"main",
"mail",
"blog",
"test",
"staging",
"prod",
"console",
]);
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
// 개발 환경 가짜 서브도메인 패턴: <prefix>.<IPv4>(.nip.io | .sslip.io)?
// 사무실 도커처럼 운영 와일드카드 DNS 가 없는 환경에서, hosts 매핑 또는 nip.io 외부 DNS 로 prefix 를 표현.
// backend SubdomainResolverFilter 도 동일 의도로 호스트 첫 라벨을 prefix 로 추출.
const DEV_TENANT_HOST = /^([a-z0-9-]+)\.\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:\.(?:nip|sslip)\.io)?$/;
export function extractTenantSubdomain(host: string): string | null {
if (!host) return null;
const cleanHost = host.split(":")[0].toLowerCase();
if (!cleanHost) return null;
if (cleanHost === "localhost") return null;
if (IPV4.test(cleanHost)) return null;
// 운영 *.invyone.com — 베이스 도메인은 null
if (cleanHost.endsWith(".invyone.com")) {
const prefix = cleanHost.substring(0, cleanHost.length - ".invyone.com".length);
if (!prefix) return null;
if (RESERVED_MAIN.has(prefix)) return null;
return prefix;
}
// dev 가짜 서브도메인 (<prefix>.<IPv4>(.nip.io|.sslip.io)?)
const devMatch = cleanHost.match(DEV_TENANT_HOST);
if (devMatch) {
const prefix = devMatch[1];
if (RESERVED_MAIN.has(prefix)) return null;
return prefix;
}
// dev *.localhost (RFC 6761) — bare "localhost" 는 위에서 제외됨
const parts = cleanHost.split(".");
if (parts.length === 2 && parts[1] === "localhost") {
const first = parts[0];
if (!first) return null;
if (RESERVED_MAIN.has(first)) return null;
return first;
}
return null;
}