aiteamutils 0.2.52__py3-none-any.whl → 0.2.53__py3-none-any.whl

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/security.py CHANGED
@@ -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
  )
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.52"
2
+ __version__ = "0.2.53"
@@ -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
@@ -8,9 +8,9 @@ aiteamutils/database.py,sha256=uSMNWJge5RuQI7zJBJVX24fAHcSPChl-95_2TwK_GOw,39654
8
8
  aiteamutils/dependencies.py,sha256=q-OrEOJh4xEpc7ag6nTyey1pQwK9G0ZEDgXB_iTbaM0,6449
9
9
  aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
10
10
  aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
11
- aiteamutils/security.py,sha256=AUcGlNZeURF9AIF87p9SvWYKVao2_CISk8B6WasF-mo,8050
11
+ aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
12
12
  aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
13
- aiteamutils/version.py,sha256=jkNi_8VOucj0ySHwq10MaZLBXEjXin60tdcDfPSEO5A,42
14
- aiteamutils-0.2.52.dist-info/METADATA,sha256=1XX034T6pG0yeYvGKWcPiSbUpmGF1NFlP8wQELSqyRo,1718
15
- aiteamutils-0.2.52.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- aiteamutils-0.2.52.dist-info/RECORD,,
13
+ aiteamutils/version.py,sha256=l6fF0dCvrM5gxQHard3VFOKO2YQLlkx_RlQ9Azm0-pk,42
14
+ aiteamutils-0.2.53.dist-info/METADATA,sha256=LKH9v0dzZbm_PRCEZ-NS-ASC1p6pRyU3h8Mk6Nw4IYE,1718
15
+ aiteamutils-0.2.53.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ aiteamutils-0.2.53.dist-info/RECORD,,