[cs] refactor: auth 리팩토링 (@common/auth)
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/auth": "workspace:*",
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
@@ -60,4 +61,4 @@
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getRemainingMs } from "@common/auth";
|
||||
import {
|
||||
ENDPOINT,
|
||||
CLAIM_NAME,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
clearAuthToken,
|
||||
setupRefreshTimer,
|
||||
} from "../lib/token-manager";
|
||||
import { getRemainingMs } from "../lib/jwt-utils";
|
||||
|
||||
function resolveLoginError(message: string | undefined): string {
|
||||
switch (message) {
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { isAxiosError } from "axios";
|
||||
import { getRemainingMs } from "@common/auth";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import {
|
||||
ENDPOINT,
|
||||
AUTH_USER_ID,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
CLAIM_NAME,
|
||||
} from "@/shared/config";
|
||||
import { getToken, setupRefreshTimer } from "./token-manager";
|
||||
import { getRemainingMs } from "./jwt-utils";
|
||||
import { silentRefresh } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
@@ -1,62 +1,15 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME, COOKIE_DOMAIN } from "@/shared/config";
|
||||
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
|
||||
import { createTokenManager } from "@common/auth";
|
||||
import { authConfig } from "@/shared/config/auth";
|
||||
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
export const tokenManager = createTokenManager(authConfig);
|
||||
|
||||
export function getToken(key: string): string | undefined {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
export function setToken(key: string, value: string): void {
|
||||
const domain = getCookieDomain();
|
||||
const isHttps =
|
||||
typeof window !== "undefined" && window.location.protocol === "https:";
|
||||
|
||||
setCookie(key, value, {
|
||||
secure: isHttps,
|
||||
sameSite: "lax",
|
||||
domain,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
export function removeToken(key: string): void {
|
||||
const domain = getCookieDomain();
|
||||
removeCookie(key, { domain, path: "/" });
|
||||
}
|
||||
|
||||
export function setupRefreshTimer(
|
||||
callback: () => Promise<void>,
|
||||
delayMs: number,
|
||||
): void {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
}
|
||||
|
||||
refreshTimer = setTimeout(() => {
|
||||
callback().catch((error) => {
|
||||
console.error("Silent refresh timer callback failed:", error);
|
||||
});
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
export function clearAuthToken(): void {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
removeToken(CLAIM_NAME);
|
||||
removeToken(AUTH_USER_ID);
|
||||
}
|
||||
|
||||
function getCookieDomain(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
|
||||
|
||||
const { hostname } = window.location;
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
|
||||
|
||||
return hostname;
|
||||
}
|
||||
export const {
|
||||
getToken,
|
||||
setToken,
|
||||
removeToken,
|
||||
setupRefreshTimer,
|
||||
clearAuthToken,
|
||||
getAccessToken,
|
||||
getRefreshToken,
|
||||
getUserId,
|
||||
} = tokenManager;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AuthConfig } from "@common/auth";
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
COOKIE_DOMAIN,
|
||||
REFRESH_TOKEN,
|
||||
} from "./env";
|
||||
|
||||
export const authConfig: AuthConfig = {
|
||||
authUserId: AUTH_USER_ID || "token-id",
|
||||
claimName: CLAIM_NAME || "Authorization",
|
||||
cookieDomain: COOKIE_DOMAIN,
|
||||
accessToken: ACCESS_TOKEN || "access-token",
|
||||
refreshToken: REFRESH_TOKEN || "refresh-token",
|
||||
};
|
||||
@@ -0,0 +1,492 @@
|
||||
# @common/auth
|
||||
|
||||
인증 관련 공통 기능을 제공하는 독립 패키지입니다. 의존성 주입 패턴을 통해 각 앱의 환경변수를 주입받아 재사용 가능한 토큰 관리 기능을 제공합니다.
|
||||
|
||||
## 특징
|
||||
|
||||
- **의존성 주입**: 환경변수를 직접 참조하지 않고 설정 객체를 주입받음
|
||||
- **재사용성**: 모든 앱에서 동일한 로직 사용 가능
|
||||
- **타입 안정성**: TypeScript 인터페이스로 계약 보장
|
||||
- **테스트 가능성**: Mock 설정 주입으로 단위 테스트 용이
|
||||
|
||||
## 패키지 구조
|
||||
|
||||
```
|
||||
packages/common/auth/
|
||||
├── package.json # 패키지 정의
|
||||
├── tsconfig.json # TypeScript 설정
|
||||
├── index.ts # 진입점 (Public API)
|
||||
├── types.ts # 타입 정의
|
||||
├── token-manager.ts # 토큰 관리 로직
|
||||
├── cookie.ts # 쿠키 유틸리티
|
||||
├── jwt-utils.ts # JWT 유틸리티
|
||||
└── README.md # 문서
|
||||
```
|
||||
|
||||
## 설치
|
||||
|
||||
각 앱의 `package.json`에 의존성을 추가합니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@common/auth": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 사용 방법
|
||||
|
||||
### 1. 설정 파일 생성
|
||||
|
||||
각 앱에서 자신의 환경변수로 `AuthConfig` 객체를 생성합니다.
|
||||
|
||||
```typescript
|
||||
// apps/cs/src/shared/config/auth.ts
|
||||
import type { AuthConfig } from "@common/auth/types";
|
||||
|
||||
export const authConfig: AuthConfig = {
|
||||
authUserId: process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id",
|
||||
claimName: process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization",
|
||||
cookieDomain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
|
||||
accessToken: process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token",
|
||||
refreshToken: process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token",
|
||||
};
|
||||
```
|
||||
|
||||
### 2. TokenManager 인스턴스 생성
|
||||
|
||||
설정을 주입하여 토큰 매니저 인스턴스를 생성합니다.
|
||||
|
||||
```typescript
|
||||
// apps/cs/src/entities/auth/lib/token-manager.ts
|
||||
import { createTokenManager } from "@common/auth";
|
||||
import { authConfig } from "@/shared/config/auth";
|
||||
|
||||
export const tokenManager = createTokenManager(authConfig);
|
||||
|
||||
// 편의를 위한 re-export
|
||||
export const {
|
||||
getToken,
|
||||
setToken,
|
||||
removeToken,
|
||||
setupRefreshTimer,
|
||||
clearAuthToken,
|
||||
getAccessToken,
|
||||
getRefreshToken,
|
||||
getUserId,
|
||||
} = tokenManager;
|
||||
```
|
||||
|
||||
### 3. 컴포넌트에서 사용
|
||||
|
||||
```typescript
|
||||
// apps/cs/src/features/auth/ui/LoginForm.tsx
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { authConfig } from "@/shared/config/auth";
|
||||
|
||||
export function LoginForm() {
|
||||
const handleLogin = async (credentials: LoginCredentials) => {
|
||||
const response = await loginApi(credentials);
|
||||
|
||||
// 토큰 저장
|
||||
tokenManager.setToken(authConfig.claimName, response.accessToken);
|
||||
tokenManager.setToken(authConfig.refreshToken, response.refreshToken);
|
||||
|
||||
// 또는 편의 메서드 사용
|
||||
// tokenManager.getAccessToken();
|
||||
// tokenManager.getRefreshToken();
|
||||
// tokenManager.getUserId();
|
||||
};
|
||||
|
||||
return (
|
||||
// ... JSX
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API 문서
|
||||
|
||||
### `createTokenManager(config: AuthConfig): TokenManager`
|
||||
|
||||
토큰 매니저 인스턴스를 생성하는 팩토리 함수입니다.
|
||||
|
||||
**Parameters:**
|
||||
- `config: AuthConfig` - 인증 설정 객체
|
||||
|
||||
**Returns:**
|
||||
- `TokenManager` - 토큰 관리 메서드를 포함한 객체
|
||||
|
||||
### TokenManager 메서드
|
||||
|
||||
#### `getToken(key: string): string | undefined`
|
||||
|
||||
지정된 키의 쿠키 값을 가져옵니다.
|
||||
|
||||
```typescript
|
||||
const token = tokenManager.getToken("access-token");
|
||||
```
|
||||
|
||||
#### `setToken(key: string, value: string): void`
|
||||
|
||||
토큰을 쿠키에 저장합니다. 자동으로 도메인, secure, sameSite 설정을 적용합니다.
|
||||
|
||||
```typescript
|
||||
tokenManager.setToken("access-token", "eyJhbGc...");
|
||||
```
|
||||
|
||||
#### `removeToken(key: string): void`
|
||||
|
||||
지정된 키의 쿠키를 삭제합니다.
|
||||
|
||||
```typescript
|
||||
tokenManager.removeToken("access-token");
|
||||
```
|
||||
|
||||
#### `setupRefreshTimer(callback: () => Promise<void>, delayMs: number): void`
|
||||
|
||||
토큰 갱신 타이머를 설정합니다.
|
||||
|
||||
```typescript
|
||||
tokenManager.setupRefreshTimer(async () => {
|
||||
await refreshAccessToken();
|
||||
}, 3600000); // 1시간 후
|
||||
```
|
||||
|
||||
#### `clearAuthToken(): void`
|
||||
|
||||
모든 인증 토큰을 삭제하고 타이머를 정리합니다.
|
||||
|
||||
```typescript
|
||||
tokenManager.clearAuthToken();
|
||||
```
|
||||
|
||||
#### `getAccessToken(): string | undefined`
|
||||
|
||||
액세스 토큰을 가져오는 편의 메서드입니다.
|
||||
|
||||
```typescript
|
||||
const accessToken = tokenManager.getAccessToken();
|
||||
```
|
||||
|
||||
#### `getRefreshToken(): string | undefined`
|
||||
|
||||
리프레시 토큰을 가져오는 편의 메서드입니다.
|
||||
|
||||
```typescript
|
||||
const refreshToken = tokenManager.getRefreshToken();
|
||||
```
|
||||
|
||||
#### `getUserId(): string | undefined`
|
||||
|
||||
사용자 ID 토큰을 가져오는 편의 메서드입니다.
|
||||
|
||||
```typescript
|
||||
const userId = tokenManager.getUserId();
|
||||
```
|
||||
|
||||
### JWT 유틸리티
|
||||
|
||||
#### `getJwtExpiry(token: string): number | null`
|
||||
|
||||
JWT 토큰의 만료 시간(Unix timestamp, 초 단위)을 반환합니다.
|
||||
|
||||
```typescript
|
||||
import { getJwtExpiry } from "@common/auth";
|
||||
|
||||
const expiry = getJwtExpiry(token);
|
||||
if (expiry) {
|
||||
console.log("Token expires at:", new Date(expiry * 1000));
|
||||
}
|
||||
```
|
||||
|
||||
#### `getRemainingMs(token: string): number`
|
||||
|
||||
토큰 만료 1분 전까지 남은 시간(밀리초)을 반환합니다.
|
||||
|
||||
```typescript
|
||||
import { getRemainingMs } from "@common/auth";
|
||||
|
||||
const remaining = getRemainingMs(token);
|
||||
if (remaining > 0) {
|
||||
console.log("Token valid for:", remaining, "ms");
|
||||
}
|
||||
```
|
||||
|
||||
### 쿠키 유틸리티
|
||||
|
||||
#### `getCookie(name: string): string | undefined`
|
||||
|
||||
쿠키 값을 가져옵니다.
|
||||
|
||||
```typescript
|
||||
import { getCookie } from "@common/auth";
|
||||
|
||||
const value = getCookie("my-cookie");
|
||||
```
|
||||
|
||||
#### `setCookie(name: string, value: string, options?: CookieOptions): void`
|
||||
|
||||
쿠키를 설정합니다.
|
||||
|
||||
```typescript
|
||||
import { setCookie } from "@common/auth";
|
||||
|
||||
setCookie("my-cookie", "value", {
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
```
|
||||
|
||||
#### `removeCookie(name: string, options?: CookieOptions): void`
|
||||
|
||||
쿠키를 삭제합니다.
|
||||
|
||||
```typescript
|
||||
import { removeCookie } from "@common/auth";
|
||||
|
||||
removeCookie("my-cookie", { path: "/" });
|
||||
```
|
||||
|
||||
## 타입 정의
|
||||
|
||||
### `AuthConfig`
|
||||
|
||||
```typescript
|
||||
interface AuthConfig {
|
||||
authUserId: string; // 사용자 ID 토큰 키
|
||||
claimName: string; // 액세스 토큰 키
|
||||
cookieDomain?: string; // 쿠키 도메인 (선택)
|
||||
accessToken?: string; // 액세스 토큰 키 (선택)
|
||||
refreshToken?: string; // 리프레시 토큰 키 (선택)
|
||||
}
|
||||
```
|
||||
|
||||
### `CookieOptions`
|
||||
|
||||
```typescript
|
||||
interface CookieOptions {
|
||||
secure?: boolean;
|
||||
sameSite?: "strict" | "lax" | "none";
|
||||
domain?: string;
|
||||
path?: string;
|
||||
expires?: number | Date;
|
||||
}
|
||||
```
|
||||
|
||||
## 마이그레이션 가이드
|
||||
|
||||
### 기존 앱에서 @common/auth로 마이그레이션
|
||||
|
||||
#### 1. 의존성 추가
|
||||
|
||||
```bash
|
||||
# 앱 디렉토리에서
|
||||
cd apps/your-app
|
||||
```
|
||||
|
||||
`package.json`에 추가:
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@common/auth": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
#### 2. 설정 파일 생성
|
||||
|
||||
```typescript
|
||||
// apps/your-app/src/shared/config/auth.ts
|
||||
import type { AuthConfig } from "@common/auth/types";
|
||||
|
||||
export const authConfig: AuthConfig = {
|
||||
authUserId: process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id",
|
||||
claimName: process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization",
|
||||
cookieDomain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
|
||||
accessToken: process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token",
|
||||
refreshToken: process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token",
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. TokenManager 수정
|
||||
|
||||
**Before (기존 코드):**
|
||||
```typescript
|
||||
// apps/your-app/src/entities/auth/lib/token-manager.ts
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
||||
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
|
||||
|
||||
export function getToken(key: string): string | undefined {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
export function clearAuthToken(): void {
|
||||
removeToken(CLAIM_NAME);
|
||||
removeToken(AUTH_USER_ID);
|
||||
}
|
||||
```
|
||||
|
||||
**After (@common/auth 사용):**
|
||||
```typescript
|
||||
// apps/your-app/src/entities/auth/lib/token-manager.ts
|
||||
import { createTokenManager } from "@common/auth";
|
||||
import { authConfig } from "@/shared/config/auth";
|
||||
|
||||
export const tokenManager = createTokenManager(authConfig);
|
||||
|
||||
// 기존 코드와의 호환성을 위한 re-export
|
||||
export const {
|
||||
getToken,
|
||||
setToken,
|
||||
removeToken,
|
||||
setupRefreshTimer,
|
||||
clearAuthToken,
|
||||
getAccessToken,
|
||||
getRefreshToken,
|
||||
getUserId,
|
||||
} = tokenManager;
|
||||
```
|
||||
|
||||
#### 4. 사용처 업데이트
|
||||
|
||||
**클래스 기반 (st, cm 앱):**
|
||||
```typescript
|
||||
// Before
|
||||
import { TokenManager } from "@/entities/auth/lib/token-manager";
|
||||
const tokenManager = TokenManager.getInstance();
|
||||
|
||||
// After
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
// 그대로 사용 가능
|
||||
```
|
||||
|
||||
**함수형 (cs 앱):**
|
||||
```typescript
|
||||
// Before
|
||||
import { getToken, setToken, clearAuthToken } from "@/entities/auth/lib/token-manager";
|
||||
|
||||
// After
|
||||
import { getToken, setToken, clearAuthToken } from "@/entities/auth/lib/token-manager";
|
||||
// 그대로 사용 가능 (re-export 덕분)
|
||||
```
|
||||
|
||||
## 다른 앱에서 사용 예시
|
||||
|
||||
### AS 앱
|
||||
|
||||
```typescript
|
||||
// apps/as/src/shared/config/auth.ts
|
||||
import type { AuthConfig } from "@common/auth/types";
|
||||
|
||||
export const authConfig: AuthConfig = {
|
||||
authUserId: process.env.NEXT_PUBLIC_AS_AUTH_USER_ID || "as-token-id",
|
||||
claimName: process.env.NEXT_PUBLIC_AS_CLAIM_NAME || "X-AS-Auth",
|
||||
cookieDomain: process.env.NEXT_PUBLIC_AS_COOKIE_DOMAIN,
|
||||
accessToken: process.env.NEXT_PUBLIC_AS_ACCESS_TOKEN || "as-access-token",
|
||||
refreshToken: process.env.NEXT_PUBLIC_AS_REFRESH_TOKEN || "as-refresh-token",
|
||||
};
|
||||
```
|
||||
|
||||
### ST 앱
|
||||
|
||||
```typescript
|
||||
// apps/st/src/shared/config/auth.ts
|
||||
import type { AuthConfig } from "@common/auth/types";
|
||||
|
||||
export const authConfig: AuthConfig = {
|
||||
authUserId: process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id",
|
||||
claimName: process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization",
|
||||
cookieDomain: ".hasuon.or.kr", // 하드코딩된 도메인도 설정으로 이동
|
||||
accessToken: process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token",
|
||||
refreshToken: process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token",
|
||||
};
|
||||
```
|
||||
|
||||
## 환경변수 설정
|
||||
|
||||
각 앱의 `.env.local` 파일:
|
||||
|
||||
```bash
|
||||
# CS 앱
|
||||
NEXT_PUBLIC_CLAIM_NAME=Authorization
|
||||
NEXT_PUBLIC_ACCESS_TOKEN=access-token
|
||||
NEXT_PUBLIC_REFRESH_TOKEN=refresh-token
|
||||
NEXT_PUBLIC_AUTH_USER_ID=token-id
|
||||
NEXT_PUBLIC_COOKIE_DOMAIN=.example.com
|
||||
|
||||
# AS 앱 (다른 설정 사용 가능)
|
||||
NEXT_PUBLIC_AS_CLAIM_NAME=X-AS-Auth
|
||||
NEXT_PUBLIC_AS_ACCESS_TOKEN=as-access-token
|
||||
NEXT_PUBLIC_AS_REFRESH_TOKEN=as-refresh-token
|
||||
NEXT_PUBLIC_AS_AUTH_USER_ID=as-token-id
|
||||
NEXT_PUBLIC_AS_COOKIE_DOMAIN=.as.example.com
|
||||
```
|
||||
|
||||
## 테스트 예시
|
||||
|
||||
```typescript
|
||||
import { createTokenManager } from "@common/auth";
|
||||
import type { AuthConfig } from "@common/auth/types";
|
||||
|
||||
describe("TokenManager", () => {
|
||||
const mockConfig: AuthConfig = {
|
||||
authUserId: "test-user-id",
|
||||
claimName: "test-claim",
|
||||
cookieDomain: ".test.com",
|
||||
accessToken: "test-access",
|
||||
refreshToken: "test-refresh",
|
||||
};
|
||||
|
||||
it("should create token manager with config", () => {
|
||||
const tokenManager = createTokenManager(mockConfig);
|
||||
expect(tokenManager).toBeDefined();
|
||||
expect(tokenManager.getToken).toBeDefined();
|
||||
});
|
||||
|
||||
it("should use injected config", () => {
|
||||
const tokenManager = createTokenManager(mockConfig);
|
||||
// 테스트 로직...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **환경변수는 빌드 타임에 주입됩니다**: `process.env.NEXT_PUBLIC_*` 값은 빌드 시점에 정적으로 치환됩니다.
|
||||
|
||||
2. **쿠키 도메인 자동 감지**: `cookieDomain`을 설정하지 않으면 자동으로 현재 호스트명을 사용합니다.
|
||||
- `localhost` / `127.0.0.1`: 도메인 설정 안 함
|
||||
- 기타: 현재 호스트명 사용
|
||||
|
||||
3. **HTTPS 자동 감지**: `secure` 옵션은 현재 프로토콜에 따라 자동 설정됩니다.
|
||||
|
||||
4. **타이머 관리**: `setupRefreshTimer`는 이전 타이머를 자동으로 정리합니다.
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### TypeScript 오류: "Cannot find module '@common/auth'"
|
||||
|
||||
1. `pnpm install` 실행
|
||||
2. TypeScript 서버 재시작
|
||||
3. `tsconfig.json`의 `paths` 설정 확인
|
||||
|
||||
### 쿠키가 저장되지 않음
|
||||
|
||||
1. 브라우저 개발자 도구에서 쿠키 확인
|
||||
2. `cookieDomain` 설정 확인
|
||||
3. HTTPS 환경에서 `secure: true` 확인
|
||||
|
||||
### 토큰이 다른 앱에서 공유되지 않음
|
||||
|
||||
1. `cookieDomain` 설정이 공통 도메인(예: `.example.com`)인지 확인
|
||||
2. 각 앱의 토큰 키 이름이 다른지 확인 (충돌 방지)
|
||||
|
||||
## 라이선스
|
||||
|
||||
ISC
|
||||
@@ -0,0 +1,29 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
export function getCookie(name: string): string | undefined {
|
||||
if (typeof window === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
return Cookies.get(name);
|
||||
}
|
||||
|
||||
export function setCookie(
|
||||
name: string,
|
||||
value: string,
|
||||
options?: Cookies.CookieAttributes,
|
||||
): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
Cookies.set(name, value, options);
|
||||
}
|
||||
|
||||
export function removeCookie(
|
||||
name: string,
|
||||
options?: Cookies.CookieAttributes,
|
||||
): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
Cookies.remove(name, options);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export { createTokenManager } from "./token-manager";
|
||||
export type { TokenManager } from "./token-manager";
|
||||
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { getJwtExpiry, getRemainingMs } from "./jwt-utils";
|
||||
|
||||
export type { AuthConfig, CookieOptions } from "./types";
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@common/auth",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-cookie": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { AuthConfig } from "./types";
|
||||
import { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
|
||||
export function createTokenManager(config: AuthConfig) {
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const getCookieDomain = (): string | undefined => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (config.cookieDomain) return config.cookieDomain;
|
||||
|
||||
const { hostname } = window.location;
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
|
||||
|
||||
return hostname;
|
||||
};
|
||||
|
||||
return {
|
||||
getToken(key: string): string | undefined {
|
||||
return getCookie(key);
|
||||
},
|
||||
|
||||
setToken(key: string, value: string): void {
|
||||
const domain = getCookieDomain();
|
||||
const isHttps =
|
||||
typeof window !== "undefined" && window.location.protocol === "https:";
|
||||
|
||||
setCookie(key, value, {
|
||||
secure: isHttps,
|
||||
sameSite: "lax",
|
||||
domain,
|
||||
path: "/",
|
||||
});
|
||||
},
|
||||
|
||||
removeToken(key: string): void {
|
||||
const domain = getCookieDomain();
|
||||
removeCookie(key, { domain, path: "/" });
|
||||
},
|
||||
|
||||
setupRefreshTimer(callback: () => Promise<void>, delayMs: number): void {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
}
|
||||
|
||||
refreshTimer = setTimeout(() => {
|
||||
callback().catch((error) => {
|
||||
console.error("Silent refresh timer callback failed:", error);
|
||||
});
|
||||
}, delayMs);
|
||||
},
|
||||
|
||||
clearAuthToken(): void {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
this.removeToken(config.claimName);
|
||||
this.removeToken(config.authUserId);
|
||||
},
|
||||
|
||||
getAccessToken(): string | undefined {
|
||||
return this.getToken(config.claimName);
|
||||
},
|
||||
|
||||
getRefreshToken(): string | undefined {
|
||||
return config.refreshToken ? this.getToken(config.refreshToken) : undefined;
|
||||
},
|
||||
|
||||
getUserId(): string | undefined {
|
||||
return this.getToken(config.authUserId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type TokenManager = ReturnType<typeof createTokenManager>;
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
"strictNullChecks": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface AuthConfig {
|
||||
authUserId: string;
|
||||
claimName: string;
|
||||
cookieDomain?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
|
||||
export interface CookieOptions {
|
||||
secure?: boolean;
|
||||
sameSite?: "strict" | "lax" | "none";
|
||||
domain?: string;
|
||||
path?: string;
|
||||
expires?: number | Date;
|
||||
}
|
||||
Generated
+103
-29
@@ -101,10 +101,10 @@ importers:
|
||||
devDependencies:
|
||||
eslint:
|
||||
specifier: ^9.0.0
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
version: 9.39.2(jiti@1.21.7)
|
||||
eslint-config-next:
|
||||
specifier: 15.4.0
|
||||
version: 15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
version: 15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
postcss:
|
||||
specifier: ^8.4.49
|
||||
version: 8.5.6
|
||||
@@ -407,6 +407,9 @@ importers:
|
||||
'@common/assets':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/common/assets
|
||||
'@common/auth':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/common/auth
|
||||
'@common/components':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/common/components
|
||||
@@ -1190,13 +1193,13 @@ importers:
|
||||
version: 5.28.5
|
||||
eslint:
|
||||
specifier: ^9
|
||||
version: 9.39.2(jiti@1.21.7)
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
eslint-config-next:
|
||||
specifier: 15.1.7
|
||||
version: 15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
version: 15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-react-hooks:
|
||||
specifier: ^5.0.0
|
||||
version: 5.2.0(eslint@9.39.2(jiti@1.21.7))
|
||||
version: 5.2.0(eslint@9.39.2(jiti@2.6.1))
|
||||
postcss:
|
||||
specifier: ^8
|
||||
version: 8.5.6
|
||||
@@ -1358,6 +1361,22 @@ importers:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/common/auth:
|
||||
dependencies:
|
||||
js-cookie:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
devDependencies:
|
||||
'@types/js-cookie':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/node':
|
||||
specifier: ^22.13.1
|
||||
version: 22.19.2
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
packages/common/business_components:
|
||||
dependencies:
|
||||
'@common/components':
|
||||
@@ -10729,26 +10748,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
source-map: 0.7.6
|
||||
|
||||
eslint-config-next@15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.1.7
|
||||
'@rushstack/eslint-patch': 1.15.0
|
||||
'@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.1.7
|
||||
@@ -10757,8 +10756,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1))
|
||||
@@ -10769,6 +10768,26 @@ snapshots:
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.4.0
|
||||
'@rushstack/eslint-patch': 1.15.0
|
||||
'@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.4.0
|
||||
@@ -10797,6 +10816,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
get-tsconfig: 4.13.0
|
||||
is-bun-module: 2.0.0
|
||||
stable-hash: 0.0.5
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
@@ -10808,7 +10842,7 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -10838,6 +10872,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
@@ -10849,7 +10894,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -10878,6 +10923,35 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
array.prototype.findlastindex: 1.2.6
|
||||
array.prototype.flat: 1.3.3
|
||||
array.prototype.flatmap: 1.3.3
|
||||
debug: 3.2.7
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
minimatch: 3.1.2
|
||||
object.fromentries: 2.0.8
|
||||
object.groupby: 1.0.3
|
||||
object.values: 1.2.1
|
||||
semver: 6.3.1
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
|
||||
Reference in New Issue
Block a user