Skip to content

Commit 47a6a38

Browse files
committed
fix: 전반적인 코드 개선 3차
1 parent 5a8e943 commit 47a6a38

13 files changed

Lines changed: 65 additions & 37 deletions

File tree

app/admin_api/test/cms_test.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99

1010
@pytest.fixture
1111
def domain_group(superuser):
12-
return DomainGroup.objects.create(
12+
obj, _ = DomainGroup.objects.get_or_create(
1313
name="2025년 PyConKR 홈페이지",
14-
domains=["2025.pycon.kr"],
15-
created_by=superuser,
16-
updated_by=superuser,
14+
defaults={
15+
"domains": ["2025.pycon.kr"],
16+
"created_by": superuser,
17+
"updated_by": superuser,
18+
},
1719
)
20+
return obj
1821

1922

2023
# ---- Auth -------------------------------------------------------------------

app/admin_api/views/modification_audit.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@
2525
}
2626

2727

28-
@utils.extend_schema_view(
29-
list=utils.extend_schema(tags=[OpenAPITag.ADMIN_MODIFICATION_AUDIT]),
30-
retrieve=utils.extend_schema(tags=[OpenAPITag.ADMIN_MODIFICATION_AUDIT]),
31-
)
28+
@utils.extend_schema_view(list=utils.extend_schema(tags=[OpenAPITag.ADMIN_MODIFICATION_AUDIT]))
3229
class ModificationAuditAdminViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
3330
serializer_class = ModificationAuditResponseAdminSerializer
3431
permission_classes = [IsSuperUser]

app/admin_api/views/user.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
from user.models import UserExt
1616
from user.models.organization import Organization
1717

18-
ADMIN_METHODS = ["list", "retrieve", "create", "partial_update", "destroy"]
18+
USER_ADMIN_METHODS = ["list", "retrieve", "create", "partial_update"]
19+
ADMIN_METHODS = USER_ADMIN_METHODS + ["destroy"]
1920

2021

