aiteamutils 0.2.52__tar.gz → 0.2.54__tar.gz
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.
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/PKG-INFO +1 -1
- aiteamutils-0.2.54/aiteamutils/database.py +207 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/dependencies.py +7 -24
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/security.py +120 -71
- aiteamutils-0.2.54/aiteamutils/version.py +2 -0
- aiteamutils-0.2.52/aiteamutils/database.py +0 -1083
- aiteamutils-0.2.52/aiteamutils/version.py +0 -2
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/.cursorrules +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/.gitignore +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/README.md +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/__init__.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/base_model.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/base_repository.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/base_service.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/cache.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/config.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/enums.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/exceptions.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/aiteamutils/validators.py +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/pyproject.toml +0 -0
- {aiteamutils-0.2.52 → aiteamutils-0.2.54}/setup.py +0 -0
@@ -0,0 +1,207 @@
|
|
1
|
+
"""데이터베이스 유틸리티 모듈."""
|
2
|
+
from typing import Any, Dict, Optional, Type, List, Union
|
3
|
+
from sqlalchemy import select, and_
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
5
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
6
|
+
|
7
|
+
from .exceptions import ErrorCode, CustomException
|
8
|
+
from .base_model import Base
|
9
|
+
|
10
|
+
class DatabaseService:
|
11
|
+
def __init__(self, session: AsyncSession):
|
12
|
+
"""DatabaseService 초기화
|
13
|
+
|
14
|
+
Args:
|
15
|
+
session (AsyncSession): 외부에서 주입받은 데이터베이스 세션
|
16
|
+
"""
|
17
|
+
self._session = session
|
18
|
+
|
19
|
+
@property
|
20
|
+
def session(self) -> AsyncSession:
|
21
|
+
"""현재 세션을 반환합니다."""
|
22
|
+
if self._session is None:
|
23
|
+
raise CustomException(
|
24
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
25
|
+
detail="session",
|
26
|
+
source_function="DatabaseService.session"
|
27
|
+
)
|
28
|
+
return self._session
|
29
|
+
|
30
|
+
async def create_entity(
|
31
|
+
self,
|
32
|
+
model: Type[Base],
|
33
|
+
entity_data: Dict[str, Any]
|
34
|
+
) -> Any:
|
35
|
+
"""엔티티를 생성합니다.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
model: 모델 클래스
|
39
|
+
entity_data: 생성할 엔티티 데이터
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
생성된 엔티티
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
CustomException: 엔티티 생성 실패 시
|
46
|
+
"""
|
47
|
+
try:
|
48
|
+
entity = model(**entity_data)
|
49
|
+
self.session.add(entity)
|
50
|
+
await self.session.flush()
|
51
|
+
await self.session.refresh(entity)
|
52
|
+
return entity
|
53
|
+
except IntegrityError as e:
|
54
|
+
await self.session.rollback()
|
55
|
+
raise CustomException(
|
56
|
+
ErrorCode.DB_INTEGRITY_ERROR,
|
57
|
+
detail=str(e),
|
58
|
+
source_function="DatabaseService.create_entity",
|
59
|
+
original_error=e
|
60
|
+
)
|
61
|
+
except Exception as e:
|
62
|
+
await self.session.rollback()
|
63
|
+
raise CustomException(
|
64
|
+
ErrorCode.DB_CREATE_ERROR,
|
65
|
+
detail=str(e),
|
66
|
+
source_function="DatabaseService.create_entity",
|
67
|
+
original_error=e
|
68
|
+
)
|
69
|
+
|
70
|
+
async def get_entity(
|
71
|
+
self,
|
72
|
+
model: Type[Base],
|
73
|
+
filters: Dict[str, Any]
|
74
|
+
) -> Optional[Any]:
|
75
|
+
"""필터 조건으로 엔티티를 조회합니다.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
model: 모델 클래스
|
79
|
+
filters: 필터 조건
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
조회된 엔티티 또는 None
|
83
|
+
|
84
|
+
Raises:
|
85
|
+
CustomException: 조회 실패 시
|
86
|
+
"""
|
87
|
+
try:
|
88
|
+
stmt = select(model).filter_by(**filters)
|
89
|
+
result = await self.session.execute(stmt)
|
90
|
+
return result.scalars().first()
|
91
|
+
except Exception as e:
|
92
|
+
raise CustomException(
|
93
|
+
ErrorCode.DB_QUERY_ERROR,
|
94
|
+
detail=str(e),
|
95
|
+
source_function="DatabaseService.get_entity",
|
96
|
+
original_error=e
|
97
|
+
)
|
98
|
+
|
99
|
+
async def list_entities(
|
100
|
+
self,
|
101
|
+
model: Type[Base],
|
102
|
+
filters: Optional[Dict[str, Any]] = None,
|
103
|
+
skip: int = 0,
|
104
|
+
limit: int = 100
|
105
|
+
) -> List[Any]:
|
106
|
+
"""엔티티 목록을 조회합니다.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
model: 모델 클래스
|
110
|
+
filters: 필터 조건
|
111
|
+
skip: 건너뛸 레코드 수
|
112
|
+
limit: 조회할 최대 레코드 수
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
엔티티 목록
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
CustomException: 조회 실패 시
|
119
|
+
"""
|
120
|
+
try:
|
121
|
+
stmt = select(model)
|
122
|
+
if filters:
|
123
|
+
stmt = stmt.filter_by(**filters)
|
124
|
+
stmt = stmt.offset(skip).limit(limit)
|
125
|
+
result = await self.session.execute(stmt)
|
126
|
+
return result.scalars().all()
|
127
|
+
except Exception as e:
|
128
|
+
raise CustomException(
|
129
|
+
ErrorCode.DB_QUERY_ERROR,
|
130
|
+
detail=str(e),
|
131
|
+
source_function="DatabaseService.list_entities",
|
132
|
+
original_error=e
|
133
|
+
)
|
134
|
+
|
135
|
+
async def update_entity(
|
136
|
+
self,
|
137
|
+
entity: Base,
|
138
|
+
update_data: Dict[str, Any]
|
139
|
+
) -> Any:
|
140
|
+
"""엔티티를 수정합니다.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
entity: 수정할 엔티티
|
144
|
+
update_data: 수정할 데이터
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
수정된 엔티티
|
148
|
+
|
149
|
+
Raises:
|
150
|
+
CustomException: 수정 실패 시
|
151
|
+
"""
|
152
|
+
try:
|
153
|
+
for key, value in update_data.items():
|
154
|
+
setattr(entity, key, value)
|
155
|
+
await self.session.flush()
|
156
|
+
await self.session.refresh(entity)
|
157
|
+
return entity
|
158
|
+
except IntegrityError as e:
|
159
|
+
await self.session.rollback()
|
160
|
+
raise CustomException(
|
161
|
+
ErrorCode.DB_INTEGRITY_ERROR,
|
162
|
+
detail=str(e),
|
163
|
+
source_function="DatabaseService.update_entity",
|
164
|
+
original_error=e
|
165
|
+
)
|
166
|
+
except Exception as e:
|
167
|
+
await self.session.rollback()
|
168
|
+
raise CustomException(
|
169
|
+
ErrorCode.DB_UPDATE_ERROR,
|
170
|
+
detail=str(e),
|
171
|
+
source_function="DatabaseService.update_entity",
|
172
|
+
original_error=e
|
173
|
+
)
|
174
|
+
|
175
|
+
async def delete_entity(
|
176
|
+
self,
|
177
|
+
entity: Base,
|
178
|
+
soft_delete: bool = True
|
179
|
+
) -> bool:
|
180
|
+
"""엔티티를 삭제합니다.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
entity: 삭제할 엔티티
|
184
|
+
soft_delete: 소프트 삭제 여부
|
185
|
+
|
186
|
+
Returns:
|
187
|
+
삭제 성공 여부
|
188
|
+
|
189
|
+
Raises:
|
190
|
+
CustomException: 삭제 실패 시
|
191
|
+
"""
|
192
|
+
try:
|
193
|
+
if soft_delete:
|
194
|
+
entity.is_deleted = True
|
195
|
+
await self.session.flush()
|
196
|
+
else:
|
197
|
+
await self.session.delete(entity)
|
198
|
+
await self.session.flush()
|
199
|
+
return True
|
200
|
+
except Exception as e:
|
201
|
+
await self.session.rollback()
|
202
|
+
raise CustomException(
|
203
|
+
ErrorCode.DB_DELETE_ERROR,
|
204
|
+
detail=str(e),
|
205
|
+
source_function="DatabaseService.delete_entity",
|
206
|
+
original_error=e
|
207
|
+
)
|
@@ -9,33 +9,13 @@ from .exceptions import CustomException, ErrorCode
|
|
9
9
|
from .config import get_settings
|
10
10
|
from .base_service import BaseService
|
11
11
|
from .base_repository import BaseRepository
|
12
|
-
from .database import
|
12
|
+
from .database import DatabaseService
|
13
13
|
|
14
14
|
T = TypeVar("T", bound=BaseService)
|
15
15
|
R = TypeVar("R", bound=BaseRepository)
|
16
16
|
|
17
17
|
_service_registry: Dict[str, Dict[str, Any]] = {}
|
18
18
|
|
19
|
-
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
20
|
-
"""데이터베이스 세션을 반환합니다.
|
21
|
-
|
22
|
-
Yields:
|
23
|
-
AsyncSession: 데이터베이스 세션
|
24
|
-
|
25
|
-
Raises:
|
26
|
-
CustomException: 세션 생성 실패 시
|
27
|
-
"""
|
28
|
-
try:
|
29
|
-
async with db_manager.get_session() as session:
|
30
|
-
yield session
|
31
|
-
except Exception as e:
|
32
|
-
raise CustomException(
|
33
|
-
ErrorCode.DATABASE_ERROR,
|
34
|
-
detail=str(e),
|
35
|
-
source_function="dependencies.get_db",
|
36
|
-
original_error=e
|
37
|
-
)
|
38
|
-
|
39
19
|
def register_service(
|
40
20
|
service_class: Type[T],
|
41
21
|
repository_class: Optional[Type[R]] = None,
|
@@ -86,6 +66,9 @@ async def _get_service(
|
|
86
66
|
repository_class = service_info["repository_class"]
|
87
67
|
dependencies = service_info["dependencies"]
|
88
68
|
|
69
|
+
# 데이터베이스 서비스 생성
|
70
|
+
db_service = DatabaseService(session=session)
|
71
|
+
|
89
72
|
# 저장소 인스턴스 생성
|
90
73
|
repository = None
|
91
74
|
if repository_class:
|
@@ -93,8 +76,8 @@ async def _get_service(
|
|
93
76
|
|
94
77
|
# 서비스 인스턴스 생성
|
95
78
|
service = service_class(
|
79
|
+
db=db_service,
|
96
80
|
repository=repository,
|
97
|
-
session=session,
|
98
81
|
request=request,
|
99
82
|
**dependencies
|
100
83
|
)
|
@@ -122,14 +105,14 @@ def get_service(service_name: str) -> Callable:
|
|
122
105
|
"""
|
123
106
|
async def _get_service_dependency(
|
124
107
|
request: Request,
|
125
|
-
session: AsyncSession
|
108
|
+
session: AsyncSession
|
126
109
|
) -> BaseService:
|
127
110
|
return await _get_service(service_name, session, request)
|
128
111
|
return _get_service_dependency
|
129
112
|
|
130
113
|
async def get_current_user(
|
131
114
|
request: Request,
|
132
|
-
session: AsyncSession
|
115
|
+
session: AsyncSession,
|
133
116
|
auth_service: BaseService = Depends(get_service("AuthService"))
|
134
117
|
) -> Dict[str, Any]:
|
135
118
|
"""현재 사용자 정보를 반환합니다.
|
@@ -1,12 +1,10 @@
|
|
1
1
|
"""보안 관련 유틸리티."""
|
2
2
|
from datetime import datetime, timedelta, UTC
|
3
|
-
from typing import Dict, Any, Optional, Literal, Callable,
|
4
|
-
from sqlalchemy.orm import DeclarativeBase as Base
|
3
|
+
from typing import Dict, Any, Optional, Literal, Callable, TYPE_CHECKING
|
5
4
|
from fastapi import Request, HTTPException, status
|
6
5
|
from functools import wraps
|
7
6
|
from jose import jwt, JWTError
|
8
7
|
from passlib.context import CryptContext
|
9
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
10
8
|
import logging
|
11
9
|
|
12
10
|
from .exceptions import CustomException, ErrorCode
|
@@ -20,29 +18,12 @@ _rate_limits: Dict[str, Dict[str, Any]] = {}
|
|
20
18
|
|
21
19
|
class RateLimitExceeded(CustomException):
|
22
20
|
"""Rate limit 초과 예외."""
|
23
|
-
|
24
|
-
def __init__(
|
25
|
-
self,
|
26
|
-
detail: str,
|
27
|
-
source_function: str,
|
28
|
-
remaining_seconds: float,
|
29
|
-
max_requests: int,
|
30
|
-
window_seconds: int
|
31
|
-
):
|
32
|
-
"""Rate limit 초과 예외를 초기화합니다."""
|
21
|
+
def __init__(self, detail: str, source_function: str):
|
33
22
|
super().__init__(
|
34
23
|
ErrorCode.RATE_LIMIT_EXCEEDED,
|
35
24
|
detail=detail,
|
36
|
-
source_function=source_function
|
37
|
-
metadata={
|
38
|
-
"remaining_seconds": remaining_seconds,
|
39
|
-
"max_requests": max_requests,
|
40
|
-
"window_seconds": window_seconds
|
41
|
-
}
|
25
|
+
source_function=source_function
|
42
26
|
)
|
43
|
-
self.remaining_seconds = remaining_seconds
|
44
|
-
self.max_requests = max_requests
|
45
|
-
self.window_seconds = window_seconds
|
46
27
|
|
47
28
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
48
29
|
"""비밀번호를 검증합니다."""
|
@@ -68,28 +49,97 @@ def hash_password(password: str) -> str:
|
|
68
49
|
original_error=e
|
69
50
|
)
|
70
51
|
|
52
|
+
def rate_limit(
|
53
|
+
max_requests: int,
|
54
|
+
window_seconds: int,
|
55
|
+
key_func: Optional[Callable] = None
|
56
|
+
):
|
57
|
+
"""Rate limiting 데코레이터."""
|
58
|
+
def decorator(func: Callable) -> Callable:
|
59
|
+
@wraps(func)
|
60
|
+
async def wrapper(*args, **kwargs):
|
61
|
+
# Request 객체 찾기
|
62
|
+
request = None
|
63
|
+
for arg in args:
|
64
|
+
if isinstance(arg, Request):
|
65
|
+
request = arg
|
66
|
+
break
|
67
|
+
if not request:
|
68
|
+
for arg in kwargs.values():
|
69
|
+
if isinstance(arg, Request):
|
70
|
+
request = arg
|
71
|
+
break
|
72
|
+
if not request:
|
73
|
+
raise CustomException(
|
74
|
+
ErrorCode.INTERNAL_ERROR,
|
75
|
+
detail="Request object not found",
|
76
|
+
source_function="rate_limit"
|
77
|
+
)
|
78
|
+
|
79
|
+
# 레이트 리밋 키 생성
|
80
|
+
if key_func:
|
81
|
+
rate_limit_key = f"rate_limit:{key_func(request)}"
|
82
|
+
else:
|
83
|
+
client_ip = request.client.host
|
84
|
+
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
85
|
+
|
86
|
+
try:
|
87
|
+
now = datetime.now(UTC)
|
88
|
+
|
89
|
+
# 현재 rate limit 정보 가져오기
|
90
|
+
rate_info = _rate_limits.get(rate_limit_key)
|
91
|
+
|
92
|
+
if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= window_seconds:
|
93
|
+
# 새로운 rate limit 설정
|
94
|
+
_rate_limits[rate_limit_key] = {
|
95
|
+
"count": 1,
|
96
|
+
"start_time": now
|
97
|
+
}
|
98
|
+
else:
|
99
|
+
# 기존 rate limit 업데이트
|
100
|
+
if rate_info["count"] >= max_requests:
|
101
|
+
# rate limit 초과
|
102
|
+
remaining_seconds = window_seconds - (now - rate_info["start_time"]).total_seconds()
|
103
|
+
raise CustomException(
|
104
|
+
ErrorCode.RATE_LIMIT_EXCEEDED,
|
105
|
+
detail=f"{int(remaining_seconds)}",
|
106
|
+
source_function=func.__name__
|
107
|
+
)
|
108
|
+
rate_info["count"] += 1
|
109
|
+
|
110
|
+
try:
|
111
|
+
# 원래 함수 실행
|
112
|
+
return await func(*args, **kwargs)
|
113
|
+
except CustomException as e:
|
114
|
+
raise e
|
115
|
+
except Exception as e:
|
116
|
+
raise CustomException(
|
117
|
+
ErrorCode.INTERNAL_ERROR,
|
118
|
+
detail=str(e),
|
119
|
+
source_function=func.__name__,
|
120
|
+
original_error=e
|
121
|
+
)
|
122
|
+
|
123
|
+
except CustomException as e:
|
124
|
+
raise e
|
125
|
+
except Exception as e:
|
126
|
+
raise CustomException(
|
127
|
+
ErrorCode.INTERNAL_ERROR,
|
128
|
+
detail=str(e),
|
129
|
+
source_function="rate_limit",
|
130
|
+
original_error=e
|
131
|
+
)
|
132
|
+
|
133
|
+
return wrapper
|
134
|
+
return decorator
|
135
|
+
|
71
136
|
async def create_jwt_token(
|
72
137
|
user_data: Dict[str, Any],
|
73
138
|
token_type: Literal["access", "refresh"],
|
74
|
-
|
75
|
-
log_model: Type[Base],
|
139
|
+
db_service: Any,
|
76
140
|
request: Optional[Request] = None
|
77
141
|
) -> str:
|
78
|
-
"""JWT 토큰을
|
79
|
-
|
80
|
-
Args:
|
81
|
-
user_data (Dict[str, Any]): 사용자 데이터
|
82
|
-
token_type (Literal["access", "refresh"]): 토큰 타입
|
83
|
-
session (AsyncSession): 데이터베이스 세션
|
84
|
-
log_model (Type[Base]): 로그 모델
|
85
|
-
request (Optional[Request], optional): FastAPI 요청 객체. Defaults to None.
|
86
|
-
|
87
|
-
Returns:
|
88
|
-
str: 생성된 JWT 토큰
|
89
|
-
|
90
|
-
Raises:
|
91
|
-
CustomException: 토큰 생성 실패 시
|
92
|
-
"""
|
142
|
+
"""JWT 토큰을 생성하고 로그를 기록합니다."""
|
93
143
|
try:
|
94
144
|
settings = get_settings()
|
95
145
|
|
@@ -98,7 +148,7 @@ async def create_jwt_token(
|
|
98
148
|
token_data = {
|
99
149
|
# 등록 클레임
|
100
150
|
"iss": settings.token_issuer,
|
101
|
-
"sub": user_data["
|
151
|
+
"sub": user_data["username"],
|
102
152
|
"aud": settings.token_audience,
|
103
153
|
"exp": expires_at,
|
104
154
|
|
@@ -123,7 +173,7 @@ async def create_jwt_token(
|
|
123
173
|
expires_at = datetime.now(UTC) + timedelta(days=14)
|
124
174
|
token_data = {
|
125
175
|
"iss": settings.token_issuer,
|
126
|
-
"sub": user_data["
|
176
|
+
"sub": user_data["username"],
|
127
177
|
"exp": expires_at,
|
128
178
|
"token_type": token_type,
|
129
179
|
"user_ulid": user_data["ulid"]
|
@@ -146,13 +196,14 @@ async def create_jwt_token(
|
|
146
196
|
# 로그 생성
|
147
197
|
try:
|
148
198
|
activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
199
|
+
await db_service.create_log(
|
200
|
+
{
|
201
|
+
"type": activity_type,
|
202
|
+
"user_ulid": user_data["ulid"],
|
203
|
+
"token": token
|
204
|
+
},
|
205
|
+
request
|
153
206
|
)
|
154
|
-
session.add(log_entry)
|
155
|
-
await session.flush()
|
156
207
|
except Exception as e:
|
157
208
|
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
158
209
|
logging.error(f"Failed to create token log: {str(e)}")
|
@@ -173,18 +224,7 @@ async def verify_jwt_token(
|
|
173
224
|
token: str,
|
174
225
|
expected_type: Optional[Literal["access", "refresh"]] = None
|
175
226
|
) -> Dict[str, Any]:
|
176
|
-
"""JWT 토큰을 검증합니다.
|
177
|
-
|
178
|
-
Args:
|
179
|
-
token: 검증할 JWT 토큰
|
180
|
-
expected_type: 예상되는 토큰 타입
|
181
|
-
|
182
|
-
Returns:
|
183
|
-
Dict[str, Any]: 토큰 페이로드
|
184
|
-
|
185
|
-
Raises:
|
186
|
-
CustomException: 토큰 검증 실패 시
|
187
|
-
"""
|
227
|
+
"""JWT 토큰을 검증합니다."""
|
188
228
|
try:
|
189
229
|
settings = get_settings()
|
190
230
|
|
@@ -202,23 +242,14 @@ async def verify_jwt_token(
|
|
202
242
|
if not token_type:
|
203
243
|
raise CustomException(
|
204
244
|
ErrorCode.INVALID_TOKEN,
|
205
|
-
detail=
|
245
|
+
detail=token,
|
206
246
|
source_function="security.verify_jwt_token"
|
207
247
|
)
|
208
248
|
|
209
249
|
if expected_type and token_type != expected_type:
|
210
250
|
raise CustomException(
|
211
251
|
ErrorCode.INVALID_TOKEN,
|
212
|
-
detail=
|
213
|
-
source_function="security.verify_jwt_token"
|
214
|
-
)
|
215
|
-
|
216
|
-
# 사용자 식별자 검증
|
217
|
-
user_ulid = payload.get("user_ulid")
|
218
|
-
if not user_ulid:
|
219
|
-
raise CustomException(
|
220
|
-
ErrorCode.INVALID_TOKEN,
|
221
|
-
detail="User identifier is missing",
|
252
|
+
detail=token,
|
222
253
|
source_function="security.verify_jwt_token"
|
223
254
|
)
|
224
255
|
|
@@ -227,7 +258,7 @@ async def verify_jwt_token(
|
|
227
258
|
except JWTError as e:
|
228
259
|
raise CustomException(
|
229
260
|
ErrorCode.INVALID_TOKEN,
|
230
|
-
detail=
|
261
|
+
detail=token,
|
231
262
|
source_function="security.verify_jwt_token",
|
232
263
|
original_error=e
|
233
264
|
)
|
@@ -239,4 +270,22 @@ async def verify_jwt_token(
|
|
239
270
|
detail=str(e),
|
240
271
|
source_function="security.verify_jwt_token",
|
241
272
|
original_error=e
|
273
|
+
)
|
274
|
+
|
275
|
+
def validate_token(token: str) -> Dict[str, Any]:
|
276
|
+
"""JWT 토큰을 검증하고 페이로드를 반환합니다."""
|
277
|
+
try:
|
278
|
+
settings = get_settings()
|
279
|
+
payload = jwt.decode(
|
280
|
+
token,
|
281
|
+
settings.jwt_secret,
|
282
|
+
algorithms=[settings.jwt_algorithm]
|
283
|
+
)
|
284
|
+
return payload
|
285
|
+
except JWTError as e:
|
286
|
+
raise CustomException(
|
287
|
+
ErrorCode.INVALID_TOKEN,
|
288
|
+
detail=token,
|
289
|
+
source_function="security.validate_token",
|
290
|
+
original_error=e
|
242
291
|
)
|