added jwt auth, base player logic

This commit is contained in:
Alexander Karpov 2022-06-05 13:09:27 +03:00
parent 00387092df
commit 80a63fe252
9 changed files with 184 additions and 24 deletions

View File

@ -15,19 +15,18 @@ from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-%_8sy196w4hzo9^cp9(@r=i+amh47r4mxfhq_(ok&=c(@%bhmk' SECRET_KEY = 'django-insecure-%_8sy196w4hzo9^cp9(@r=i+amh47r4mxfhq_(ok&=c(@%bhmk'
TOKEN_EXP = 2678400 # 31 day
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -76,7 +75,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'chess_backend.wsgi.application' WSGI_APPLICATION = 'chess_backend.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
@ -87,7 +85,6 @@ DATABASES = {
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
@ -106,7 +103,6 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/ # https://docs.djangoproject.com/en/4.0/topics/i18n/

25
game/authentication.py Normal file
View File

@ -0,0 +1,25 @@
from rest_framework import authentication
from rest_framework import exceptions
from .models import Player
from .services.jwt import read_jwt
class PlayerAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
token = request.headers["Authorization"]
if not token:
raise exceptions.AuthenticationFailed("No credentials provided.")
t = read_jwt(token)
if not t:
raise exceptions.AuthenticationFailed("Token is incorrect of expired")
if "id" not in t:
raise exceptions.AuthenticationFailed("No user data")
try:
user = Player.objects.get(id=int(t["id"]))
except Player.DoesNotExist:
raise exceptions.AuthenticationFailed("No such user")
return user, None

View File

@ -1,7 +1,15 @@
import random
import uuid import uuid
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.files import File
from django.core.validators import (
MinValueValidator,
MaxValueValidator,
MinLengthValidator,
MaxLengthValidator,
)
from django.db import models from django.db import models
from django.conf import settings
HER0_TYPES = [("WIZARD", "wizard"), ("ARCHER", "archer"), ("WARRIOR", "warrior")] HER0_TYPES = [("WIZARD", "wizard"), ("ARCHER", "archer"), ("WARRIOR", "warrior")]
@ -9,17 +17,47 @@ HER0_TYPES = [("WIZARD", "wizard"), ("ARCHER", "archer"), ("WARRIOR", "warrior")
class Player(models.Model): class Player(models.Model):
"""base model to handle and store users""" """base model to handle and store users"""
# TODO: connect real TON wallet ton_wallet = models.CharField(
ton_wallet = models.CharField(max_length=50, verbose_name="TON wallet") verbose_name="TON wallet",
validators=[MinLengthValidator(48), MaxLengthValidator(48)],
max_length=48,
unique=True,
)
name = models.CharField(max_length=100, blank=True) name = models.CharField(max_length=100, blank=True)
added = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
super(Player, self).save()
types = (
["ARCHER" for _ in range(4)]
+ ["WARRIOR" for _ in range(6)]
+ ["WIZARD" for _ in range(2)]
+ [random.choice(HER0_TYPES)[0] for _ in range(4)]
)
for t in types:
hero = Hero()
hero.player = self
hero.type = t
with open("/home/sanspie/Projects/chess_rpg_backend/media/dummy.jpg", "rb+") as file:
hero.idle_img = File(file, name="dummy.jpg")
hero.attack_img = File(file, name="dummy.jpg")
hero.die_img = File(file, name="dummy.jpg")
hero.health = random.randint(0, 10)
hero.attack = random.randint(0, 10)
hero.speed = random.randint(0, 10)
hero.save()
def __str__(self): def __str__(self):
return self.name return self.name
class Meta: class Meta:
indexes = [models.Index(fields=["ton_wallet"])] indexes = [models.Index(fields=["ton_wallet"])]
ordering = ["-added"] ordering = ["-created"]
verbose_name = "player" verbose_name = "player"
verbose_name_plural = "players" verbose_name_plural = "players"
@ -28,12 +66,12 @@ class Hero(models.Model):
"""Model to store heroes and their stats, connected to player""" """Model to store heroes and their stats, connected to player"""
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
# player = models.ForeignKey( player = models.ForeignKey(
# Player, Player,
# on_delete=models.CASCADE, on_delete=models.CASCADE,
# related_name="heroes", related_name="heroes",
# related_query_name="hero", related_query_name="hero",
# ) )
added = models.DateTimeField(auto_now_add=True) added = models.DateTimeField(auto_now_add=True)
type = models.CharField(blank=False, choices=HER0_TYPES, max_length=7) type = models.CharField(blank=False, choices=HER0_TYPES, max_length=7)
@ -43,6 +81,9 @@ class Hero(models.Model):
health = models.IntegerField( health = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False
) )
attack = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False
)
speed = models.IntegerField( speed = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False
) )

View File

@ -1,12 +1,20 @@
from rest_framework import serializers from rest_framework import serializers
from game.models import Hero from game.models import Hero, Player
class CreateHeroSerializer(serializers.ModelSerializer): class CreateHeroSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Hero model = Hero
fields = ("type", "idle_img", "attack_img", "die_img", "health", "speed") fields = (
"type",
"idle_img",
"attack_img",
"die_img",
"health",
"attack",
"speed",
)
class GetHeroSerializer(serializers.ModelSerializer): class GetHeroSerializer(serializers.ModelSerializer):
@ -19,5 +27,12 @@ class GetHeroSerializer(serializers.ModelSerializer):
"attack_img", "attack_img",
"die_img", "die_img",
"health", "health",
"attack",
"speed", "speed",
) )
class CreatePlayerView(serializers.ModelSerializer):
class Meta:
model = Player
fields = ("ton_wallet", "name")

View File

49
game/services/jwt.py Normal file
View File

@ -0,0 +1,49 @@
import jwt
import pytz
from datetime import datetime
from django.conf import settings
from jwt import ExpiredSignatureError, InvalidSignatureError
TIMEZONE = pytz.timezone("Europe/Moscow")
def sign_jwt(data: dict, t_life: None | int = None) -> str:
"""generate and sign jwt with iat and exp using data from settings"""
iat = int(datetime.now(tz=TIMEZONE).timestamp())
exp = iat + settings.TOKEN_EXP if not t_life else iat + t_life
payload = {"iat": iat, "exp": exp}
for nm, el in data.items():
if nm not in ["iat", "exp"]:
payload[nm] = el
secret = settings.SECRET_KEY
token = jwt.encode(payload=payload, key=secret)
return token
def read_jwt(token: str) -> dict | bool:
"""reads jwt, validates it and return payload if correct"""
header_data = jwt.get_unverified_header(token)
secret = settings.SECRET_KEY
try:
payload = jwt.decode(token, key=secret, algorithms=[header_data["alg"]])
except ExpiredSignatureError as e:
return False
except InvalidSignatureError as e:
return False
if "exp" not in payload:
return False
if int(datetime.now(tz=TIMEZONE).timestamp()) > payload["exp"]:
return False
payload.pop("iat", None)
payload.pop("exp", None)
return payload
def generate_refresh_token(payload: dict) -> str:
return sign_jwt(payload)

View File

@ -1,8 +1,9 @@
from django.urls import path from django.urls import path
from game.views import CreateHeroView, RetrieveHeroView from game.views import CreateHeroView, RetrieveHeroView, PlayerCreateView
urlpatterns = [ urlpatterns = [
path("hero/", CreateHeroView.as_view(), name="hero_api_create"), path("hero/", CreateHeroView.as_view(), name="hero_api_create"),
path("hero/<uuid:uuid>", RetrieveHeroView.as_view(), name="hero_api_retrieve"), path("hero/<uuid:uuid>", RetrieveHeroView.as_view(), name="hero_api_retrieve"),
] path("player/", PlayerCreateView.as_view(), name="player_create_api"),
]

View File

@ -1,15 +1,17 @@
from rest_framework import status from rest_framework import status
from rest_framework.generics import GenericAPIView, UpdateAPIView from rest_framework.generics import GenericAPIView, UpdateAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, ListModelMixin
from rest_framework.response import Response from rest_framework.response import Response
from game.authentication import PlayerAuthentication
from game.models import Hero from game.models import Hero
from game.serializers import CreateHeroSerializer, GetHeroSerializer from game.serializers import CreateHeroSerializer, GetHeroSerializer, CreatePlayerView
from game.services.jwt import sign_jwt
class CreateHeroView(GenericAPIView, CreateModelMixin): class CreateHeroView(GenericAPIView, CreateModelMixin, ListModelMixin):
serializer_class = CreateHeroSerializer authentication_classes = (PlayerAuthentication,)
def perform_create(self, serializer): def perform_create(self, serializer):
return serializer.save() return serializer.save()
@ -20,9 +22,22 @@ class CreateHeroView(GenericAPIView, CreateModelMixin):
instance = self.perform_create(serializer) instance = self.perform_create(serializer)
return Response({"uuid": instance.uuid}, status=status.HTTP_201_CREATED) return Response({"uuid": instance.uuid}, status=status.HTTP_201_CREATED)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def get_serializer_class(self):
if self.request.method == "GET":
return GetHeroSerializer
else:
return CreateHeroSerializer
def get_queryset(self):
return Hero.objects.filter(player_id=self.request.user.id)
class RetrieveHeroView(RetrieveModelMixin, UpdateAPIView, GenericAPIView): class RetrieveHeroView(RetrieveModelMixin, UpdateAPIView, GenericAPIView):
serializer_class = GetHeroSerializer serializer_class = GetHeroSerializer
authentication_classes = (PlayerAuthentication,)
lookup_field = "uuid" lookup_field = "uuid"
queryset = Hero.objects.all() queryset = Hero.objects.all()
@ -34,3 +49,20 @@ class RetrieveHeroView(RetrieveModelMixin, UpdateAPIView, GenericAPIView):
def patch(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs) return self.partial_update(request, *args, **kwargs)
class PlayerCreateView(GenericAPIView, CreateModelMixin):
serializer_class = CreatePlayerView
def perform_create(self, serializer):
return serializer.save()
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer)
# TODO: add JTI to refresh token
access_jwt = sign_jwt({"id": instance.id, "type": "access"}, t_life=3600)
refresh_jwt = sign_jwt({"id": instance.id, "type": "refresh"})
return Response({"access_token": access_jwt, "refresh_token": refresh_jwt}, status=status.HTTP_201_CREATED)

View File

@ -4,3 +4,4 @@ djangorestframework==3.13.1
Pillow==9.1.1 Pillow==9.1.1
pytz==2022.1 pytz==2022.1
sqlparse==0.4.2 sqlparse==0.4.2
PyJWT==2.4.0