v0.0.36 | Upbit API 자격증명 관리 기능 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 2m31s

- CustomUser 모델에 Upbit access/secret key 암호화 필드 추가
- UpbitCredentialsView: 자격증명 저장/조회(마스킹)/삭제 API
- UpbitSecretKeyView: 복호화된 키 조회 API (내부 서비스 호출용)
- 마이그레이션 파일 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 23:26:01 +09:00
parent 7614dcb888
commit 963fd4c103
5 changed files with 171 additions and 1 deletions

View File

@ -2075,3 +2075,109 @@ class SiteSettingsView(APIView):
# 메타 정보
"updated_at": settings_obj.updated_at.isoformat() if settings_obj.updated_at else None,
})
# ============================================
# 📈 Upbit API 자격증명 관리
# ============================================
class UpbitCredentialsView(APIView):
"""Upbit API 자격증명 저장/조회/삭제"""
permission_classes = [IsAuthenticated]
def get(self, request):
"""자격증명 조회 (키 마스킹)"""
enrich_span(request, request.user, operation="upbit.credentials.get")
email, ip, ua = get_request_info(request)
user = request.user
with child_span("get_credentials", user_id=user.id):
has_credentials = bool(user.encrypted_upbit_access_key and user.encrypted_upbit_secret_key)
access_key_masked = ""
if has_credentials:
try:
ak = user.decrypt_upbit_access_key()
access_key_masked = ak[:4] + "****" + ak[-4:] if len(ak) > 8 else "****"
except Exception:
access_key_masked = "****"
span_success(200)
return Response({
"has_credentials": has_credentials,
"access_key": access_key_masked,
})
def post(self, request):
"""자격증명 저장"""
enrich_span(request, request.user, operation="upbit.credentials.save")
email, ip, ua = get_request_info(request)
user = request.user
access_key = request.data.get("access_key")
secret_key = request.data.get("secret_key")
if not access_key or not secret_key:
span_error("missing_fields", 400)
return Response(
{"error": "access_key, secret_key는 필수입니다."},
status=status.HTTP_400_BAD_REQUEST,
)
try:
with child_span("encrypt_and_save"):
user.save_upbit_credentials(access_key, secret_key)
logger.info(f"[UPBIT CREDENTIALS SAVE] user={email} | status=success | IP={ip}")
span_success(201)
return Response({"message": "Upbit API 키가 저장되었습니다."}, status=201)
except Exception as e:
logger.exception(f"[UPBIT CREDENTIALS SAVE] user={email} | status=fail | IP={ip}")
span_error(str(e), 500)
return Response({"error": f"저장 실패: {str(e)}"}, status=500)
def delete(self, request):
"""자격증명 삭제"""
enrich_span(request, request.user, operation="upbit.credentials.delete")
email, ip, ua = get_request_info(request)
user = request.user
with child_span("delete_credentials", user_id=user.id):
user.clear_upbit_credentials()
logger.info(f"[UPBIT CREDENTIALS DELETE] user={email} | status=success | IP={ip}")
span_success(200)
return Response({"message": "Upbit API 키가 삭제되었습니다."})
class UpbitSecretKeyView(APIView):
"""Upbit API 키 조회 (복호화) - msa-django-upbit에서 호출용"""
permission_classes = [IsAuthenticated]
def get(self, request):
"""복호화된 API 키 조회"""
enrich_span(request, request.user, operation="upbit.keys.get")
email, ip, ua = get_request_info(request)
user = request.user
if not user.encrypted_upbit_access_key or not user.encrypted_upbit_secret_key:
span_error("not_found", 404)
return Response(
{"error": "Upbit API 키가 등록되어 있지 않습니다."},
status=404
)
try:
with child_span("decrypt_keys"):
access_key = user.decrypt_upbit_access_key()
secret_key = user.decrypt_upbit_secret_key()
logger.info(f"[UPBIT KEYS GET] user={email} | status=success | IP={ip}")
span_success(200)
return Response({
"access_key": access_key,
"secret_key": secret_key,
})
except Exception as e:
logger.exception(f"[UPBIT KEYS GET] user={email} | status=fail | IP={ip}")
span_error(str(e), 500)
return Response({"error": f"키 복호화 실패: {str(e)}"}, status=500)