Backend - account app

kukudas·2022년 2월 28일
0

industry-app-client

목록 보기
6/11

EveLoginViewSet.callback()에서 EVE SSO랑 통신해서 ESI 조회에 사용되는 access_token을 발급 받고 해당 토큰으로 토큰 소유자의 케릭터 이름이랑 케릭터 아이디를 가져옴.

이렇게 가져온 케릭터 이름으로 이메일형식으로 이메일 하나 만들고, 이름은 케릭터 이름으로 하고 비밀번호는 어차피 EVE SSO를 통해서 로그인하게되니 랜덤스트링으로 만들어주고 케릭터 아이디도 포함해서 유저를 생성해줌.

유저를 생성하거나 이미 유저가 존재할 수 있는데 각각 백엔드 토큰을 생성하거나 이미 존재하는거를 리턴해줌.

// account/views.py
from rest_framework import viewsets, serializers, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.authtoken.models import Token
from .models import EveAccessToken, User
from .serializers import LoginSerializer, UserSerializer, EveUserSerializer, EveAccessTokenSerializer, InvalidPassword

# eve login
import requests
import base64
import os
from dotenv import load_dotenv
from .utils import url_creator, email_creator, create_random_string

# eve access token
import datetime

# celery task
from .tasks import get_industry_jobs

# 이브 로그인 관련
class EveLoginViewSet(viewsets.GenericViewSet):
    permission_classes = [AllowAny]
    queryset = User.objects.all()
    serializer_class = EveUserSerializer

    @action(methods=['get'], detail=False)
    def callback(self, request):
        # get()
        # Returns the value for key in the dictionary; if not found returns a default value.
        # Optional. 
        # Value that is returned when the key is not found. Defaults to None, so that this method never raises a KeyError.
        
        # 저쪽에서 이쪽으로 request 보낸거는 정상으로 간주하고 해야함
        auth_code = request.GET.get('code')
        state = request.GET.get('state')

        # Now that your application has the authorization code, 
        # it needs to send a POST request to
        # https://login.eveonline.com/v2/oauth/token
        # where your application’s client ID will be the user
        #  your secret key will be the password
        load_dotenv()
        client_id = os.getenv('ID')
        secret_key = os.getenv('KEY')

        # You will need to send the following HTTP headers (replace anything between <>, including <>)
        # Authorization: Basic <URL safe Base64 encoded credentials>
        # Content-Type: application/x-www-form-urlencoded
        # Host: login.eveonline.com
        user_pass = f'{client_id}:{secret_key}'
        basic_auth = base64.urlsafe_b64encode(user_pass.encode()).decode()
        auth_header = f'Basic {basic_auth}'

        headers = {
            "Authorization": auth_header,
            "Content-Type": "application/x-www-form-urlencoded",
            "Host": "login.eveonline.com",
        }
        body = {
            'grant_type': 'authorization_code',
            'code': auth_code
        }

        # Finally, send a POST request to https://login.eveonline.com/v2/oauth/token with your form encoded values and the headers from the last step.
        try:
            res = requests.post(
                'https://login.eveonline.com/v2/oauth/token',
                headers=headers,
                data=body
            )
            # If the previous step was done correctly, the EVE SSO will respond with a JSON payload containing an access token (which is a Json Web Token) 
            # and a refresh token that looks like this (Anything wrapped by <> will look different for you):
            res_dict = res.json()
            # access_token = res_dict.get('access_token')
            # get()은 default를 return 하니때문에 keyerror생성안하니 get()이거 쓰면 안됨
            access_token = res_dict['access_token']
            res_dict['expires_in'] = datetime.datetime.now() + datetime.timedelta(minutes=19, seconds=59)
            # 이거 어차피 여기서 access_token이 안온거면 연결이  실패한거임
            # 그러니까 예외는 여기서 keyError하나만 잡고 나머지 다른 에러는
            # django에서 500에러 주니 이거 logging 해서 잡아내면됨
            # 클라이언트는 잡다한 예외 상황 알 필요없고 여기 기준으로는 이브와의 서버 통신을 실패한거만 알면 되기 떄문에
            # 그냥 access_token 없으면 이브 서버와의 통신이 실패한거니 실패했다고 알려주면됨.
        except KeyError:
            return Response({"status": "failed", "errors": "이브서버와 통신을 실패했습니다."})

        # 여기서 말하는 JSON payload가 위의 res에 저장됨

        # access_token으로 eve character name 가져옴
        acc = f'Bearer {access_token}'
        try:
            character_res = requests.get(
                "https://login.eveonline.com/oauth/verify",
                headers={"Authorization": acc}
            )
            character_dict = character_res.json()
            character_id = character_dict['CharacterID']
        except KeyError:
            return Response({"status": "failed", "errors": "이브서버와 통신을 실패했습니다."})

        # create django user and return its token
        # eve_user = {} 이렇게 하는거보다 {}이 set, dictionary 둘 다여서 dict()해주는게 좋음
        eve_user = dict()
        eve_user_email = email_creator(character_dict['CharacterName'])
        eve_user['email'] = eve_user_email
        eve_user['name'] = character_dict['CharacterName']
        eve_user['password'] = create_random_string()
        eve_user['character_id'] = character_id

        temp_dict = res_dict.copy()
        temp_dict['user'] = eve_user

        # EveAccessToken 저장
        serializer = EveAccessTokenSerializer(data=temp_dict)
        try:
            serializer.is_valid(raise_exception=True)
            serializer.save()

            # celery로 job 받아오기
            get_industry_jobs.delay(character_id, access_token, eve_user_email)
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        except serializers.ValidationError:
            return Response({"status": "failed login user via eve account", "errors": serializer.errors})

