diff --git a/Django/NoUgly/NoUgly/settings.py b/Django/NoUgly/NoUgly/settings.py index ec92196..7d0de97 100644 --- a/Django/NoUgly/NoUgly/settings.py +++ b/Django/NoUgly/NoUgly/settings.py @@ -14,7 +14,7 @@ from django.core.exceptions import ImproperlyConfigured import datetime import os - +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -31,7 +31,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.environ.get('NOUGLY_DEBUG', False) -ALLOWED_HOSTS = ["*"] +ALLOWED_HOSTS = ['*'] # 배포 시 # ALLOWED_HOSTS = ['cmsong111.pythonanywhere.com'] @@ -46,18 +46,18 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'rest_framework', # DRF Authentication 이용 + 'rest_framework', 'rest_framework.authtoken', - 'rest_auth', - + 'rest_framework_simplejwt.token_blacklist', # 회원가입 'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', - 'rest_auth.registration', + 'dj_rest_auth', + 'dj_rest_auth.registration', 'django_filters', # myapp @@ -65,6 +65,10 @@ 'store', # 'phonenumber_field', + + # provider + 'allauth.socialaccount.providers.kakao', + 'allauth.socialaccount.providers.naver', ] MIDDLEWARE = [ @@ -175,12 +179,32 @@ REST_USE_JWT = True +# 하나의 사이트에서 여러 domain을 가질 수 있는 기능, 사이트 ID를 강제로 지정한다. SITE_ID = 1 MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Kakao, Naer 제공 +SOCIALACCOUNT_PROVIDERS = { + 'kakao': { + 'APP': { + 'client_id': os.environ.get('kakao_client_id'), + 'secret': os.environ.get('kakao_secret'), + 'key': '' + } + }, + 'naver': { + 'APP': { + 'client_id': os.environ.get('naver_client_id'), + 'secret': os.environ.get('naver_secret'), + 'key': '' + } + } +} + # DRF REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ @@ -190,9 +214,11 @@ ], 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend', ], 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + + 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', + # 'rest_framework.authentication.TokenAuthentication', - # 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.SessionAuthentication', # 'rest_framework.authentication.BasicAuthentication', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', @@ -211,12 +237,18 @@ 'LOGIN_SERIALIZER': 'accounts.serializers.UserLoginSerializer' } -JWT_AUTH = { - 'JWT_SECRET_KEY': SECRET_KEY, - 'JWT_ALGORITHM': 'HS256', # 암호화 알고리즘 - 'JWT_ALLOW_REFRESH': True, # refresh 사용 여부 - 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 유효기간 설정 - # JWT 토큰 갱신 유효기간 - 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=28), - # import datetime 상단에 import 하기 +# JWT_AUTH = { +# 'JWT_SECRET_KEY': SECRET_KEY, +# 'JWT_ALGORITHM': 'HS256', # 암호화 알고리즘 +# 'JWT_ALLOW_REFRESH': True, # refresh 사용 여부 +# 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 유효기간 설정 +# # JWT 토큰 갱신 유효기간 +# 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=28), +# # import datetime 상단에 import 하기 +# } +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(hours=2), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': False, # Token 재발급 관련 설정 + 'BLACKLIST_AFTER_ROTATION': True, } diff --git a/Django/NoUgly/NoUgly/urls.py b/Django/NoUgly/NoUgly/urls.py index 2271056..15a0581 100644 --- a/Django/NoUgly/NoUgly/urls.py +++ b/Django/NoUgly/NoUgly/urls.py @@ -21,11 +21,13 @@ urlpatterns = [ path('admin/', admin.site.urls), path('store/', include('store.urls')), - path('user/', include('accounts.urls')), + path('accounts/', include('accounts.urls')), # 로그인, 로그아웃, 비밀번호 재설정 및 비밀번호 변경과 같은 기본 인증 기능이 있습니다. - path('rest-auth/', include('rest_auth.urls')), + path('accounts/', include('dj_rest_auth.urls')), + path('accounts/', include('allauth.urls')), + # 등록 및 소셜 미디어 인증과 관련된 논리가 있습니다. - path('rest-auth/registration/', include('rest_auth.registration.urls')), + path('accounts/registration/', include('dj_rest_auth.registration.urls')), ] diff --git a/Django/NoUgly/accounts/migrations/0002_alter_user_gender.py b/Django/NoUgly/accounts/migrations/0002_alter_user_gender.py new file mode 100644 index 0000000..5cd221c --- /dev/null +++ b/Django/NoUgly/accounts/migrations/0002_alter_user_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-03-08 16:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='gender', + field=models.CharField(choices=[('여', '여자'), ('남', '남자')], max_length=20), + ), + ] diff --git a/Django/NoUgly/accounts/migrations/0003_alter_user_gender.py b/Django/NoUgly/accounts/migrations/0003_alter_user_gender.py new file mode 100644 index 0000000..d61b7b1 --- /dev/null +++ b/Django/NoUgly/accounts/migrations/0003_alter_user_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-03-08 16:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_alter_user_gender'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='gender', + field=models.CharField(choices=[('남', '남자'), ('여', '여자')], max_length=20), + ), + ] diff --git a/Django/NoUgly/accounts/migrations/0004_alter_user_gender.py b/Django/NoUgly/accounts/migrations/0004_alter_user_gender.py new file mode 100644 index 0000000..fd8b8af --- /dev/null +++ b/Django/NoUgly/accounts/migrations/0004_alter_user_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-03-10 23:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_alter_user_gender'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='gender', + field=models.CharField(choices=[('여', '여자'), ('남', '남자')], max_length=20), + ), + ] diff --git a/Django/NoUgly/accounts/migrations/0005_user_user_type.py b/Django/NoUgly/accounts/migrations/0005_user_user_type.py new file mode 100644 index 0000000..67e20a2 --- /dev/null +++ b/Django/NoUgly/accounts/migrations/0005_user_user_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-03-11 20:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_alter_user_gender'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='user_type', + field=models.CharField(default='basic', max_length=50), + ), + ] diff --git a/Django/NoUgly/accounts/migrations/0006_alter_user_gender.py b/Django/NoUgly/accounts/migrations/0006_alter_user_gender.py new file mode 100644 index 0000000..d5a589c --- /dev/null +++ b/Django/NoUgly/accounts/migrations/0006_alter_user_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-03-12 16:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_user_user_type'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='gender', + field=models.CharField(choices=[('남', '남자'), ('여', '여자')], max_length=20), + ), + ] diff --git a/Django/NoUgly/accounts/migrations/0007_alter_user_date.py b/Django/NoUgly/accounts/migrations/0007_alter_user_date.py new file mode 100644 index 0000000..1379c53 --- /dev/null +++ b/Django/NoUgly/accounts/migrations/0007_alter_user_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-03-13 22:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_alter_user_gender'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/Django/NoUgly/accounts/models.py b/Django/NoUgly/accounts/models.py index aa5b4b3..c2b1503 100644 --- a/Django/NoUgly/accounts/models.py +++ b/Django/NoUgly/accounts/models.py @@ -45,14 +45,14 @@ class User(AbstractBaseUser, PermissionsMixin): ('여', '여자') } name = models.CharField(max_length=15) - date = models.DateField(blank=True) + date = models.DateField(blank=True, null=True) gender = models.CharField(max_length=20, choices=Gender_choices) address = models.CharField(max_length=250, blank=True, null=True) phone_num = models.CharField( max_length=16, blank=True, null=True, unique=True) - + user_type = models.CharField(max_length=50, default='basic') is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) diff --git a/Django/NoUgly/accounts/urls.py b/Django/NoUgly/accounts/urls.py index 7edcc87..8caf387 100644 --- a/Django/NoUgly/accounts/urls.py +++ b/Django/NoUgly/accounts/urls.py @@ -1,3 +1,4 @@ +from django.urls import path, include from .views import * from rest_framework.routers import DefaultRouter @@ -7,4 +8,11 @@ router.register(r'destination', DestinationViewSet) -urlpatterns = router.urls +urlpatterns = [ + path('', include(router.urls)), + path('login/kakao/', kakao_login, name='kakao_login'), + path('login/kakao/callback/', kakao_callback, name='kakao_callback'), + path('login/kakao/django', KakaoToDjangoLogin.as_view(), + name='kakao_django_login'), + +] diff --git a/Django/NoUgly/accounts/views.py b/Django/NoUgly/accounts/views.py index ec595e4..08a8e1f 100644 --- a/Django/NoUgly/accounts/views.py +++ b/Django/NoUgly/accounts/views.py @@ -1,10 +1,18 @@ -from rest_framework import viewsets, permissions +from json import JSONDecodeError +from django.http import JsonResponse +from django.shortcuts import redirect +from rest_framework import viewsets, permissions, status from accounts.permissions import IsUserOrReadOnly - - -from .models import Destination +from allauth.socialaccount.providers.kakao import views as kakao_views +from allauth.socialaccount.providers.oauth2.client import OAuth2Client +from rest_auth.registration.views import SocialLoginView, SocialLoginSerializer +import requests +from allauth.socialaccount.models import SocialAccount +from .models import Destination, User from .serializers import DestinationSerializer # Create your views here. +import os +import urllib class DestinationViewSet(viewsets.ModelViewSet): @@ -20,3 +28,137 @@ def get_queryset(self): queryset = Destination.objects.filter(uIDX=user) return queryset + + +class KakaoException(Exception): + pass + + +def kakao_login(request): + app_rest_api_key = os.environ.get('kakao_client_id') + redirect_uri = os.environ.get('kakao_redirect_uri') + return redirect( + f"https://kauth.kakao.com/oauth/authorize?client_id={app_rest_api_key}&redirect_uri={redirect_uri}&response_type=code" + ) + + +def kakao_callback(request): + # params = urllib.parse.urlencode(request.GET) + # redirect_uri = os.environ.get('kakao_redirect_uri') + + # rturn redirect(f'{redirect_uri}?{params}') + try: + app_rest_api_key = os.environ.get('kakao_client_id') + redirect_uri = os.environ.get('kakao_redirect_uri') + user_token = request.GET.get("code") + # post request + token_request = requests.get( + f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={app_rest_api_key}&redirect_uri={redirect_uri}&code={user_token}" + ) + token_response_json = token_request.json() + error = token_response_json.get("error", None) + # if there is an error from token_request + if error is not None: + raise JSONDecodeError(error) + access_token = token_response_json.get("access_token") + print('access_token :', access_token) + # post request + profile_request = requests.post( + "https://kapi.kakao.com/v2/user/me", + headers={"Authorization": f"Bearer {access_token}"}, + ) + profile_json = profile_request.json() + # parsing profile json + kakao_account = profile_json.get("kakao_account") + email = kakao_account.get("email", None) + # 이메일은 필수제공 항목이 아니므로 수정 필요 (비즈니스 채널을 연결하면 검수 신청 후 필수 변환 가능) + profile = kakao_account.get("profile") + nickname = profile.get("nickname") + profile_image = profile.get("thumbnail_image_url") + + print('image :', profile_image) + try: + user_in_db = User.objects.get(email=email) + # kakao계정 email이 서비스에 이미 따로 가입된 email 과 충돌한다면 + print('user_in_db :', user_in_db.user_type) + if user_in_db.user_type != 'kakao': + raise KakaoException() + # if social_user is None: + # return JsonResponse( + # {'err_msg': 'email exists but not social user'}, status=status.HTTP_400_BAD_REQUEST) + # if social_user.provider != 'kakao': + # return JsonResponse( + # {'err_msg': 'no matching social type'}, status=status.HTTP_400_BAD_REQUEST) + # 이미 kakao로 가입된 유저라면 + else: + # 서비스에 rest-auth 로그인 + data = {'code': user_token, + 'access_token': access_token, + } + print('data :', data) + + accept = requests.post( + f"http://127.0.0.1:8000/accounts/login/kakao/django", + data=data, + ) + accept_status = accept.status_code + if accept_status != 200: + return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status) + accept_json = accept.json() + # accept_json.pop('user', None) + accept_jwt = accept_json.get("token") + User.objects.filter(email=email).update( + name=nickname, + email=email, + user_type='kakao', + is_active=True, + ) + except User.DoesNotExist: + # 서비스에 rest-auth 로그인 + data = {'access_token': access_token, 'code': user_token, + } + print('data :', data) + accept = requests.post( + f"http://127.0.0.1:8000/accounts/login/kakao/django", + data=data, + # encoding="UTF-8" + ) + accept_status = accept.status_code + print('User 없을때', accept_status) + if accept_status != 200: + return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status) + + accept_json = accept.json() + # accept_json.pop('user', None) + + accept_jwt = accept_json.get("token") + User.objects.filter(email=email).update( + name=nickname, + email=email, + user_type='kakao', + is_active=True, + ) + + return redirect("http://127.0.0.1:8000/") + except KakaoException: + return redirect("http://127.0.0.1:8000/account/login") + + +class KakaoToDjangoLogin(SocialLoginView): + adapter_class = kakao_views.KakaoOAuth2Adapter + client_class = OAuth2Client + serializer_class = SocialLoginSerializer + + def get_serializer(self, *args, **kwargs): + serializer_class = self.get_serializer_class() + kwargs['context'] = self.get_serializer_context() + return serializer_class(*args, **kwargs) + + +def naver_login(request): + app_rest_api_key = os.environ.get('naver_client_id') + redirect_uri = "http://127.0.0.1:8000/accounts/login/naver/callback/" + return redirect( + f"https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id={app_rest_api_key}&state=STATE_STRING&redirect_uri={redirect_uri}" + + ) diff --git a/Django/NoUgly/store/models.py b/Django/NoUgly/store/models.py index efe7143..1b4888b 100644 --- a/Django/NoUgly/store/models.py +++ b/Django/NoUgly/store/models.py @@ -65,7 +65,8 @@ class Order(models.Model): max_length=500, verbose_name='주소지', null=False) uIDX = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, verbose_name='회원', null=True) - fIDX = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True) + fIDX = models.ForeignKey( + Product, on_delete=models.SET_NULL, null=True) class Meta: verbose_name = '주문' diff --git a/Django/NoUgly/store/serializers.py b/Django/NoUgly/store/serializers.py index cfe7dd2..711ba59 100644 --- a/Django/NoUgly/store/serializers.py +++ b/Django/NoUgly/store/serializers.py @@ -15,13 +15,10 @@ class ProductNamePriceSerializer(serializers.ModelSerializer): class Meta: model = Product - fields = ['name', 'price', 'image'] + fields = ['fIDX', 'name', 'price', 'image'] class ProductKindSerializer(serializers.ModelSerializer): - - # products = ProductSerializer(many=True, read_only=True) - class Meta: model = Product_kind fields = '__all__' @@ -39,10 +36,41 @@ def to_representation(self, instance): return response +class OrderListSerializer(serializers.ListSerializer): + + def create(self, validated_data): + orders = [Order(**item) for item in validated_data] + return Order.objects.bulk_create(orders, ignore_conflicts=True) + + def update(self, instance, validated_data): + return super().update(instance, validated_data) + # def update(self, instance, validated_data): + # # Maps for id->instance and id->data item. + # order_mapping = {order.order_id: order for order in instance} + # data_mapping = {item['order_id']: item for item in validated_data} + + # # Perform creations and updates. + # ret = [] + # for order_id, data in data_mapping.items(): + # order = order_mapping.get(order_id, None) + # if order is None: + # ret.append(self.child.create(data)) + # else: + # ret.append(self.child.update(order, data)) + + # # Perform deletions. + # for order_id, order in order_mapping.items(): + # if order_id not in data_mapping: + # order.delete() + + # return ret + + class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order - exclude = ['uIDX'] + fields = ['order_id', 'count', 'price', 'destination', 'fIDX'] + list_serializer_class = OrderListSerializer def to_representation(self, instance): response = super().to_representation(instance) diff --git a/Django/NoUgly/store/views.py b/Django/NoUgly/store/views.py index d97a838..c1657a9 100644 --- a/Django/NoUgly/store/views.py +++ b/Django/NoUgly/store/views.py @@ -1,5 +1,4 @@ -from __future__ import barry_as_FLUFL -from math import prod +from typing import Optional from .serializers import * from rest_framework import permissions, viewsets, status from accounts.permissions import IsUserOrReadOnly @@ -75,6 +74,25 @@ def get_queryset(self): return queryset + def create(self, request, *args, **kwargs): + serializer: CartProuductSerializer = self.get_serializer( + data=request.data) + serializer.is_valid(raise_exception=True) + fIDX = serializer.validated_data['fIDX'] + cart_product: Optional[Cart_product] = self.get_queryset().filter( + fIDX=fIDX).first() + + if cart_product is not None: + cart_product.quantity = F('quantity') + \ + serializer.validated_data["quantity"] + cart_product.save() + cart_product.refresh_from_db() + return Response(self.get_serializer(cart_product, many=False).data, status=status.HTTP_200_OK) + else: + serializer.save() + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + class OrderViewSet(viewsets.ModelViewSet): queryset = Order.objects.all() @@ -88,3 +106,25 @@ def get_queryset(self): queryset = Order.objects.filter(uIDX=user) return queryset + + def create(self, request, *args, **kwargs): + many = isinstance(request.data, list) + print("many : ", many) + if not many: + serializer = self.get_serializer(data=request.data, many=many) + print("시리얼라이저 단일존재 :", serializer) + serializer.is_valid(raise_exception=True) + user = self.request.user + serializer.save(uIDX_id=user.id) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, headers=headers, status=status.HTTP_201_CREATED) + else: + serializer = self.get_serializer(data=request.data, many=many) + print("시리얼라이저 복수존재 :", serializer) + serializer.is_valid(raise_exception=True) + user = self.request.user + serializer.save(uIDX_id=user.id) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, headers=headers, status=status.HTTP_201_CREATED)