v0.0.36 | Upbit API 자격증명 관리 기능 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 2m31s
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:
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-16 01:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0015_customuser_locked_until_customuser_login_failures_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='encrypted_upbit_access_key',
|
||||
field=models.BinaryField(blank=True, null=True, verbose_name='Upbit Access Key (암호화)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='encrypted_upbit_secret_key',
|
||||
field=models.BinaryField(blank=True, null=True, verbose_name='Upbit Secret Key (암호화)'),
|
||||
),
|
||||
]
|
||||
@ -141,6 +141,10 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||
social_id = models.CharField(max_length=255, blank=True, null=True, verbose_name="소셜 고유 ID")
|
||||
profile_image = models.URLField(max_length=500, blank=True, null=True, verbose_name="프로필 이미지 URL")
|
||||
|
||||
# 📈 Upbit API 자격증명 필드
|
||||
encrypted_upbit_access_key = models.BinaryField(blank=True, null=True, verbose_name="Upbit Access Key (암호화)")
|
||||
encrypted_upbit_secret_key = models.BinaryField(blank=True, null=True, verbose_name="Upbit Secret Key (암호화)")
|
||||
|
||||
# ☁️ NHN Cloud 자격증명 필드
|
||||
nhn_tenant_id = models.CharField(max_length=64, blank=True, null=True, verbose_name="NHN Cloud Tenant ID")
|
||||
nhn_username = models.EmailField(blank=True, null=True, verbose_name="NHN Cloud Username")
|
||||
@ -218,6 +222,38 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||
'nhn_tenant_id', 'nhn_username', 'encrypted_nhn_api_password', 'nhn_storage_account'
|
||||
])
|
||||
|
||||
# 📈 Upbit API 키 암복호화
|
||||
def encrypt_upbit_key(self, key: str) -> bytes:
|
||||
"""Upbit API 키 암호화"""
|
||||
cipher = Fernet(self.get_encryption_key())
|
||||
return cipher.encrypt(key.encode())
|
||||
|
||||
def decrypt_upbit_access_key(self) -> str:
|
||||
"""Upbit Access Key 복호화"""
|
||||
if self.encrypted_upbit_access_key:
|
||||
cipher = Fernet(self.get_encryption_key())
|
||||
return cipher.decrypt(self.encrypted_upbit_access_key).decode()
|
||||
return ""
|
||||
|
||||
def decrypt_upbit_secret_key(self) -> str:
|
||||
"""Upbit Secret Key 복호화"""
|
||||
if self.encrypted_upbit_secret_key:
|
||||
cipher = Fernet(self.get_encryption_key())
|
||||
return cipher.decrypt(self.encrypted_upbit_secret_key).decode()
|
||||
return ""
|
||||
|
||||
def save_upbit_credentials(self, access_key: str, secret_key: str):
|
||||
"""Upbit API 자격증명 저장"""
|
||||
self.encrypted_upbit_access_key = self.encrypt_upbit_key(access_key)
|
||||
self.encrypted_upbit_secret_key = self.encrypt_upbit_key(secret_key)
|
||||
self.save(update_fields=['encrypted_upbit_access_key', 'encrypted_upbit_secret_key'])
|
||||
|
||||
def clear_upbit_credentials(self):
|
||||
"""Upbit API 자격증명 삭제"""
|
||||
self.encrypted_upbit_access_key = None
|
||||
self.encrypted_upbit_secret_key = None
|
||||
self.save(update_fields=['encrypted_upbit_access_key', 'encrypted_upbit_secret_key'])
|
||||
|
||||
|
||||
# ============================================
|
||||
# NHN Cloud 프로젝트 (멀티 프로젝트 지원)
|
||||
|
||||
@ -14,6 +14,8 @@ from .views import (
|
||||
GoogleLoginView, GoogleLinkWithPasswordView, GoogleLinkView, GoogleUnlinkView,
|
||||
# 사이트 설정
|
||||
SiteSettingsView,
|
||||
# Upbit API 자격증명
|
||||
UpbitCredentialsView, UpbitSecretKeyView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
|
||||
from .views_jwks import jwks_view # django-jwks
|
||||
@ -56,4 +58,7 @@ urlpatterns = [
|
||||
path('auth/google/unlink/', GoogleUnlinkView.as_view(), name='google_unlink'),
|
||||
# 사이트 설정
|
||||
path('settings/', SiteSettingsView.as_view(), name='site_settings'),
|
||||
# Upbit API 자격증명
|
||||
path('upbit/', UpbitCredentialsView.as_view(), name='upbit_credentials'),
|
||||
path('upbit/keys/', UpbitSecretKeyView.as_view(), name='upbit_keys'),
|
||||
]
|
||||
|
||||
106
users/views.py
106
users/views.py
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user