# drf login
# https://stackoverflow.com/questions/26906630/django-rest-framework-authentication-credentials-were-not-provided 이거 지금 해결안되고있음
class AccountViewSet(viewsets.GenericViewSet):
    # permission_classes = [AllowAny]
    queryset = User.objects.all()
    serializer_class = UserSerializer

    # @action은
    # This decorator can be used to add any custom endpoints that don't fit into the standard create/update/delete style.
    # @action decorator will respond to GET requests by default.
    # We can use the methods argument if we wanted an action that responded to POST requests.
    # /.../foo/bar나
    # /.../foo/{pk}/bar
    # 이런 api를 추가하고싶을때쓰는거임
    # detail=False면 위 detail=True면 아래

    # @action(detail=True, methods=['get'])
    # def asd(self, request, pk):
    # 이거면 http://localhost:8000/test/login/[pk]/asd 여기로 등록됨
    # detail=False여야지만 http://localhost:8000/test/login/asd/로 감
    # http GET http://127.0.0.1:8000/api/v1/user/asd
    @action(methods=['get'], detail=False, permission_classes=[AllowAny])
    def asd(self, request):
        serializer = self.serializer_class(self.queryset, many=True)
        return Response(serializer.data)

    # /api/v1/user 로 GET요청 받을 user list api (UserSerializer 사용)
    # http GET http://127.0.0.1:8000/api/v1/user "Authorization: Token 01ecad58c74bb6a6c52ab3f8cb6946cb7312e0d6"
    def list(self, request):
        # self.get_queryset() 이거로 queryset = User.objects.all() 이거 가져오는거임
        queryset = self.get_queryset()
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

    # /api/v1/user/pk 로 GET요청 받을 user retrieve api (UserSerializer사용)
    # http GET http://127.0.0.1:8000/api/v1/user/48 "Authorization: Token 01ecad58c74bb6a6c52ab3f8cb6946cb7312e0d6"
    def retrieve(self, request, pk):
        # queryset과 pk값을 인자로 받아서,
        # queryset.filter(pk=pk)로 queryset을 뽑고,
        # instance = queryset.get()으로 객체만 뽑아서 리턴해 주는 메소드임
        # => 결국, 위 코드는 Customer.objects.get(pk=pk) 리턴함
        # https://velog.io/@jcinsh/RetrieveUpdateDestroyView-%EC%9D%B4%ED%95%B4 참조
        # queryset = User.objects.all() 이거를 기반으로 하는거임
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

    # /api/v1/user/pk 로 PATCH 요청 받을 user update ap
    # http PUT http://127.0.0.1:8000/api/v1/user/50 "Authorization: Token 01ecad58c74bb6a6c52ab3f8cb6946cb7312e0d6"
    # 이거 비밀번호를 바꾸려면 userserilaier에서 비밀번호가 readonly여서 안보이니 비밀번호를 바꾸려면 serializer를 새로 만들어야함
    def update(self, request, pk):
        instance = self.get_object()
        # data앞에 뭐가 있으니 업데이트함
        serializer = self.get_serializer(instance, data=request.data, partial=True)
        try:
            serializer.is_valid(raise_exception=True)
            serializer.save()
            return Response(serializer.data)
        except serializers.ValidationError:
            return Response({"status": "failed", "errors": serializer.errors})

    # /api/v1/user/pk 로 DELETE 요청 받을 user delete api (Serializer 사용 x)
    # http DELETE http://127.0.0.1:8000/api/v1/user/2000 "Authorization: Token 01ecad58c74bb6a6c52ab3f8cb6946cb7312e0d6"
    def delete(self, request, pk):
        print("in delete")
        instance = self.get_object()
        instance.delete()
        return Response({'success': True}, status=status.HTTP_204_NO_CONTENT)

    # /api/v1/user 로 POST요청을 받을 registration api (UserSerializer 사용)
    # http POST http://127.0.0.1:8000/api/v1/user/register email="id@gmail.com" password="pw"
    @action(detail=False, methods=['post'], permission_classes=[AllowAny])
    def register(self, request, eve_user=None):
        print("in register")
        print(eve_user)
        if eve_user:
            serializer = self.get_serializer(data=eve_user)
            try:
                serializer.is_valid(raise_exception=True)
                # 이거 save()했을때 불려오는 method는
                # serializer = UserSerializer(data=request.data)에서 data앞에 뭐가 없으면
                # UserSerializer.create()를 불러오는거임
                serializer.save()
                # 이거 pw는 write_only라서 안보임
                print("in register2")
                print(serializer.data)
                return Response(serializer.data)
            except serializers.ValidationError:
                return Response({"status": "failed", "errors": serializer.errors})
        else: 
            serializer = self.get_serializer(data=request.data)
            try:
                serializer.is_valid(raise_exception=True)
                # 이거 save()했을때 불려오는 method는
                # serializer = UserSerializer(data=request.data)에서 data앞에 뭐가 없으면
                # UserSerializer.create()를 불러오는거임
                serializer.save()
                # 이거 pw는 write_only라서 안보임
                print(serializer.data)
                return Response(serializer.data)
            except serializers.ValidationError:
                return Response({"status": "failed", "errors": serializer.errors})

    # /api/v1/user/login 으로 POST요청을 받을 login api (LoginSerializer 사용)
    # http POST http://127.0.0.1:8000/api/v1/user/login email="id@gmail.com" password="pw"
    # 로그아웃 안하고 서버가 그냥 닫아지면 토큰 값이 유지되는거 같음 어떻게 해결해야함?
    # 이거 get_or_create로 하기때문에 같은거임 안그러면 서버껐다켰는데 애들다 토큰 날아가서 다 에러나거나 로그인페이지로 날아감
    # 해결하기 위해서는 토큰에 만료기한을 두고 토큰유출대도 credential이 없으면 만료시점이후에는 무효처리되도록 다시로그인시켜보는거지
    @action(detail=False, methods=['post'], permission_classes=[AllowAny])
    def login(self, request):
        serializer = LoginSerializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
            serializer.save()
            return Response(serializer.data)
        except InvalidPassword:
            return Response({'success': False, 'error': '패스워드가 일치하지 않습니다'}, status=status.HTTP_400_BAD_REQUEST)
        # 이거는 user = User.objects.get(email=email)에서 없으면 자동으로 raise됨
        except User.DoesNotExist:
            return Response({'success': False, 'error': '유저가 존재하지 않습니다'}, status=status.HTTP_400_BAD_REQUEST)

    # /api/v1/user/logout 으로 DELETE 요청을 받을 logout api(Serializer 사용 x)
    # http POST http://127.0.0.1:8000/api/v1/user/logout "Authorization: Token f44c39fec18227d5aa555dcbd20aa7d56d0f55ef"
    @action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
    def logout(self, request):
        request.user.auth_token.delete()

        return Response({'success': "로그아웃 성공"}, status=status.HTTP_200_OK)

    # 특정 유저가 publish 한 post 보기
