From 03f7ad94a945060bc3b7ae43dfdcea0729e9ce1a Mon Sep 17 00:00:00 2001 From: icurfer Date: Sun, 18 Jan 2026 23:39:23 +0900 Subject: [PATCH] =?UTF-8?q?v0.0.29=20|=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95(SiteSettings)=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=B0=8F=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Google 로그인 활성화 여부 관리 기능 - 관리자 전용 설정 수정 API - 싱글톤 패턴으로 구현 Co-Authored-By: Claude Opus 4.5 --- users/migrations/0014_sitesettings.py | 25 ++++++++++ users/models.py | 42 +++++++++++++++++ users/urls.py | 4 ++ users/views.py | 67 ++++++++++++++++++++++++++- version | 2 +- 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 users/migrations/0014_sitesettings.py diff --git a/users/migrations/0014_sitesettings.py b/users/migrations/0014_sitesettings.py new file mode 100644 index 0000000..ff10004 --- /dev/null +++ b/users/migrations/0014_sitesettings.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.14 on 2026-01-18 14:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0013_add_social_login_fields'), + ] + + operations = [ + migrations.CreateModel( + name='SiteSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('google_login_enabled', models.BooleanField(default=True, verbose_name='Google 로그인 활성화')), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': '사이트 설정', + 'verbose_name_plural': '사이트 설정', + }, + ), + ] diff --git a/users/models.py b/users/models.py index da3f008..404847d 100644 --- a/users/models.py +++ b/users/models.py @@ -5,6 +5,48 @@ from django.conf import settings # ✅ 추가 from cryptography.fernet import Fernet import base64, hashlib # ✅ SECRET_KEY 암호화 키 생성용 + +# ============================================ +# 사이트 설정 (싱글톤) +# ============================================ + +class SiteSettings(models.Model): + """ + 사이트 전역 설정 (싱글톤 패턴) + - Google 로그인 활성화 여부 등 관리 + """ + # 소셜 로그인 설정 + google_login_enabled = models.BooleanField(default=True, verbose_name="Google 로그인 활성화") + + # 향후 확장 가능한 설정들 + # kakao_login_enabled = models.BooleanField(default=False, verbose_name="카카오 로그인 활성화") + # naver_login_enabled = models.BooleanField(default=False, verbose_name="네이버 로그인 활성화") + # registration_enabled = models.BooleanField(default=True, verbose_name="회원가입 허용") + + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "사이트 설정" + verbose_name_plural = "사이트 설정" + + def save(self, *args, **kwargs): + # 싱글톤 패턴: 항상 id=1로 저장 + self.pk = 1 + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + # 삭제 방지 + pass + + @classmethod + def get_settings(cls): + """설정 인스턴스 가져오기 (없으면 생성)""" + obj, created = cls.objects.get_or_create(pk=1) + return obj + + def __str__(self): + return "사이트 설정" + class CustomUserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): if not email: diff --git a/users/urls.py b/users/urls.py index 837a6f5..152f737 100644 --- a/users/urls.py +++ b/users/urls.py @@ -12,6 +12,8 @@ from .views import ( KVMServerActivateView, KVMServerSSHKeyView, KVMServerSSHKeyUploadView, # 소셜 로그인 GoogleLoginView, GoogleLinkWithPasswordView, GoogleLinkView, GoogleUnlinkView, + # 사이트 설정 + SiteSettingsView, ) from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView from .views_jwks import jwks_view # django-jwks @@ -49,4 +51,6 @@ urlpatterns = [ path('auth/google/link-with-password/', GoogleLinkWithPasswordView.as_view(), name='google_link_with_password'), path('auth/google/link/', GoogleLinkView.as_view(), name='google_link'), path('auth/google/unlink/', GoogleUnlinkView.as_view(), name='google_unlink'), + # 사이트 설정 + path('settings/', SiteSettingsView.as_view(), name='site_settings'), ] diff --git a/users/views.py b/users/views.py index 9642d34..6549698 100644 --- a/users/views.py +++ b/users/views.py @@ -12,7 +12,7 @@ from django.conf import settings from google.oauth2 import id_token from google.auth.transport import requests as google_requests from .serializers import RegisterSerializer, CustomTokenObtainPairSerializer, UserListSerializer, KVMServerSerializer -from .models import CustomUser, NHNCloudProject, KVMServer +from .models import CustomUser, NHNCloudProject, KVMServer, SiteSettings logger = logging.getLogger(__name__) tracer = trace.get_tracer(__name__) # ✅ 트레이서 생성 @@ -1426,3 +1426,68 @@ class GoogleUnlinkView(APIView): "message": "Google 계정 연동이 해제되었습니다.", "social_provider": None, }, status=status.HTTP_200_OK) + + +# ============================================ +# 사이트 설정 API +# ============================================ + +class IsAdmin(BasePermission): + """admin 등급만 접근 가능""" + def has_permission(self, request, view): + if not request.user or not request.user.is_authenticated: + return False + return request.user.grade == 'admin' + + +class SiteSettingsView(APIView): + """ + 사이트 설정 조회/수정 (관리자 전용) + """ + + def get_permissions(self): + # GET은 인증된 사용자면 누구나 가능 (설정 조회) + # PUT/PATCH는 관리자만 가능 (설정 변경) + if self.request.method in ['PUT', 'PATCH']: + return [IsAuthenticated(), IsAdmin()] + return [] # GET은 누구나 가능 (비로그인 포함) + + def get(self, request): + """사이트 설정 조회 (공개)""" + with tracer.start_as_current_span("SiteSettingsView GET") as span: + settings_obj = SiteSettings.get_settings() + span.add_event("Site settings retrieved") + + return Response({ + "google_login_enabled": settings_obj.google_login_enabled, + "updated_at": settings_obj.updated_at.isoformat() if settings_obj.updated_at else None, + }) + + def patch(self, request): + """사이트 설정 수정 (관리자 전용)""" + with tracer.start_as_current_span("SiteSettingsView PATCH") as span: + email, ip, ua = get_request_info(request) + settings_obj = SiteSettings.get_settings() + + # 업데이트할 필드 처리 + updated_fields = [] + + if 'google_login_enabled' in request.data: + settings_obj.google_login_enabled = request.data['google_login_enabled'] + updated_fields.append('google_login_enabled') + + if updated_fields: + settings_obj.save() + logger.info( + f"[SITE SETTINGS UPDATE] admin={email} | fields={updated_fields} | IP={ip} | UA={ua}" + ) + span.add_event("Site settings updated", attributes={ + "admin": email, + "fields": str(updated_fields) + }) + + return Response({ + "message": "설정이 저장되었습니다.", + "google_login_enabled": settings_obj.google_login_enabled, + "updated_at": settings_obj.updated_at.isoformat() if settings_obj.updated_at else None, + }) diff --git a/version b/version index 9dc9cb1..dc16803 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.0.28 \ No newline at end of file +v0.0.29 \ No newline at end of file