From 0f4a4574788502d04d5494bb554726050ce4f804 Mon Sep 17 00:00:00 2001 From: icurfer Date: Tue, 22 Apr 2025 18:40:48 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=B0=8F=20=ED=86=A0=ED=81=B0=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=9B=84=20=EB=93=B1=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 10 ++ blog/__init__.py | 0 blog/admin.py | 3 + blog/apps.py | 6 ++ blog/authentication.py | 14 +++ blog/migrations/0001_initial.py | 24 +++++ blog/migrations/__init__.py | 0 blog/models.py | 12 +++ blog/serializers.py | 10 ++ blog/tests.py | 3 + blog/urls.py | 8 ++ blog/utils.py | 13 +++ blog/views.py | 18 ++++ blog_prj/__init__.py | 0 blog_prj/asgi.py | 16 +++ blog_prj/settings.py | 170 ++++++++++++++++++++++++++++++++ blog_prj/urls.py | 23 +++++ blog_prj/wsgi.py | 16 +++ manage.py | 22 +++++ requirementes.txt | 26 +++++ 20 files changed, 394 insertions(+) create mode 100644 .env.dev create mode 100644 blog/__init__.py create mode 100644 blog/admin.py create mode 100644 blog/apps.py create mode 100644 blog/authentication.py create mode 100644 blog/migrations/0001_initial.py create mode 100644 blog/migrations/__init__.py create mode 100644 blog/models.py create mode 100644 blog/serializers.py create mode 100644 blog/tests.py create mode 100644 blog/urls.py create mode 100644 blog/utils.py create mode 100644 blog/views.py create mode 100644 blog_prj/__init__.py create mode 100644 blog_prj/asgi.py create mode 100644 blog_prj/settings.py create mode 100644 blog_prj/urls.py create mode 100644 blog_prj/wsgi.py create mode 100755 manage.py create mode 100644 requirementes.txt diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..86da117 --- /dev/null +++ b/.env.dev @@ -0,0 +1,10 @@ +DEBUG=1 +SQL_ENGINE='django.db.backends.mysql' +SQL_HOST='192.168.0.101' +SQL_USER='dev' +SQL_PASSWORD='Rlaxodms90!@' +SQL_DATABASE='msa-django-demo' +SQL_PORT='3306' +SECRET_KEY='django-insecure-*kh6e0376o-0m5n*xz^2a2t^fa^77c1=))f$3egn7!w7axaj-l' +AUTH_VERIFY_URL=http://192.168.0.202:8000/api/auth/verify/ +# CSRF_TRUSTED_ORIGINS=https://www.example.com,https://butler.example.com,https://butler.example.com:8000 \ No newline at end of file diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/blog/apps.py b/blog/apps.py new file mode 100644 index 0000000..94788a5 --- /dev/null +++ b/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'blog' diff --git a/blog/authentication.py b/blog/authentication.py new file mode 100644 index 0000000..da5f6f7 --- /dev/null +++ b/blog/authentication.py @@ -0,0 +1,14 @@ +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.exceptions import InvalidToken + +class StatelessUser: + def __init__(self, username): + self.username = username + self.is_authenticated = True + +class StatelessJWTAuthentication(JWTAuthentication): + def get_user(self, validated_token): + name = validated_token.get("name") + if not name: + raise InvalidToken("Token에 'name' 항목이 없습니다.") + return StatelessUser(username=name) diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000..84e531d --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.14 on 2025-04-22 08:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('content', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('author_name', models.CharField(max_length=150)), + ], + ), + ] diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000..d261a80 --- /dev/null +++ b/blog/models.py @@ -0,0 +1,12 @@ +from django.db import models +from django.conf import settings +from django.contrib.auth.models import User + +class Post(models.Model): + title = models.CharField(max_length=255) + content = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + author_name = models.CharField(max_length=150) + + def __str__(self): + return self.title diff --git a/blog/serializers.py b/blog/serializers.py new file mode 100644 index 0000000..18c88f8 --- /dev/null +++ b/blog/serializers.py @@ -0,0 +1,10 @@ +# blog/serializers.py + +from rest_framework import serializers +from .models import Post + +class PostSerializer(serializers.ModelSerializer): + class Meta: + model = Post + fields = ['id', 'title', 'content', 'author_name', 'created_at'] + read_only_fields = ['author_name', 'created_at'] diff --git a/blog/tests.py b/blog/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/blog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/blog/urls.py b/blog/urls.py new file mode 100644 index 0000000..f88e924 --- /dev/null +++ b/blog/urls.py @@ -0,0 +1,8 @@ +# blog/urls.py + +from django.urls import path +from .views import PostListCreateView + +urlpatterns = [ + path('posts/', PostListCreateView.as_view(), name='post-list-create'), +] diff --git a/blog/utils.py b/blog/utils.py new file mode 100644 index 0000000..bed277e --- /dev/null +++ b/blog/utils.py @@ -0,0 +1,13 @@ +import requests +from rest_framework.exceptions import AuthenticationFailed + +def verify_token_with_auth_server(token: str): + # url = "http://192.168.0.202:8000/api/auth/verify/" + url = settings.AUTH_VERIFY_URL # ✅ .env에서 설정한 값 사용 + headers = {"Content-Type": "application/json"} + try: + response = requests.post(url, json={"token": token}, headers=headers, timeout=3) + if response.status_code != 200: + raise AuthenticationFailed("유효하지 않은 토큰입니다 (auth 서버 응답 오류)") + except requests.exceptions.RequestException as e: + raise AuthenticationFailed(f"auth 서버 통신 실패: {str(e)}") \ No newline at end of file diff --git a/blog/views.py b/blog/views.py new file mode 100644 index 0000000..562e76a --- /dev/null +++ b/blog/views.py @@ -0,0 +1,18 @@ +# blog/views.py + +from rest_framework import generics, permissions +from .models import Post +from .serializers import PostSerializer +from .utils import verify_token_with_auth_server # ✅ 추가 + +class PostListCreateView(generics.ListCreateAPIView): + queryset = Post.objects.all().order_by('-created_at') + serializer_class = PostSerializer + permission_classes = [permissions.IsAuthenticated] + + def perform_create(self, serializer): + # ✅ 토큰 추출 및 유효성 2차 검증 + token = self.request.headers.get("Authorization", "").replace("Bearer ", "") + verify_token_with_auth_server(token) + + serializer.save(author_name=self.request.user.username) diff --git a/blog_prj/__init__.py b/blog_prj/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog_prj/asgi.py b/blog_prj/asgi.py new file mode 100644 index 0000000..8c9e130 --- /dev/null +++ b/blog_prj/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for blog_prj project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_prj.settings') + +application = get_asgi_application() diff --git a/blog_prj/settings.py b/blog_prj/settings.py new file mode 100644 index 0000000..97b6a74 --- /dev/null +++ b/blog_prj/settings.py @@ -0,0 +1,170 @@ +""" +Django settings for blog_prj project. + +Generated by 'django-admin startproject' using Django 4.2.14. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +import os +from dotenv import load_dotenv +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# 우선순위: .env.dev > .env.prd > .env +if os.path.exists(os.path.join(BASE_DIR, '.env.dev')): + print("Read Environment File > Used : .env.dev") + load_dotenv(os.path.join(BASE_DIR, '.env.dev')) +elif os.path.exists(os.path.join(BASE_DIR, '.env.prd')): + print("Read Environment File > Used : .env.prd") + load_dotenv(os.path.join(BASE_DIR, '.env.prd')) +else: + print("None Environment File > Used : local_env") + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-ec9me^z%x7-2vwee5#qq(kvn@^cs!!22_*f-im(320_k5-=0j5') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = int(os.environ.get('DEBUG', 1)) + +AUTH_VERIFY_URL = os.environ.get('AUTH_VERIFY_URL', 'NONE') + +ALLOWED_HOSTS = ["*"] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # by.sdjo 2025-04-22 + 'rest_framework', + 'rest_framework_simplejwt', + 'drf_yasg', + 'corsheaders', + # create by.sdjo 2025-04-22 + 'blog', # 2025-04-22 custom app create +] + +# AUTH_USER_MODEL = 'users.CustomUser' + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +# by.sdjo 2025-04-22 +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://192.168.0.100:3000", + "https://demo.test", + "http://demo.test", + "https://sample.test", + "http://sample.test", +] + +# by.sdjo 2025-04-22 +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'blog.authentication.StatelessJWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ) +} + +ROOT_URLCONF = 'blog_prj.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'blog_prj.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + 'ENGINE': os.environ.get('SQL_ENGINE', 'django.db.backends.sqlite3'), + 'NAME': os.environ.get('SQL_DATABASE', BASE_DIR / 'db.sqlite3'), + 'USER': os.environ.get('SQL_USER', 'user'), + 'PASSWORD': os.environ.get('SQL_PASSWORD', 'password'), + 'HOST': os.environ.get('SQL_HOST', 'localhost'), + 'PORT': os.environ.get('SQL_PORT', '3306'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/blog_prj/urls.py b/blog_prj/urls.py new file mode 100644 index 0000000..0913fa1 --- /dev/null +++ b/blog_prj/urls.py @@ -0,0 +1,23 @@ +# blog_prj/urls.py + +from django.contrib import admin +from django.urls import path, include +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework import permissions + +schema_view = get_schema_view( + openapi.Info( + title="Blog API", + default_version='v1', + description="MSA Django Blog API", + ), + public=True, + permission_classes=[permissions.AllowAny], +) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/blog/', include('blog.urls')), # ✅ 이 줄 추가 + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), +] diff --git a/blog_prj/wsgi.py b/blog_prj/wsgi.py new file mode 100644 index 0000000..9915607 --- /dev/null +++ b/blog_prj/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for blog_prj project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_prj.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..41afbc7 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_prj.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirementes.txt b/requirementes.txt new file mode 100644 index 0000000..2427f37 --- /dev/null +++ b/requirementes.txt @@ -0,0 +1,26 @@ +asgiref==3.8.1 +certifi==2025.1.31 +charset-normalizer==3.4.1 +coreapi==2.3.3 +coreschema==0.0.4 +Django==4.2.14 +django-cors-headers==4.7.0 +djangorestframework==3.16.0 +djangorestframework_simplejwt==5.5.0 +drf-yasg==1.21.10 +idna==3.10 +inflection==0.5.1 +itypes==1.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.2 +mysqlclient==2.2.7 +packaging==25.0 +PyJWT==2.9.0 +python-dotenv==1.0.1 +pytz==2025.2 +PyYAML==6.0.2 +requests==2.32.3 +sqlparse==0.5.3 +typing_extensions==4.13.2 +uritemplate==4.1.1 +urllib3==2.4.0