aiteamutils 0.2.73__py3-none-any.whl → 0.2.76__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/base_model.py +12 -4
- aiteamutils/base_repository.py +88 -5
- aiteamutils/base_service.py +199 -20
- aiteamutils/database.py +396 -53
- aiteamutils/exceptions.py +6 -2
- aiteamutils/security.py +32 -47
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.73.dist-info → aiteamutils-0.2.76.dist-info}/METADATA +1 -1
- aiteamutils-0.2.76.dist-info/RECORD +15 -0
- aiteamutils-0.2.73.dist-info/RECORD +0 -15
- {aiteamutils-0.2.73.dist-info → aiteamutils-0.2.76.dist-info}/WHEEL +0 -0
aiteamutils/base_model.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from datetime import datetime, timezone
|
1
|
+
from datetime import datetime, timedelta, timezone
|
2
2
|
from typing import Any, Dict, TypeVar, Generic, Optional
|
3
3
|
from ulid import ULID
|
4
4
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
@@ -23,10 +23,18 @@ class BaseColumn(Base):
|
|
23
23
|
doc="ULID",
|
24
24
|
nullable=False
|
25
25
|
)
|
26
|
-
created_at: Mapped[datetime] = mapped_column(
|
26
|
+
created_at: Mapped[datetime] = mapped_column(
|
27
|
+
default=datetime.now(timezone.utc),
|
28
|
+
index=True
|
29
|
+
)
|
27
30
|
updated_at: Mapped[datetime] = mapped_column(
|
28
|
-
default=datetime.
|
29
|
-
onupdate=datetime.
|
31
|
+
default=datetime.now(timezone.utc),
|
32
|
+
onupdate=datetime.now(timezone.utc),
|
33
|
+
index=True
|
34
|
+
)
|
35
|
+
deleted_at: Mapped[datetime] = mapped_column(
|
36
|
+
default=None,
|
37
|
+
nullable=True
|
30
38
|
)
|
31
39
|
is_deleted: Mapped[bool] = mapped_column(
|
32
40
|
default=False,
|
aiteamutils/base_repository.py
CHANGED
@@ -7,7 +7,13 @@ from sqlalchemy import select
|
|
7
7
|
|
8
8
|
#패키지 라이브러리
|
9
9
|
from .exceptions import ErrorCode, CustomException
|
10
|
-
from .database import
|
10
|
+
from .database import (
|
11
|
+
list_entities,
|
12
|
+
get_entity,
|
13
|
+
create_entity,
|
14
|
+
update_entity,
|
15
|
+
delete_entity
|
16
|
+
)
|
11
17
|
|
12
18
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
13
19
|
|
@@ -29,13 +35,67 @@ class BaseRepository(Generic[ModelType]):
|
|
29
35
|
source_function=f"{self.__class__.__name__}.session"
|
30
36
|
)
|
31
37
|
self._session = value
|
38
|
+
|
39
|
+
#######################
|
40
|
+
# 입력 및 수정, 삭제 #
|
41
|
+
#######################
|
42
|
+
async def create(
|
43
|
+
self,
|
44
|
+
entity_data: Dict[str, Any],
|
45
|
+
exclude_entities: List[str] | None = None
|
46
|
+
) -> ModelType:
|
47
|
+
try:
|
48
|
+
return await create_entity(
|
49
|
+
session=self.session,
|
50
|
+
model=self.model,
|
51
|
+
entity_data=entity_data,
|
52
|
+
exclude_entities=exclude_entities
|
53
|
+
)
|
54
|
+
except CustomException as e:
|
55
|
+
raise e
|
56
|
+
except Exception as e:
|
57
|
+
raise CustomException(
|
58
|
+
ErrorCode.INTERNAL_ERROR,
|
59
|
+
detail=str(e),
|
60
|
+
source_function=f"{self.__class__.__name__}.create",
|
61
|
+
original_error=e
|
62
|
+
)
|
63
|
+
|
64
|
+
async def update(
|
65
|
+
self,
|
66
|
+
update_data: Dict[str, Any],
|
67
|
+
conditions: Dict[str, Any]
|
68
|
+
) -> ModelType:
|
69
|
+
try:
|
70
|
+
return await update_entity(
|
71
|
+
session=self.session,
|
72
|
+
model=self.model,
|
73
|
+
update_data=update_data,
|
74
|
+
conditions=conditions
|
75
|
+
)
|
76
|
+
except CustomException as e:
|
77
|
+
raise e
|
78
|
+
|
79
|
+
async def delete(
|
80
|
+
self,
|
81
|
+
conditions: Dict[str, Any]
|
82
|
+
) -> None:
|
83
|
+
await delete_entity(
|
84
|
+
session=self.session,
|
85
|
+
model=self.model,
|
86
|
+
conditions=conditions
|
87
|
+
)
|
32
88
|
|
89
|
+
#########################
|
90
|
+
# 조회 및 검색 메서드 #
|
91
|
+
#########################
|
33
92
|
async def list(
|
34
93
|
self,
|
35
94
|
skip: int = 0,
|
36
95
|
limit: int = 100,
|
37
96
|
filters: Optional[Dict[str, Any]] = None,
|
38
|
-
|
97
|
+
explicit_joins: Optional[List[Any]] = None,
|
98
|
+
loading_joins: Optional[List[Any]] = None
|
39
99
|
) -> List[ModelType]:
|
40
100
|
"""
|
41
101
|
엔티티 목록 조회.
|
@@ -48,11 +108,10 @@ class BaseRepository(Generic[ModelType]):
|
|
48
108
|
skip=skip,
|
49
109
|
limit=limit,
|
50
110
|
filters=filters,
|
51
|
-
|
111
|
+
explicit_joins=explicit_joins,
|
112
|
+
loading_joins=loading_joins
|
52
113
|
)
|
53
114
|
except CustomException as e:
|
54
|
-
e.detail = f"Repository list error for {self.model.__tablename__}: {e.detail}"
|
55
|
-
e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
|
56
115
|
raise e
|
57
116
|
except Exception as e:
|
58
117
|
raise CustomException(
|
@@ -61,3 +120,27 @@ class BaseRepository(Generic[ModelType]):
|
|
61
120
|
source_function=f"{self.__class__.__name__}.list",
|
62
121
|
original_error=e
|
63
122
|
)
|
123
|
+
|
124
|
+
async def get(
|
125
|
+
self,
|
126
|
+
conditions: Dict[str, Any] | None = None,
|
127
|
+
explicit_joins: Optional[List[Any]] = None,
|
128
|
+
loading_joins: Optional[List[Any]] = None
|
129
|
+
) -> ModelType:
|
130
|
+
try:
|
131
|
+
return await get_entity(
|
132
|
+
session=self.session,
|
133
|
+
model=self.model,
|
134
|
+
conditions=conditions,
|
135
|
+
explicit_joins=explicit_joins,
|
136
|
+
loading_joins=loading_joins
|
137
|
+
)
|
138
|
+
except CustomException as e:
|
139
|
+
raise e
|
140
|
+
except Exception as e:
|
141
|
+
raise CustomException(
|
142
|
+
ErrorCode.INTERNAL_ERROR,
|
143
|
+
detail=str(e),
|
144
|
+
source_function=f"{self.__class__.__name__}.get",
|
145
|
+
original_error=e
|
146
|
+
)
|
aiteamutils/base_service.py
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
#기본 라이브러리
|
2
2
|
from fastapi import Request
|
3
|
-
from typing import TypeVar, Generic, Type, Dict, Any, Union, List
|
3
|
+
from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional
|
4
4
|
from sqlalchemy.orm import DeclarativeBase
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
6
6
|
from datetime import datetime
|
7
|
+
from ulid import ULID
|
7
8
|
|
8
9
|
#패키지 라이브러리
|
9
10
|
from .exceptions import ErrorCode, CustomException
|
10
11
|
from .base_repository import BaseRepository
|
11
12
|
from .database import (
|
12
13
|
process_response,
|
13
|
-
|
14
|
+
validate_unique_fields
|
14
15
|
)
|
15
|
-
|
16
|
+
from .security import hash_password
|
16
17
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
17
18
|
|
18
19
|
class BaseService(Generic[ModelType]):
|
@@ -30,26 +31,149 @@ class BaseService(Generic[ModelType]):
|
|
30
31
|
self.repository = repository
|
31
32
|
self.db_session = db_session
|
32
33
|
self.additional_models = additional_models or {},
|
34
|
+
|
35
|
+
#######################
|
36
|
+
# 입력 및 수정, 삭제 #
|
37
|
+
#######################
|
38
|
+
async def create(
|
39
|
+
self,
|
40
|
+
entity_data: Dict[str, Any],
|
41
|
+
model_name: str | None = None,
|
42
|
+
response_model: Any = None,
|
43
|
+
exclude_entities: List[str] | None = None,
|
44
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
45
|
+
fk_check: List[Dict[str, Any]] | None = None,
|
46
|
+
) -> ModelType:
|
47
|
+
|
48
|
+
try:
|
49
|
+
async with self.db_session.begin():
|
50
|
+
# 고유 검사 수행
|
51
|
+
if unique_check:
|
52
|
+
await validate_unique_fields(self.db_session, unique_check, find_value=True)
|
53
|
+
# 외래 키 검사 수행
|
54
|
+
if fk_check:
|
55
|
+
await validate_unique_fields(self.db_session, fk_check, find_value=False)
|
56
|
+
|
57
|
+
# repository의 create 메서드를 트랜잭션 내에서 실행
|
58
|
+
return await self.repository.create(
|
59
|
+
entity_data=entity_data,
|
60
|
+
exclude_entities=exclude_entities
|
61
|
+
)
|
62
|
+
except CustomException as e:
|
63
|
+
raise e
|
64
|
+
except Exception as e:
|
65
|
+
# 다른 예외 처리
|
66
|
+
raise CustomException(
|
67
|
+
ErrorCode.INTERNAL_ERROR,
|
68
|
+
detail=str(e),
|
69
|
+
source_function=f"{self.__class__.__name__}.create",
|
70
|
+
original_error=e
|
71
|
+
)
|
72
|
+
|
73
|
+
async def update(
|
74
|
+
self,
|
75
|
+
ulid: str | None = None,
|
76
|
+
update_data: Dict[str, Any] | None = None,
|
77
|
+
conditions: Dict[str, Any] | None = None,
|
78
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
79
|
+
exclude_entities: List[str] | None = None
|
80
|
+
) -> ModelType:
|
81
|
+
try:
|
82
|
+
# 고유 검사 수행
|
83
|
+
if unique_check:
|
84
|
+
await validate_unique_fields(self.db_session, unique_check, find_value=True)
|
85
|
+
# 비밀번호가 있으면 해시 처리
|
86
|
+
if "password" in update_data:
|
87
|
+
update_data["password"] = hash_password(update_data["password"])
|
88
|
+
# 제외할 엔티티가 있으면 제외
|
89
|
+
if exclude_entities:
|
90
|
+
update_data = {k: v for k, v in update_data.items() if k not in exclude_entities}
|
91
|
+
|
92
|
+
if not ulid and not conditions:
|
93
|
+
raise CustomException(
|
94
|
+
ErrorCode.INVALID_INPUT,
|
95
|
+
detail="Either 'ulid' or 'conditions' must be provided.",
|
96
|
+
source_function="database.update_entity"
|
97
|
+
)
|
98
|
+
|
99
|
+
# ulid로 조건 생성
|
100
|
+
if ulid:
|
101
|
+
if not ULID.from_str(ulid):
|
102
|
+
raise CustomException(
|
103
|
+
ErrorCode.VALIDATION_ERROR,
|
104
|
+
detail=ulid,
|
105
|
+
source_function=f"{self.__class__.__name__}.update"
|
106
|
+
)
|
107
|
+
|
108
|
+
conditions = {"ulid": ulid}
|
109
|
+
|
110
|
+
return await self.repository.update(
|
111
|
+
update_data=update_data,
|
112
|
+
conditions=conditions
|
113
|
+
)
|
114
|
+
except CustomException as e:
|
115
|
+
raise e
|
116
|
+
except Exception as e:
|
117
|
+
raise CustomException(
|
118
|
+
ErrorCode.INTERNAL_ERROR,
|
119
|
+
detail=str(e),
|
120
|
+
source_function=f"{self.__class__.__name__}.update",
|
121
|
+
original_error=e
|
122
|
+
)
|
123
|
+
|
124
|
+
async def delete(
|
125
|
+
self,
|
126
|
+
ulid: str | None = None,
|
127
|
+
conditions: Dict[str, Any] | None = None
|
128
|
+
) -> None:
|
129
|
+
try:
|
130
|
+
if not ULID.from_str(ulid):
|
131
|
+
raise CustomException(
|
132
|
+
ErrorCode.VALIDATION_ERROR,
|
133
|
+
detail=ulid,
|
134
|
+
source_function=f"{self.__class__.__name__}.delete"
|
135
|
+
)
|
136
|
+
|
137
|
+
if not ulid and not conditions:
|
138
|
+
raise CustomException(
|
139
|
+
ErrorCode.INVALID_INPUT,
|
140
|
+
detail="Either 'ulid' or 'conditions' must be provided.",
|
141
|
+
source_function="database.update_entity"
|
142
|
+
)
|
33
143
|
|
144
|
+
# ulid로 조건 생성
|
145
|
+
if ulid:
|
146
|
+
conditions = {"ulid": ulid}
|
147
|
+
|
148
|
+
conditions["is_deleted"] = False
|
149
|
+
|
150
|
+
return await self.repository.delete(
|
151
|
+
conditions=conditions
|
152
|
+
)
|
153
|
+
except CustomException as e:
|
154
|
+
raise e
|
155
|
+
except Exception as e:
|
156
|
+
raise CustomException(
|
157
|
+
ErrorCode.INTERNAL_ERROR,
|
158
|
+
detail=str(e),
|
159
|
+
source_function=f"{self.__class__.__name__}.delete",
|
160
|
+
original_error=e
|
161
|
+
)
|
162
|
+
|
163
|
+
#########################
|
164
|
+
# 조회 및 검색 메서드 #
|
165
|
+
#########################
|
34
166
|
async def list(
|
35
167
|
self,
|
36
168
|
skip: int = 0,
|
37
169
|
limit: int = 100,
|
38
|
-
filters: Dict[str, Any] | None = None,
|
39
|
-
search_params: Dict[str, Any] | None = None,
|
170
|
+
filters: List[Dict[str, Any]] | None = None,
|
40
171
|
model_name: str | None = None,
|
41
|
-
response_model: Any = None
|
172
|
+
response_model: Any = None,
|
173
|
+
explicit_joins: Optional[List[Any]] = None,
|
174
|
+
loading_joins: Optional[List[Any]] = None
|
42
175
|
) -> List[Dict[str, Any]]:
|
43
176
|
try:
|
44
|
-
# 검색 조건 처리 및 필터 병합
|
45
|
-
if search_params:
|
46
|
-
search_filters = build_search_filters(search_params)
|
47
|
-
if filters:
|
48
|
-
# 기존 filters와 search_filters 병합 (search_filters가 우선 적용)
|
49
|
-
filters.update(search_filters)
|
50
|
-
else:
|
51
|
-
filters = search_filters
|
52
|
-
|
53
177
|
# 모델 이름을 통한 동적 처리
|
54
178
|
if model_name:
|
55
179
|
if model_name not in self.additional_models:
|
@@ -59,19 +183,74 @@ class BaseService(Generic[ModelType]):
|
|
59
183
|
source_function=f"{self.__class__.__name__}.list"
|
60
184
|
)
|
61
185
|
model = self.additional_models[model_name]
|
62
|
-
|
186
|
+
entities = await self.repository.list(
|
187
|
+
skip=skip,
|
188
|
+
limit=limit,
|
189
|
+
filters=filters,
|
190
|
+
model=model
|
191
|
+
)
|
192
|
+
return [process_response(entity, response_model) for entity in entities]
|
63
193
|
|
64
|
-
|
194
|
+
entities = await self.repository.list(
|
195
|
+
skip=skip,
|
196
|
+
limit=limit,
|
197
|
+
filters=filters,
|
198
|
+
explicit_joins=explicit_joins,
|
199
|
+
loading_joins=loading_joins
|
200
|
+
)
|
201
|
+
return [process_response(entity, response_model) for entity in entities]
|
202
|
+
|
65
203
|
except CustomException as e:
|
66
|
-
e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
|
67
|
-
e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
|
68
204
|
raise e
|
69
205
|
except Exception as e:
|
70
206
|
raise CustomException(
|
71
207
|
ErrorCode.INTERNAL_ERROR,
|
72
|
-
detail=str(e),
|
73
208
|
source_function=f"{self.__class__.__name__}.list",
|
74
209
|
original_error=e
|
75
210
|
)
|
211
|
+
|
212
|
+
async def get(
|
213
|
+
self,
|
214
|
+
ulid: str,
|
215
|
+
model_name: str | None = None,
|
216
|
+
response_model: Any = None,
|
217
|
+
conditions: Dict[str, Any] | None = None,
|
218
|
+
explicit_joins: Optional[List[Any]] = None,
|
219
|
+
loading_joins: Optional[List[Any]] = None
|
220
|
+
):
|
221
|
+
try:
|
222
|
+
if not ulid and not conditions:
|
223
|
+
raise CustomException(
|
224
|
+
ErrorCode.INVALID_INPUT,
|
225
|
+
detail="Either 'ulid' or 'conditions' must be provided.",
|
226
|
+
source_function="database.update_entity"
|
227
|
+
)
|
228
|
+
|
229
|
+
# ulid로 조건 생성
|
230
|
+
if ulid:
|
231
|
+
if not ULID.from_str(ulid):
|
232
|
+
raise CustomException(
|
233
|
+
ErrorCode.VALIDATION_ERROR,
|
234
|
+
detail=ulid,
|
235
|
+
source_function=f"{self.__class__.__name__}.update"
|
236
|
+
)
|
237
|
+
|
238
|
+
conditions = {"ulid": ulid}
|
239
|
+
|
240
|
+
entity = await self.repository.get(
|
241
|
+
conditions=conditions,
|
242
|
+
explicit_joins=explicit_joins,
|
243
|
+
loading_joins=loading_joins
|
244
|
+
)
|
245
|
+
return process_response(entity, response_model)
|
76
246
|
|
247
|
+
except CustomException as e:
|
248
|
+
raise e
|
249
|
+
except Exception as e:
|
250
|
+
raise CustomException(
|
251
|
+
ErrorCode.INTERNAL_ERROR,
|
252
|
+
detail=str(e),
|
253
|
+
source_function=f"{self.__class__.__name__}.get",
|
254
|
+
original_error=e
|
255
|
+
)
|
77
256
|
|
aiteamutils/database.py
CHANGED
@@ -1,22 +1,100 @@
|
|
1
1
|
#기본 라이브러리
|
2
|
-
from typing import
|
2
|
+
from typing import (
|
3
|
+
TypeVar,
|
4
|
+
Generic,
|
5
|
+
Type,
|
6
|
+
Any,
|
7
|
+
Dict,
|
8
|
+
List,
|
9
|
+
Optional,
|
10
|
+
AsyncGenerator
|
11
|
+
)
|
3
12
|
from sqlalchemy.ext.asyncio import AsyncSession
|
4
|
-
from sqlalchemy import select, and_
|
5
|
-
from sqlalchemy.orm import DeclarativeBase
|
13
|
+
from sqlalchemy import select, and_, or_
|
14
|
+
from sqlalchemy.orm import DeclarativeBase, joinedload, selectinload
|
6
15
|
from sqlalchemy.exc import SQLAlchemyError
|
7
16
|
from datetime import datetime
|
17
|
+
from contextlib import asynccontextmanager
|
18
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
19
|
+
from fastapi import Request
|
20
|
+
from ulid import ULID
|
21
|
+
from sqlalchemy import MetaData, Table, insert
|
8
22
|
|
9
23
|
#패키지 라이브러리
|
10
24
|
from .exceptions import ErrorCode, CustomException
|
11
25
|
|
12
|
-
|
13
26
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
14
27
|
|
15
28
|
##################
|
16
29
|
# 전처리 #
|
17
30
|
##################
|
31
|
+
def process_entity_data(
|
32
|
+
model: Type[ModelType],
|
33
|
+
entity_data: Dict[str, Any],
|
34
|
+
existing_data: Dict[str, Any] = None,
|
35
|
+
exclude_entities: List[str] | None = None
|
36
|
+
) -> Dict[str, Any]:
|
37
|
+
from .security import hash_password
|
38
|
+
|
39
|
+
"""
|
40
|
+
엔티티 데이터를 전처리하고 모델 속성과 extra_data를 분리합니다.
|
41
|
+
|
42
|
+
이 함수는 다음과 같은 작업을 수행합니다:
|
43
|
+
1. 모델의 기본 속성을 식별합니다.
|
44
|
+
2. Swagger 자동 생성 속성을 제외합니다.
|
45
|
+
3. 모델 속성에 해당하는 데이터는 model_data에 저장
|
46
|
+
4. 모델 속성에 없는 데이터는 extra_data에 저장
|
47
|
+
5. 기존 엔티티 데이터의 extra_data를 유지할 수 있습니다.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
model (Type[ModelType]): 데이터 모델 클래스
|
51
|
+
entity_data (Dict[str, Any]): 처리할 엔티티 데이터
|
52
|
+
existing_entity_data (Dict[str, Any], optional): 기존 엔티티 데이터. Defaults to None.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Dict[str, Any]: 전처리된 모델 데이터 (extra_data 포함)
|
56
|
+
"""
|
57
|
+
# 모델의 속성을 가져와서 전처리합니다.
|
58
|
+
model_attr = {
|
59
|
+
attr for attr in dir(model)
|
60
|
+
if not attr.startswith('_') and not callable(getattr(model, attr))
|
61
|
+
}
|
62
|
+
model_data = {}
|
63
|
+
extra_data = {}
|
64
|
+
|
65
|
+
# 기존 엔티티 데이터가 있으면 추가
|
66
|
+
if existing_data and "extra_data" in existing_data:
|
67
|
+
extra_data = existing_data["extra_data"].copy()
|
68
|
+
|
69
|
+
# 제외할 엔티티가 있으면 제거
|
70
|
+
if exclude_entities:
|
71
|
+
entity_data = {k: v for k, v in entity_data.items() if k not in exclude_entities}
|
72
|
+
|
73
|
+
# Swagger 자동 생성 속성 패턴
|
74
|
+
swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
|
75
|
+
|
76
|
+
for key, value in entity_data.items():
|
77
|
+
# Swagger 자동 생성 속성 무시
|
78
|
+
if key in swagger_patterns:
|
79
|
+
continue
|
80
|
+
|
81
|
+
# 패스워드 필드 처리
|
82
|
+
if key == "password":
|
83
|
+
value = hash_password(value) # 비밀번호 암호화
|
84
|
+
|
85
|
+
# 모델 속성에 있는 경우 model_data에 추가
|
86
|
+
if key in model_attr:
|
87
|
+
model_data[key] = value
|
88
|
+
|
89
|
+
# 모델 속성에 없는 경우 extra_data에 추가
|
90
|
+
else:
|
91
|
+
extra_data[key] = value
|
18
92
|
|
93
|
+
# extra_data가 있고 모델에 extra_data 속성이 있는 경우 추가
|
94
|
+
if extra_data and "extra_data" in model_attr:
|
95
|
+
model_data["extra_data"] = extra_data
|
19
96
|
|
97
|
+
return model_data
|
20
98
|
|
21
99
|
##################
|
22
100
|
# 응답 처리 #
|
@@ -115,79 +193,224 @@ def process_response(
|
|
115
193
|
|
116
194
|
return result
|
117
195
|
|
118
|
-
|
119
196
|
##################
|
120
197
|
# 조건 처리 #
|
121
198
|
##################
|
122
|
-
def build_search_filters(
|
123
|
-
search_params: Dict[str, Dict[str, Any]]
|
124
|
-
) -> Dict[str, Any]:
|
125
|
-
"""
|
126
|
-
검색 파라미터를 기반으로 필터 조건을 생성합니다.
|
127
|
-
|
128
|
-
Args:
|
129
|
-
search_params: 검색 조건 설정을 위한 파라미터.
|
130
|
-
|
131
|
-
Returns:
|
132
|
-
filters: 필터 조건 딕셔너리.
|
133
|
-
"""
|
134
|
-
filters = {}
|
135
|
-
for key, param in search_params.items():
|
136
|
-
value = param.get("value")
|
137
|
-
if value is not None:
|
138
|
-
if param.get("like", False):
|
139
|
-
filters[key] = {"field": param["fields"][0], "operator": "like", "value": f"%{value}%"}
|
140
|
-
else:
|
141
|
-
filters[key] = {"field": param["fields"][0], "operator": "eq", "value": value}
|
142
|
-
return filters
|
143
|
-
|
144
199
|
def build_conditions(
|
145
|
-
|
146
|
-
|
200
|
+
filters: List[Dict[str, Any]],
|
201
|
+
model: Type[ModelType]
|
147
202
|
) -> List[Any]:
|
148
203
|
"""
|
149
204
|
필터 조건을 기반으로 SQLAlchemy 조건 리스트를 생성합니다.
|
150
205
|
|
151
206
|
Args:
|
152
|
-
filters: 필터 조건
|
207
|
+
filters: 필터 조건 리스트.
|
153
208
|
model: SQLAlchemy 모델 클래스.
|
154
209
|
|
155
210
|
Returns:
|
156
211
|
List[Any]: SQLAlchemy 조건 리스트.
|
157
212
|
"""
|
158
213
|
conditions = []
|
159
|
-
for filter_data in filters.values():
|
160
|
-
if "." in filter_data["field"]:
|
161
|
-
# 관계를 따라 암묵적으로 연결된 모델의 필드 가져오기
|
162
|
-
related_model_name, field_name = filter_data["field"].split(".")
|
163
|
-
relationship_property = getattr(model, related_model_name)
|
164
|
-
related_model = relationship_property.property.mapper.class_
|
165
|
-
field = getattr(related_model, field_name)
|
166
|
-
else:
|
167
|
-
# 현재 모델의 필드 가져오기
|
168
|
-
field = getattr(model, filter_data["field"])
|
169
214
|
|
170
|
-
|
171
|
-
|
172
|
-
value
|
215
|
+
for filter_item in filters:
|
216
|
+
value = filter_item.get("value")
|
217
|
+
if not value: # 값이 없으면 건너뜀
|
218
|
+
continue
|
219
|
+
|
220
|
+
operator = filter_item.get("operator", "eq")
|
221
|
+
or_conditions = []
|
222
|
+
|
223
|
+
for field_path in filter_item.get("fields", []):
|
224
|
+
current_model = model
|
173
225
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
226
|
+
# 관계를 따라 필드 가져오기
|
227
|
+
for part in field_path.split(".")[:-1]:
|
228
|
+
relationship_property = getattr(current_model, part)
|
229
|
+
current_model = relationship_property.property.mapper.class_
|
230
|
+
|
231
|
+
field = getattr(current_model, field_path.split(".")[-1])
|
232
|
+
|
233
|
+
# 조건 생성
|
234
|
+
if operator == "like":
|
235
|
+
or_conditions.append(field.ilike(f"%{value}%"))
|
236
|
+
elif operator == "eq":
|
237
|
+
or_conditions.append(field == value)
|
238
|
+
|
239
|
+
if or_conditions: # OR 조건이 있을 때만 추가
|
240
|
+
conditions.append(or_(*or_conditions))
|
178
241
|
|
179
242
|
return conditions
|
180
243
|
|
181
244
|
##################
|
182
245
|
# 쿼리 실행 #
|
183
246
|
##################
|
247
|
+
async def create_entity(
|
248
|
+
session: AsyncSession,
|
249
|
+
model: Type[ModelType],
|
250
|
+
entity_data: Dict[str, Any],
|
251
|
+
exclude_entities: List[str] | None = None
|
252
|
+
) -> ModelType:
|
253
|
+
"""
|
254
|
+
새로운 엔티티를 데이터베이스에 생성합니다.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
session (AsyncSession): 데이터베이스 세션
|
258
|
+
model (Type[ModelType]): 생성할 모델 클래스
|
259
|
+
entity_data (Dict[str, Any]): 엔티티 생성에 필요한 데이터
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
ModelType: 생성된 엔티티
|
263
|
+
|
264
|
+
Raises:
|
265
|
+
CustomException: 엔티티 생성 중 발생하는 데이터베이스 오류
|
266
|
+
"""
|
267
|
+
try:
|
268
|
+
# 엔티티 데이터 전처리
|
269
|
+
processed_data = process_entity_data(
|
270
|
+
model=model,
|
271
|
+
entity_data=entity_data,
|
272
|
+
exclude_entities=exclude_entities
|
273
|
+
)
|
274
|
+
|
275
|
+
# 새로운 엔티티 생성
|
276
|
+
entity = model(**processed_data)
|
277
|
+
|
278
|
+
# 세션에 엔티티 추가
|
279
|
+
session.add(entity)
|
280
|
+
|
281
|
+
# 데이터베이스에 커밋
|
282
|
+
await session.flush()
|
283
|
+
await session.refresh(entity)
|
284
|
+
|
285
|
+
# 생성된 엔티티 반환
|
286
|
+
return entity
|
287
|
+
|
288
|
+
except SQLAlchemyError as e:
|
289
|
+
# 데이터베이스 오류 발생 시 CustomException으로 변환
|
290
|
+
raise CustomException(
|
291
|
+
ErrorCode.DB_CREATE_ERROR,
|
292
|
+
detail=f"{model.__name__}|{str(e)}",
|
293
|
+
source_function="database.create_entity",
|
294
|
+
original_error=e
|
295
|
+
)
|
296
|
+
|
297
|
+
async def update_entity(
|
298
|
+
session: AsyncSession,
|
299
|
+
model: Type[ModelType],
|
300
|
+
conditions: Dict[str, Any],
|
301
|
+
update_data: Dict[str, Any]
|
302
|
+
) -> ModelType:
|
303
|
+
"""
|
304
|
+
조건을 기반으로 엔티티를 조회하고 업데이트합니다.
|
305
|
+
|
306
|
+
Args:
|
307
|
+
session (AsyncSession): 데이터베이스 세션
|
308
|
+
model (Type[ModelType]): 업데이트할 모델 클래스
|
309
|
+
conditions (Dict[str, Any]): 엔티티 조회 조건
|
310
|
+
conditions = {"user_id": 1, "status": "active"}
|
311
|
+
update_data (Dict[str, Any]): 업데이트할 데이터
|
312
|
+
update_data = {"status": "inactive"}
|
313
|
+
Returns:
|
314
|
+
ModelType: 업데이트된 엔티티
|
315
|
+
|
316
|
+
Raises:
|
317
|
+
CustomException: 엔티티 조회 또는 업데이트 중 발생하는 데이터베이스 오류
|
318
|
+
"""
|
319
|
+
try:
|
320
|
+
# 조건 기반 엔티티 조회
|
321
|
+
stmt = select(model)
|
322
|
+
for key, value in conditions.items():
|
323
|
+
stmt = stmt.where(getattr(model, key) == value)
|
324
|
+
|
325
|
+
result = await session.execute(stmt)
|
326
|
+
entity = result.scalar_one_or_none()
|
327
|
+
|
328
|
+
if not entity:
|
329
|
+
raise CustomException(
|
330
|
+
ErrorCode.NOT_FOUND,
|
331
|
+
detail=f"{model.__name__}|{conditions}.",
|
332
|
+
source_function="database.update_entity"
|
333
|
+
)
|
334
|
+
|
335
|
+
# 기존 데이터를 딕셔너리로 변환
|
336
|
+
existing_data = {
|
337
|
+
column.name: getattr(entity, column.name)
|
338
|
+
for column in entity.__table__.columns
|
339
|
+
}
|
340
|
+
|
341
|
+
# 데이터 병합 및 전처리
|
342
|
+
processed_data = process_entity_data(model, update_data, existing_data)
|
343
|
+
|
344
|
+
# 엔티티 데이터 업데이트
|
345
|
+
for key, value in processed_data.items():
|
346
|
+
if hasattr(entity, key):
|
347
|
+
setattr(entity, key, value)
|
348
|
+
|
349
|
+
# 변경 사항 커밋
|
350
|
+
await session.flush()
|
351
|
+
await session.refresh(entity)
|
352
|
+
|
353
|
+
return entity
|
354
|
+
|
355
|
+
except SQLAlchemyError as e:
|
356
|
+
raise CustomException(
|
357
|
+
ErrorCode.DB_UPDATE_ERROR,
|
358
|
+
detail=f"{model.__name__}|{conditions}",
|
359
|
+
source_function="database.update_entity",
|
360
|
+
original_error=e
|
361
|
+
)
|
362
|
+
|
363
|
+
async def delete_entity(
|
364
|
+
session: AsyncSession,
|
365
|
+
model: Type[ModelType],
|
366
|
+
conditions: Dict[str, Any]
|
367
|
+
) -> None:
|
368
|
+
try:
|
369
|
+
stmt = select(model)
|
370
|
+
for key, value in conditions.items():
|
371
|
+
stmt = stmt.where(getattr(model, key) == value)
|
372
|
+
|
373
|
+
result = await session.execute(stmt)
|
374
|
+
entity = result.scalar_one_or_none()
|
375
|
+
|
376
|
+
if not entity:
|
377
|
+
raise CustomException(
|
378
|
+
ErrorCode.NOT_FOUND,
|
379
|
+
detail=f"{model.__name__}|{conditions}.",
|
380
|
+
source_function="database.delete_entity"
|
381
|
+
)
|
382
|
+
|
383
|
+
entity.is_deleted = True
|
384
|
+
entity.deleted_at = datetime.now()
|
385
|
+
|
386
|
+
await session.flush()
|
387
|
+
await session.refresh(entity)
|
388
|
+
|
389
|
+
except SQLAlchemyError as e:
|
390
|
+
raise CustomException(
|
391
|
+
ErrorCode.DB_DELETE_ERROR,
|
392
|
+
detail=f"{model.__name__}|{conditions}",
|
393
|
+
source_function="database.delete_entity",
|
394
|
+
original_error=e
|
395
|
+
)
|
396
|
+
|
397
|
+
async def purge_entity(
|
398
|
+
session: AsyncSession,
|
399
|
+
model: Type[ModelType],
|
400
|
+
entity: ModelType
|
401
|
+
) -> None:
|
402
|
+
# 엔티티를 영구 삭제합니다.
|
403
|
+
await session.delete(entity)
|
404
|
+
await session.commit()
|
405
|
+
|
184
406
|
async def list_entities(
|
185
407
|
session: AsyncSession,
|
186
408
|
model: Type[ModelType],
|
187
409
|
skip: int = 0,
|
188
410
|
limit: int = 100,
|
189
411
|
filters: Optional[Dict[str, Any]] = None,
|
190
|
-
|
412
|
+
explicit_joins: Optional[List[Any]] = None,
|
413
|
+
loading_joins: Optional[List[Any]] = None
|
191
414
|
) -> List[Dict[str, Any]]:
|
192
415
|
"""
|
193
416
|
엔터티 리스트를 필터 및 조건에 따라 가져오는 함수.
|
@@ -218,20 +441,26 @@ async def list_entities(
|
|
218
441
|
try:
|
219
442
|
query = select(model)
|
220
443
|
|
444
|
+
# 명시적 조인 적용
|
445
|
+
if explicit_joins:
|
446
|
+
for join_target in explicit_joins:
|
447
|
+
query = query.join(join_target) # 명시적으로 정의된 조인 추가
|
448
|
+
|
449
|
+
# 조인 로딩 적용
|
450
|
+
if loading_joins:
|
451
|
+
for join_option in loading_joins:
|
452
|
+
query = query.options(join_option)
|
453
|
+
|
221
454
|
# 필터 조건 적용
|
222
455
|
if filters:
|
223
456
|
conditions = build_conditions(filters, model)
|
224
457
|
query = query.where(and_(*conditions))
|
225
458
|
|
226
|
-
# 조인 로딩 적용
|
227
|
-
if joins:
|
228
|
-
for join_option in joins:
|
229
|
-
query = query.options(join_option)
|
230
|
-
|
231
459
|
# 페이지네이션 적용
|
232
460
|
query = query.limit(limit).offset(skip)
|
233
461
|
|
234
462
|
result = await session.execute(query)
|
463
|
+
|
235
464
|
return result.scalars().unique().all()
|
236
465
|
except SQLAlchemyError as e:
|
237
466
|
raise CustomException(
|
@@ -240,3 +469,117 @@ async def list_entities(
|
|
240
469
|
source_function="database.list_entities",
|
241
470
|
original_error=e
|
242
471
|
)
|
472
|
+
|
473
|
+
async def get_entity(
|
474
|
+
session: AsyncSession,
|
475
|
+
model: Type[ModelType],
|
476
|
+
conditions: Dict[str, Any],
|
477
|
+
explicit_joins: Optional[List[Any]] = None,
|
478
|
+
loading_joins: Optional[List[Any]] = None
|
479
|
+
) -> ModelType:
|
480
|
+
try:
|
481
|
+
query = select(model)
|
482
|
+
|
483
|
+
if explicit_joins:
|
484
|
+
for join_target in explicit_joins:
|
485
|
+
query = query.join(join_target)
|
486
|
+
|
487
|
+
if loading_joins:
|
488
|
+
for join_option in loading_joins:
|
489
|
+
query = query.options(join_option)
|
490
|
+
|
491
|
+
if conditions:
|
492
|
+
for key, value in conditions.items():
|
493
|
+
query = query.where(getattr(model, key) == value)
|
494
|
+
|
495
|
+
result = await session.execute(query)
|
496
|
+
return result.scalars().unique().one_or_none()
|
497
|
+
|
498
|
+
except SQLAlchemyError as e:
|
499
|
+
raise CustomException(
|
500
|
+
ErrorCode.DB_READ_ERROR,
|
501
|
+
detail=str(e),
|
502
|
+
source_function="database.get_entity",
|
503
|
+
original_error=str(e)
|
504
|
+
)
|
505
|
+
|
506
|
+
##################
|
507
|
+
# 로그 등 #
|
508
|
+
##################
|
509
|
+
async def log_create(
|
510
|
+
session: AsyncSession,
|
511
|
+
model: str,
|
512
|
+
log_data: Dict[str, Any],
|
513
|
+
request: Optional[Request] = None
|
514
|
+
) -> None:
|
515
|
+
try:
|
516
|
+
# 사용자 에이전트 및 IP 주소 추가
|
517
|
+
if request:
|
518
|
+
log_data["user_agent"] = request.headers.get("user-agent")
|
519
|
+
log_data["ip_address"] = request.headers.get("x-forwarded-for") or request.client.host
|
520
|
+
|
521
|
+
await create_entity(
|
522
|
+
session=session,
|
523
|
+
model=model,
|
524
|
+
entity_data=log_data
|
525
|
+
)
|
526
|
+
except Exception as e:
|
527
|
+
raise CustomException(
|
528
|
+
ErrorCode.INTERNAL_ERROR,
|
529
|
+
detail=f"{model}|{str(e)}",
|
530
|
+
source_function="database.log_create",
|
531
|
+
original_error=e
|
532
|
+
)
|
533
|
+
|
534
|
+
######################
|
535
|
+
# 검증 #
|
536
|
+
######################
|
537
|
+
async def validate_unique_fields(
|
538
|
+
session: AsyncSession,
|
539
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
540
|
+
find_value: bool = True # True: 값이 있는지, False: 값이 없는지 확인
|
541
|
+
) -> None:
|
542
|
+
try:
|
543
|
+
for check in unique_check:
|
544
|
+
value = check["value"]
|
545
|
+
model = check["model"]
|
546
|
+
fields = check["fields"]
|
547
|
+
|
548
|
+
# 여러 개의 컬럼이 있을 경우 모든 조건을 만족해야 한다
|
549
|
+
conditions = [getattr(model, column) == value for column in fields]
|
550
|
+
|
551
|
+
# 쿼리 실행
|
552
|
+
query = select(model).where(or_(*conditions))
|
553
|
+
result = await session.execute(query)
|
554
|
+
existing = result.scalar_one_or_none()
|
555
|
+
|
556
|
+
# 값이 있는지 확인 (find_value=True) 또는 값이 없는지 확인 (find_value=False)
|
557
|
+
if find_value and existing: # 값이 존재하는 경우
|
558
|
+
raise CustomException(
|
559
|
+
ErrorCode.FIELD_INVALID_UNIQUE,
|
560
|
+
detail=f"{model.name}|{value}",
|
561
|
+
source_function="database.validate_unique_fields.existing"
|
562
|
+
)
|
563
|
+
elif not find_value and not existing: # 값이 존재하지 않는 경우
|
564
|
+
raise CustomException(
|
565
|
+
ErrorCode.FIELD_INVALID_NOT_EXIST,
|
566
|
+
detail=f"{model.name}|{value}",
|
567
|
+
source_function="database.validate_unique_fields.not_existing"
|
568
|
+
)
|
569
|
+
|
570
|
+
except CustomException as e:
|
571
|
+
# 특정 CustomException 처리
|
572
|
+
raise CustomException(
|
573
|
+
e.error_code,
|
574
|
+
detail=str(e),
|
575
|
+
source_function="database.validate_unique_fields.Exception",
|
576
|
+
original_error=e
|
577
|
+
)
|
578
|
+
except Exception as e:
|
579
|
+
# 알 수 없는 예외 처리
|
580
|
+
raise CustomException(
|
581
|
+
ErrorCode.INTERNAL_ERROR,
|
582
|
+
detail=f"Unexpected error: {str(e)}",
|
583
|
+
source_function="database.validate_unique_fields.Exception",
|
584
|
+
original_error=e
|
585
|
+
)
|
aiteamutils/exceptions.py
CHANGED
@@ -64,12 +64,16 @@ class ErrorCode(Enum):
|
|
64
64
|
VALIDATION_ERROR = ErrorResponse(4001, "VALIDATION_ERROR", 422, "유효성 검사 오류")
|
65
65
|
FIELD_INVALID_FORMAT = ErrorResponse(4002, "FIELD_INVALID_FORMAT", 400, "잘못된 형식입니다")
|
66
66
|
REQUIRED_FIELD_MISSING = ErrorResponse(4003, "VALIDATION_REQUIRED_FIELD_MISSING", 400, "필수 필드가 누락되었습니다")
|
67
|
+
FIELD_INVALID_UNIQUE = ErrorResponse(4004, "VALIDATION_FIELD_INVALID_UNIQUE", 400, "중복된 값이 존재합니다")
|
68
|
+
FIELD_INVALID_NOT_EXIST = ErrorResponse(4005, "VALIDATION_FIELD_INVALID_NOT_EXIST", 400, "존재하지 않는 값입니다")
|
67
69
|
|
68
70
|
# General 에러: 5000번대
|
69
71
|
NOT_FOUND = ErrorResponse(5001, "GENERAL_NOT_FOUND", 404, "리소스를 찾을 수 없습니다")
|
70
72
|
INTERNAL_ERROR = ErrorResponse(5002, "GENERAL_INTERNAL_ERROR", 500, "내부 서버 오류")
|
71
73
|
SERVICE_UNAVAILABLE = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
|
72
74
|
SERVICE_NOT_REGISTERED = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
|
75
|
+
LOGIN_ERROR = ErrorResponse(5004, "LOGIN_ERROR", 401, "로그인 오류")
|
76
|
+
TOKEN_ERROR = ErrorResponse(5005, "TOKEN_ERROR", 401, "토큰 오류")
|
73
77
|
|
74
78
|
class CustomException(Exception):
|
75
79
|
"""사용자 정의 예외 클래스"""
|
@@ -98,7 +102,7 @@ class CustomException(Exception):
|
|
98
102
|
|
99
103
|
self.error_chain.append({
|
100
104
|
"error_code": str(self.error_code),
|
101
|
-
"detail": str(self.detail),
|
105
|
+
"detail": str(self.detail) if self.detail else None,
|
102
106
|
"source_function": self.source_function,
|
103
107
|
"original_error": str(self.original_error) if self.original_error else None
|
104
108
|
})
|
@@ -115,7 +119,7 @@ class CustomException(Exception):
|
|
115
119
|
"""에러 정보를 딕셔너리로 반환합니다."""
|
116
120
|
return {
|
117
121
|
"error_code": self.error_code.name,
|
118
|
-
"detail": self.detail,
|
122
|
+
"detail": self.detail if self.detail else None,
|
119
123
|
"source_function": self.source_function,
|
120
124
|
"error_chain": self.error_chain,
|
121
125
|
"original_error": str(self.original_error) if self.original_error else None
|
aiteamutils/security.py
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
"""보안 관련 유틸리티."""
|
2
|
-
from datetime import datetime, timedelta,
|
2
|
+
from datetime import datetime, timedelta, timezone
|
3
3
|
from typing import Dict, Any, Optional, Literal, Callable, TYPE_CHECKING
|
4
4
|
from fastapi import Request, HTTPException, status
|
5
5
|
from functools import wraps
|
6
6
|
from jose import jwt, JWTError
|
7
7
|
from passlib.context import CryptContext
|
8
8
|
import logging
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
9
10
|
|
10
11
|
from .exceptions import CustomException, ErrorCode
|
11
12
|
from .enums import ActivityType
|
12
|
-
from .
|
13
|
+
from .database import log_create
|
13
14
|
|
14
15
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
15
16
|
|
@@ -84,7 +85,7 @@ def rate_limit(
|
|
84
85
|
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
85
86
|
|
86
87
|
try:
|
87
|
-
now = datetime.now(
|
88
|
+
now = datetime.now(timezone.utc)
|
88
89
|
|
89
90
|
# 현재 rate limit 정보 가져오기
|
90
91
|
rate_info = _rate_limits.get(rate_limit_key)
|
@@ -115,7 +116,6 @@ def rate_limit(
|
|
115
116
|
except Exception as e:
|
116
117
|
raise CustomException(
|
117
118
|
ErrorCode.INTERNAL_ERROR,
|
118
|
-
detail=str(e),
|
119
119
|
source_function=func.__name__,
|
120
120
|
original_error=e
|
121
121
|
)
|
@@ -125,7 +125,6 @@ def rate_limit(
|
|
125
125
|
except Exception as e:
|
126
126
|
raise CustomException(
|
127
127
|
ErrorCode.INTERNAL_ERROR,
|
128
|
-
detail=str(e),
|
129
128
|
source_function="rate_limit",
|
130
129
|
original_error=e
|
131
130
|
)
|
@@ -136,78 +135,63 @@ def rate_limit(
|
|
136
135
|
async def create_jwt_token(
|
137
136
|
user_data: Dict[str, Any],
|
138
137
|
token_type: Literal["access", "refresh"],
|
139
|
-
|
140
|
-
|
138
|
+
db_session: AsyncSession,
|
139
|
+
token_settings: Dict[str, Any],
|
141
140
|
) -> str:
|
142
141
|
"""JWT 토큰을 생성하고 로그를 기록합니다."""
|
143
142
|
try:
|
144
|
-
|
145
|
-
|
143
|
+
# 토큰 데이터 구성
|
146
144
|
if token_type == "access":
|
147
|
-
expires_at = datetime.now(
|
145
|
+
expires_at = datetime.now(timezone.utc) + timedelta(minutes=token_settings["ACCESS_TOKEN_EXPIRE_MINUTES"])
|
146
|
+
|
148
147
|
token_data = {
|
149
148
|
# 등록 클레임
|
150
|
-
"iss":
|
151
|
-
"sub": user_data
|
152
|
-
"aud":
|
149
|
+
"iss": token_settings["TOKEN_ISSUER"],
|
150
|
+
"sub": user_data.username,
|
151
|
+
"aud": token_settings["TOKEN_AUDIENCE"],
|
153
152
|
"exp": expires_at,
|
154
153
|
|
155
154
|
# 공개 클레임
|
156
|
-
"username": user_data
|
157
|
-
"name": user_data.
|
155
|
+
"username": user_data.username,
|
156
|
+
"name": user_data.name,
|
158
157
|
|
159
158
|
# 비공개 클레임
|
160
|
-
"user_ulid": user_data
|
161
|
-
"role_ulid": user_data.
|
162
|
-
"status": user_data.
|
163
|
-
"last_login": datetime.now(
|
159
|
+
"user_ulid": user_data.ulid,
|
160
|
+
"role_ulid": user_data.role_ulid,
|
161
|
+
"status": user_data.status,
|
162
|
+
"last_login": datetime.now(timezone.utc).isoformat(),
|
164
163
|
"token_type": token_type,
|
165
164
|
|
166
165
|
# 조직 관련 클레임
|
167
|
-
"organization_ulid": user_data.
|
168
|
-
"organization_id": user_data.
|
169
|
-
"organization_name": user_data.
|
170
|
-
"company_name": user_data.
|
166
|
+
"organization_ulid": user_data.role.organization.ulid,
|
167
|
+
"organization_id": user_data.role.organization.id,
|
168
|
+
"organization_name": user_data.role.organization.name,
|
169
|
+
"company_name": user_data.role.organization.company.name
|
171
170
|
}
|
172
171
|
else: # refresh token
|
173
|
-
expires_at = datetime.now(
|
172
|
+
expires_at = datetime.now(timezone.utc) + timedelta(days=14)
|
174
173
|
token_data = {
|
175
|
-
"iss":
|
176
|
-
"sub": user_data
|
174
|
+
"iss": token_settings["TOKEN_ISSUER"],
|
175
|
+
"sub": user_data.username,
|
177
176
|
"exp": expires_at,
|
178
177
|
"token_type": token_type,
|
179
|
-
"user_ulid": user_data
|
178
|
+
"user_ulid": user_data.ulid
|
180
179
|
}
|
181
180
|
|
181
|
+
# JWT 토큰 생성
|
182
182
|
try:
|
183
183
|
token = jwt.encode(
|
184
184
|
token_data,
|
185
|
-
|
186
|
-
algorithm=
|
185
|
+
token_settings["JWT_SECRET"],
|
186
|
+
algorithm=token_settings["JWT_ALGORITHM"]
|
187
187
|
)
|
188
188
|
except Exception as e:
|
189
189
|
raise CustomException(
|
190
|
-
ErrorCode.
|
190
|
+
ErrorCode.TOKEN_ERROR,
|
191
191
|
detail=f"token|{token_type}",
|
192
192
|
source_function="security.create_jwt_token",
|
193
193
|
original_error=e
|
194
194
|
)
|
195
|
-
|
196
|
-
# 로그 생성
|
197
|
-
try:
|
198
|
-
activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
|
199
|
-
await db_service.create_log(
|
200
|
-
{
|
201
|
-
"type": activity_type,
|
202
|
-
"user_ulid": user_data["ulid"],
|
203
|
-
"token": token
|
204
|
-
},
|
205
|
-
request
|
206
|
-
)
|
207
|
-
except Exception as e:
|
208
|
-
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
209
|
-
logging.error(f"Failed to create token log: {str(e)}")
|
210
|
-
|
211
195
|
return token
|
212
196
|
|
213
197
|
except CustomException as e:
|
@@ -217,9 +201,10 @@ async def create_jwt_token(
|
|
217
201
|
ErrorCode.INTERNAL_ERROR,
|
218
202
|
detail=str(e),
|
219
203
|
source_function="security.create_jwt_token",
|
220
|
-
original_error=e
|
204
|
+
original_error=str(e)
|
221
205
|
)
|
222
206
|
|
207
|
+
|
223
208
|
async def verify_jwt_token(
|
224
209
|
token: str,
|
225
210
|
expected_type: Optional[Literal["access", "refresh"]] = None
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.76"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
|
+
aiteamutils/base_model.py,sha256=SWifZPzlUGyKAjmV7Uoe7PUhedm215efD7zOay8DhNc,2906
|
3
|
+
aiteamutils/base_repository.py,sha256=VLCLLVkNnYLmZ6EMcr3Tu0RkMqDyMhtCO3M_2j6q-R8,4409
|
4
|
+
aiteamutils/base_service.py,sha256=GmO_fqrSEbIs_Jc5BoRBTLfaJaS6CIxYPAs4B4TdtaU,9123
|
5
|
+
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
|
+
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
|
+
aiteamutils/database.py,sha256=zf5Ab5ZFYQBrtvxW9te9RoVsWlMkOpTdmJ8rDwfvINs,19192
|
8
|
+
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
9
|
+
aiteamutils/exceptions.py,sha256=3FUCIqXgYmMqonnMgUlh-J2xtApiiCgg4WM-2UV4vmQ,15823
|
10
|
+
aiteamutils/security.py,sha256=xjuSTwn3_DeLHqUb17GwDU58nH7wDyk_7Wq8CPskXo8,9579
|
11
|
+
aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
|
12
|
+
aiteamutils/version.py,sha256=sAzd6RPEDU8drCBs0LCLhJkj0zQgL2FdPYnX-PVpUrs,42
|
13
|
+
aiteamutils-0.2.76.dist-info/METADATA,sha256=EVxAPwXRwIYNWzHV8ekVUIKXKlyY_RogJn1wWK5fE58,1718
|
14
|
+
aiteamutils-0.2.76.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
aiteamutils-0.2.76.dist-info/RECORD,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
|
-
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
|
-
aiteamutils/base_repository.py,sha256=tG_xz4hHYAN3-wkrLvEPxyTucV4pzT6dihoKVJp2JIc,2079
|
4
|
-
aiteamutils/base_service.py,sha256=M02lAcDmzTul97VBC1H8i5OtNKV8a9En2_sNCz84e-w,2852
|
5
|
-
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
|
-
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
|
-
aiteamutils/database.py,sha256=4Z1baj7UISE8KsVC_KilKwtCR4Yp8qR-kbwB7CHpDtE,8032
|
8
|
-
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
9
|
-
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
10
|
-
aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
|
11
|
-
aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
|
12
|
-
aiteamutils/version.py,sha256=p9Q6ycHvK4ulfUsk9t-OMhQk41NdIQmGrAm5soMo2OY,42
|
13
|
-
aiteamutils-0.2.73.dist-info/METADATA,sha256=oGgO7P8aKOYwsEPaGzDSRRIxCXfs3PVqhDT9GCehssM,1718
|
14
|
-
aiteamutils-0.2.73.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
-
aiteamutils-0.2.73.dist-info/RECORD,,
|
File without changes
|