mirror of
https://github.com/evgen-app/chess_rpg_backend.git
synced 2024-11-22 09:37:05 +03:00
added jwt auth, base player logic
This commit is contained in:
parent
00387092df
commit
80a63fe252
|
@ -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
25
game/authentication.py
Normal 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
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
0
game/services/__init__.py
Normal file
0
game/services/__init__.py
Normal file
49
game/services/jwt.py
Normal file
49
game/services/jwt.py
Normal 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)
|
|
@ -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"),
|
||||||
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue
Block a user