21-
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_USER]) for m in ADMIN_METHODS})
22+
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_USER]) for m in USER_ADMIN_METHODS})
2223
class UserAdminViewSet(
2324
mixins.RetrieveModelMixin,
2425
mixins.ListModelMixin,

app/cms/test/sitemap_api_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_list_view(api_client, create_sitemap):
2424
@pytest.mark.django_db
2525
def test_list_view_returns_only_matching_domain(api_client):
2626
group_main = DomainGroup.objects.create(name="main", domains=["pycon.kr"])
27-
group_legacy = DomainGroup.objects.create(name="legacy", domains=["2025.pycon.kr"])
27+
group_legacy = DomainGroup.objects.create(name="legacy", domains=["legacy.pycon.kr"])
2828

2929
_create_sitemap("main_about", group=group_main)
3030
_create_sitemap("legacy_about", group=group_legacy)

app/core/authn/api_key.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from hmac import compare_digest
34
from typing import TYPE_CHECKING
45

56
from django.conf import settings
@@ -30,10 +31,11 @@ def authenticate(self, request: Request) -> tuple["UserExt", None] | None:
3031
api_key = request.headers.get("x-api-key", "")
3132
api_secret = request.headers.get("x-api-secret", "")
3233

33-
if api_key.lower() in settings.EXT_API_KEYS and api_secret == settings.EXT_API_KEYS.get(api_key.lower()):
34-
return self._get_or_create_api_key_user(api_key), None
35-
36-
return None
34+
if not (expected_secret := settings.EXT_API_KEYS.get(api_key.lower())):
35+
return None
36+
if not compare_digest(api_secret.encode(), expected_secret.encode()):
37+
return None
38+
return self._get_or_create_api_key_user(api_key), None
3739

3840

3941
class APIKeyAuthenticationScheme(OpenApiAuthenticationExtension): # type: ignore[no-untyped-call]

app/core/external_apis/portone/client.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

3-
from datetime import datetime
3+
from datetime import datetime, timedelta
44
from logging import getLogger
5+
from time import time
56
from traceback import format_exception
67
from typing import Any, Literal
78

@@ -14,6 +15,11 @@
1415
logger = getLogger(__name__)
1516

1617
DEFAULT_TIMEOUT = 5
18+
# PortOne v1 access_token 의 공식 TTL 은 발행 시점 +30분 (developers.portone.io 명시).
19+
# 응답에 `expired_at` (unix epoch sec) 가 포함되며, 만료 직전 재발급 race 를 막기 위한 안전 마진.
20+
TOKEN_REFRESH_MARGIN = timedelta(seconds=30)
21+
# `expired_at` 가 응답에 누락된 비정상 케이스의 보수적 fallback (공식 TTL 30분보다 짧게).
22+
TOKEN_FALLBACK_TTL = timedelta(minutes=5)
1723
RequestMethodType = Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"]
1824

1925

@@ -29,6 +35,8 @@ class PortOneClient:
2935
def __init__(self, timeout: TimeoutTypes = DEFAULT_TIMEOUT) -> None:
3036
self._timeout = timeout
3137
self._client: Client | None = None
38+
self._cached_token: str | None = None
39+
self._cached_token_expires_at: float = 0.0
3240

3341
@property
3442
def client(self) -> Client:
@@ -39,6 +47,10 @@ def client(self) -> Client:
3947

4048
@property
4149
def _access_token(self) -> str:
50+
# 만료 직전 안전 마진까지는 캐시 재사용 (PortOne 토큰 TTL 30분).
51+
if self._cached_token and time() < self._cached_token_expires_at - TOKEN_REFRESH_MARGIN.total_seconds():
52+
return self._cached_token
53+
4254
response = self.client.post(
4355
url="/users/getToken", json={"imp_key": settings.PORTONE.imp_key, "imp_secret": settings.PORTONE.imp_secret}
4456
)
@@ -47,15 +59,18 @@ def _access_token(self) -> str:
4759
resp_serializer = PortOneV1ResponseSerializer.from_response(response)
4860
resp_serializer.is_valid(raise_exception=True)
4961

50-
if not (access_token := resp_serializer.validated_data["response"]["access_token"]):
62+
resp = resp_serializer.validated_data["response"]
63+
if not (access_token := resp.get("access_token")):
5164
raise ValueError("PortOne access_token 값이 존재하지 않습니다.")
5265

53-
return access_token
54-
5566
except Exception as e:
5667
logger.error(format_exception(e))
5768
raise PortOneException("PortOne AccessToken 획득에 실패했습니다.") from e
5869

70+
self._cached_token = access_token
71+
self._cached_token_expires_at = float(resp.get("expired_at") or (time() + TOKEN_FALLBACK_TTL.total_seconds()))
72+
return access_token
73+
5974
def _request(
6075
self,
6176
method: RequestMethodType,

app/core/util/strutil.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,4 @@ def uuid_to_b64(in_str: UUID | str) -> str:
1414

1515

1616
def b64_to_uuid(in_str: str) -> UUID:
17-
in_str += "=" * (4 - len(in_str) % 4)
18-
return UUID(bytes=urlsafe_b64decode(in_str + "=="))
17+
return UUID(bytes=urlsafe_b64decode(in_str + "=" * (-len(in_str) % 4)))

app/core/util/totp.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from base64 import b32encode
23
from dataclasses import dataclass
34
from hmac import new as hmac_new
@@ -21,9 +22,15 @@ def __post_init__(self) -> None:
2122
if self.digest not in ALLOWED_DIGESTS:
2223
raise ValueError(f"Unsupported digest algorithm: {self.digest}")
2324
if self.digest != "sha1":
24-
raise Warning(f"Using {self.digest} is not recommended as Google Authenticator does not support it.")
25+
warnings.warn(
26+
f"Using {self.digest} is not recommended as Google Authenticator does not support it.",
27+
stacklevel=2,
28+
)
2529
if self.time_step != 30:
26-
raise Warning(f"Using {self.time_step} is not recommended as Google Authenticator does not support it.")
30+
warnings.warn(
31+
f"Using time_step={self.time_step} is not recommended as Google Authenticator does not support it.",
32+
stacklevel=2,
33+
)
2734

2835
def get_hotp(self, counter: int) -> str:
2936
mac = hmac_new(self.key, pack(">Q", counter), self.digest).digest()

app/internal_api/views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,11 @@ class DeskSupportViewSet(
8989
parameters=[
9090
OpenApiParameter(
9191
name="otp",
92-
type=OpenApiTypes.INT,
92+
type=OpenApiTypes.STR,
9393
location=OpenApiParameter.QUERY,
9494
allow_blank=False,
9595
required=True,
96+
description="환불 승인자의 6자리 TOTP 코드",
9697
),
9798
],
9899
responses={
@@ -112,7 +113,7 @@ def destroy(
112113
"""
113114
serializer = OrderTotalRefundSerializer(
114115
instance=self.get_object(),
115-
data={"totp": request.GET.get("otp")},
116+
data={"totp": request.query_params.get("otp")},
116117
context={"check_refundable_date": False},
117118
)
118119
serializer.is_valid(raise_exception=True)

app/shop/order/views/orders.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,9 @@ def retrieve_receipt(
270270

271271
class OrderProductViewSet(mixins.DestroyModelMixin, viewsets.GenericViewSet):
272272
lookup_url_kwarg = "order_product_rel_id"
273-
serializer_class = OrderDto
273+
serializer_class = None
274274

275-
def get_queryset(self) -> models.QuerySet[Order]:
275+
def get_queryset(self) -> models.QuerySet[OrderProductRelation]:
276276
if not isinstance(self.request.user, UserExt):
277277
return OrderProductRelation.objects.none()
278278

0 commit comments

Comments
 (0)