From 75f7a8d7cff4cd626e5a332eb7ba28d1fe39a988 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 29 Nov 2025 18:57:37 +0900 Subject: [PATCH 1/9] chore: add `drf_yasg` to requirements.txt (Yet another Swagger generator) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index bec54d2..85a5301 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ django djangorestframework +drf-yasg pytest-django python-dotenv From ec5122b984694bfba57e91fd5b29b44c4a14ed36 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 5 Dec 2025 18:07:43 +0900 Subject: [PATCH 2/9] chore(app.settings): add `drf-yasg` configuration for Swagger, Redoc --- app/app/settings.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/app/settings.py b/app/app/settings.py index 89cfde3..e2d3409 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -12,6 +12,8 @@ from pathlib import Path +from drf_yasg.openapi import Contact, Info + from app import env # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -70,6 +72,7 @@ # Application definition INSTALLED_APPS = [ + 'drf_yasg', 'rest_framework', 'django.contrib.admin', @@ -162,3 +165,25 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +# drf-yasg settings +# https://drf-yasg.readthedocs.io/en/stable/settings.html +SWAGGER_SETTINGS = { + # NOTE: DEFAULT_INFO 설정을 해두어야 `generate_swagger` management command를 사용할 수 있음에 유의. + "DEFAULT_INFO": "app.settings.OPEN_API_INFO", +} + +REDOC_SETTINGS = { + "LAZY_RENDERING": True, +} + +OPEN_API_INFO = Info( + title="Time Limit Exceeded :: Authentication API", + default_version='v1', + description=( + "This API provides authentication endpoints and related features for the Time Limit Exceeded application, " + "including user login, registration, and token management." + ), + contact=Contact(email="hepheir@gmail.com"), +) From d320345c37dc6cd460a3c771d385dfd5acdb08e4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Dec 2025 06:13:57 +0900 Subject: [PATCH 3/9] chore(doc): initialize and install app `doc` --- app/app/settings.py | 2 ++ app/doc/__init__.py | 0 app/doc/apps.py | 6 ++++++ app/doc/urls.py | 20 ++++++++++++++++++++ app/doc/views.py | 3 +++ 5 files changed, 31 insertions(+) create mode 100644 app/doc/__init__.py create mode 100644 app/doc/apps.py create mode 100644 app/doc/urls.py create mode 100644 app/doc/views.py diff --git a/app/app/settings.py b/app/app/settings.py index e2d3409..dab5211 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -75,6 +75,8 @@ 'drf_yasg', 'rest_framework', + 'doc', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/app/doc/__init__.py b/app/doc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/doc/apps.py b/app/doc/apps.py new file mode 100644 index 0000000..9f8fe2b --- /dev/null +++ b/app/doc/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DocConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'doc' diff --git a/app/doc/urls.py b/app/doc/urls.py new file mode 100644 index 0000000..b72cd9e --- /dev/null +++ b/app/doc/urls.py @@ -0,0 +1,20 @@ +""" +URL configuration for doc app. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path + + +urlpatterns = [] diff --git a/app/doc/views.py b/app/doc/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/app/doc/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From daa182d529efb3b7d49e7d1ae6b07ea629f78f13 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Dec 2025 06:14:33 +0900 Subject: [PATCH 4/9] feat(doc.views): implement OpenAPI schema views with Swagger and Redoc support --- app/doc/views.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/doc/views.py b/app/doc/views.py index 91ea44a..b446094 100644 --- a/app/doc/views.py +++ b/app/doc/views.py @@ -1,3 +1,13 @@ -from django.shortcuts import render +from django.conf import settings +from drf_yasg.views import get_schema_view -# Create your views here. + +SchemaView = get_schema_view( + info=settings.OPEN_API_INFO, + public=True, +) + + +redoc_view = SchemaView.with_ui('redoc') +swagger_view = SchemaView.with_ui('swagger') +schema_format_view = SchemaView.without_ui() From 7c919fbdcac601cd911b7137b99165d7edf76d65 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Dec 2025 06:22:55 +0900 Subject: [PATCH 5/9] chore(doc.urls): register URL configuration for Swagger and Redoc views --- app/doc/urls.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/doc/urls.py b/app/doc/urls.py index b72cd9e..7e60f73 100644 --- a/app/doc/urls.py +++ b/app/doc/urls.py @@ -16,5 +16,11 @@ """ from django.urls import path +from . import views -urlpatterns = [] + +urlpatterns = [ + path('swagger/', views.schema_format_view), + path('swagger/', views.swagger_view), + path('redoc/', views.redoc_view), +] From 9a16a9b79a393d07606c2c1e5afd9be95b78af93 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Dec 2025 06:26:08 +0900 Subject: [PATCH 6/9] chore(app.urls): register URL configurations of `doc` app --- app/app/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/app/urls.py b/app/app/urls.py index 93599f7..83df3d1 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -15,8 +15,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), + path('doc/', include('doc.urls')), ] From 947f36f7c4a3329d83bd34fdb45029353c45d583 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Dec 2025 06:51:14 +0900 Subject: [PATCH 7/9] feat(doc.views): add permission class `IsDebug` to restrict OpenAPI schema access to debug mode --- app/doc/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/doc/views.py b/app/doc/views.py index b446094..65d58bd 100644 --- a/app/doc/views.py +++ b/app/doc/views.py @@ -1,5 +1,15 @@ from django.conf import settings from drf_yasg.views import get_schema_view +from rest_framework.permissions import BasePermission + + +class IsDebug(BasePermission): + """ + Permission class that allows access only when Django's DEBUG mode is enabled. + Useful for restricting certain endpoints to development environments. + """ + def has_permission(self, request, view): + return settings.DEBUG SchemaView = get_schema_view( From 3f2735bbce0f7e661e1a8614cec533ba6e128460 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Dec 2025 06:52:25 +0900 Subject: [PATCH 8/9] test(doc.urls): add tests for document endpoint accessibility in debug and production modes --- app/doc/test_urls.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 app/doc/test_urls.py diff --git a/app/doc/test_urls.py b/app/doc/test_urls.py new file mode 100644 index 0000000..df6c66e --- /dev/null +++ b/app/doc/test_urls.py @@ -0,0 +1,58 @@ +from django.test import TestCase, override_settings +from rest_framework import status + + +class DocEndpointsDebugModeTestCase(TestCase): + """DEBUG=True일 때 문서 엔드포인트 접근 가능 여부 테스트""" + + @override_settings(DEBUG=True) + def test_swagger_ui_accessible_in_debug_mode(self): + """DEBUG 모드에서 Swagger UI 접근 가능""" + response = self.client.get('/doc/swagger/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_settings(DEBUG=True) + def test_redoc_ui_accessible_in_debug_mode(self): + """DEBUG 모드에서 ReDoc UI 접근 가능""" + response = self.client.get('/doc/redoc/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_settings(DEBUG=True) + def test_schema_json_accessible_in_debug_mode(self): + """DEBUG 모드에서 스키마 JSON 접근 가능""" + response = self.client.get('/doc/swagger.json/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_settings(DEBUG=True) + def test_schema_yaml_accessible_in_debug_mode(self): + """DEBUG 모드에서 스키마 YAML 접근 가능""" + response = self.client.get('/doc/swagger.yaml/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class DocEndpointsProductionModeTestCase(TestCase): + """DEBUG=False일 때 문서 엔드포인트 접근 제한 테스트""" + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_swagger_ui_not_accessible_in_production(self): + """프로덕션 모드에서 Swagger UI 접근 불가""" + response = self.client.get('/doc/swagger/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_redoc_ui_not_accessible_in_production(self): + """프로덕션 모드에서 ReDoc UI 접근 불가""" + response = self.client.get('/doc/redoc/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_schema_json_not_accessible_in_production(self): + """프로덕션 모드에서 스키마 JSON 접근 불가""" + response = self.client.get('/doc/swagger.json/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_schema_yaml_not_accessible_in_production(self): + """프로덕션 모드에서 스키마 YAML 접근 불가""" + response = self.client.get('/doc/swagger.yaml/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) From 5b986a3f2012a63233567cba8d70cc2ac25d891e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Dec 2025 07:20:03 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat(doc.views):=20`SchemaView`=20=EC=9D=98?= =?UTF-8?q?=20=EC=A0=91=EA=B7=BC=20=EA=B6=8C=ED=95=9C=EC=9D=84=20`IsDebug`?= =?UTF-8?q?=20=EB=A1=9C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/doc/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/doc/views.py b/app/doc/views.py index 65d58bd..c6be006 100644 --- a/app/doc/views.py +++ b/app/doc/views.py @@ -14,6 +14,7 @@ def has_permission(self, request, view): SchemaView = get_schema_view( info=settings.OPEN_API_INFO, + permission_classes=[IsDebug], public=True, )