// account/serializers.py
from requests.api import get
from rest_framework import serializers
from .models import User, EveAccessToken
from django.contrib.auth import get_user_model
from rest_framework.authtoken.models import Token
# get_user_model() : 클래스이다.
# >>> get_user_model()
# <class 'accounts.models.User'>
# settings.AUTH_USER_MODEL : 문자열이다.
# >>> settings.AUTH_USER_MODEL
# >>> settings.AUTH_USER_MODEL
# 'accounts.User'

# exceptions
class InvalidPassword(Exception):
    pass


class EveUserSerializer(serializers.Serializer):
    email = serializers.CharField(write_only=True)
    name = serializers.CharField(write_only=True)
    password = serializers.CharField(write_only=True)
    character_id = serializers.IntegerField(write_only=True)

# https://stackoverflow.com/questions/42314882/drf-onetoonefield-create-serializer
class EveAccessTokenSerializer(serializers.Serializer):
    # required=True는 default임
    # 이거 돌려줄 필요없음
    user = EveUserSerializer(write_only=True)
    access_token = serializers.CharField()
    expires_in = serializers.DateTimeField(write_only=True)
    token_type = serializers.CharField(write_only=True)
    refresh_token = serializers.CharField(write_only=True)
    # 여기 token 넣어줘야지 create()에서 리턴해줄 수 있음 아니면 serialize를 못함
    token = serializers.CharField(read_only=True)


    class Meta:
        model = EveAccessToken
        fields = ['id', 'user', 'access_token', 'expires_in', 'token_type', 'refresh_token', 'token']

    def create(self, validated_data):
        user_data = validated_data.pop('user')
        user_email = user_data.pop('email')
        # 계정을 kwargs로 찾고 계정이 없으면 kwargs랑 defaults 둘 다 이용해서 생성해줌
        user_instance, _ = User.objects.get_or_create(email=user_email, defaults=user_data)
        # 내 토큰 발급
        token, _ = Token.objects.get_or_create(user=user_instance)
        # EAT update하거나 생성
        EveAccessToken.objects.update_or_create(user=user_instance, defaults=validated_data)
        # 여기서 return 하는 instance랑 시리얼라이저의 field를 기반으로 serializer.data가 만들어짐
        # 여기서 리턴하는게 field에 있어야만 serializer.data에 들어가는거임
        # 여기서 튜플리턴하는데 없는것들 있어서 안가지는거임
        #   return instance, user_instance, updated, 201, {"token": token.key}
        return {"token": token.key}

    def update(self, instance, validated_data):
        user_data = validated_data.pop('user')
        user_instance = User.objects.get(email=user_data['email'])
        validated_data['user'] = user_instance

        for key, value in validated_data.items():
            if hasattr(instance, key):
                setattr(instance, key, value)
        instance.save()

        # print("in EAT 시리얼라지어 INSTANCE=", instance)
        return instance


