aiteamutils 0.2.52__tar.gz → 0.2.53__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.52
3
+ Version: 0.2.53
4
4
  Summary: AI Team Utilities
5
5
  Project-URL: Homepage, https://github.com/yourusername/aiteamutils
6
6
  Project-URL: Issues, https://github.com/yourusername/aiteamutils/issues
@@ -1,12 +1,10 @@
1
1
  """보안 관련 유틸리티."""
2
2
  from datetime import datetime, timedelta, UTC
3
- from typing import Dict, Any, Optional, Literal, Callable, Type
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
- session: AsyncSession,
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["ulid"],
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["ulid"],
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
- log_entry = log_model(
150
- type=activity_type,
151
- user_ulid=user_data["ulid"],
152
- token=token
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="Token type is missing",
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=f"Expected {expected_type} token but got {token_type}",
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=str(e),
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
  )
@@ -0,0 +1,2 @@
1
+ """버전 정보"""
2
+ __version__ = "0.2.53"
@@ -1,2 +0,0 @@
1
- """버전 정보"""
2
- __version__ = "0.2.52"
File without changes
File without changes
File without changes
File without changes