# drf
# 1. Serializer를 상속받은 LoginSerializer, 그리고 ModelSerializer를 상속받은 UserSerializer 두 개 작성
# 2. 각 serializer는 아래와 같은 field를 가지고 이름에 맞는 동작을 해야함
# https://eunjin3786.tistory.com/253
# LoginSerializer
#     쓰기전용 : username, password
#     읽기전용 : token
#     동작 : username과 password로 user인증을 하고, user의 token이 있으면 그것을, 없으면 새로 발행해서 돌려줌
class LoginSerializer(serializers.Serializer):
    # write_only는 값을 받아서 create/update같은거만 하는거임
    email = serializers.CharField(write_only=True)
    # required=False
    password = serializers.CharField(write_only=True)
    # 입력받는게 아니라 return으로 돌려주는 값이니까 read_only임
    token = serializers.CharField(read_only=True)

    # 로그인 확인
    def create(self, validated_data):
        email = validated_data['email']
        password = validated_data['password']

        user = User.objects.get(email=email)

        # 비밀번호 일치하면
        if user.check_password(password):
            token, _ = Token.objects.get_or_create(user=user)
            # 이거 그냥 return token 하면 안돼는 이유는
            # 밑의 유저시리얼라이저에서는 user.token에 token.key를 넣어줬는데
            # 이거 리턴되는 object에서 getattr로 리턴 field들이 다 있으면 알아서 만들어주는건데
            # 밑에서는 리턴이 user객체였고 이 객체에는 class Meta에 있는 3개의 field가 다 있어서 문제없이 된거고
            # 지금은 token을 리턴하게 되면 token.token은 없고 token.key에 원하는값이있지
            # 그래서 지금 에러인 {}가 가는거임
            # token을 리턴하면 Token object인데 내가 serializer에 리턴하겠다고 명시한건
            # token이라는 필드고 리턴한걸 serialize했을때 리턴 결과물에 token이라는 attribute가 있어야하는데
            # token.token이 없으니 아무런 결과물이 안나오는거임
            # 이제 {'token': token.key}를 리턴하면 getattr(리턴 결과물, 'token')하면
            # 값이 있으니 결과물이 나오는거임
            # 객체 serialize하면 __str__()로 나오는 결과물이 나옴
            # print(getattr(token, 'key'))
            print(token.key)
            return {'token': token.key}
        # 일치하는 비밀번호가 없으면
        raise InvalidPassword


# UserSerializer
#     둘 다 : 필요한 모든 유저정보들
#     쓰기전용 : password
#     읽기전용 : token
#     동작 : 받은 user정보를 통해 User를 생성하고, 생성된 user의 token을 새로 발행해서 password를 제외한 나머지 정보와 함께 돌려줌
class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=False)
    token = serializers.CharField(read_only=True)

    class Meta:
        model = get_user_model()
        fields = ['email', 'password', 'name', 'character_id', 'token']

    # 이거 근데 user.mode()에서 해주는데 없어야하는거 같음
    # override하는거니까 인자 맞춰줘야함
    def create(self, validated_data):
        # print("유저시리얼라이저 - create")
        # 이제 이 validate_data는 validation도 끝났으니 정합한 데이터니 그냥 때려버리면 됨
        # 이거 **은
        # **d means "take all additional named arguments to this function
        # and insert them into this parameter as dictionary entries."
        # 이거 create_user가 인자를 2개 받아야하니 **validated_data
        # print(validated_data)
        user = User.objects.create_user(**validated_data)
        # 가져오거나 생성한값이랑
        # 생성됐는지 가져온건지 여부를
        # 튜플로줌
        # 저게 생성으로 가져온 결과물이면 True
        # 있던거 가져온거면 False
        # _는 안쓸값은 _로 저장함
        token, _ = Token.objects.get_or_create(user=user)
        # 이거 가능한 이유는 user는 User의 인스턴스고 파이썬 클래스는 getter setter없이 걍 뭐든 할 수 있어서
        # 이렇게 하면 token이 추가가 가능한거임
        # 추가로 걍 객체를 serialize시키면 아까말한 __str__()값이 들어감
        user.token = token
        # token object의 key가 사용하는 token임
        return {"token": token.key}

    def update(self, instance, validated_data):
        # items()은 key, value 튜플쌍을 튜플로 리턴하는 함수임
        # print(instance.password)
        for key, value in validated_data.items():
            # 제일 첫번째 인자에 key가 있으면 True 없으면 False 반환하는게 hasattr이고
            if hasattr(instance, key):
                # 값 저장하는게 setattr
                setattr(instance, key, value)
        instance.save()
        return instance

0개의 댓글