From 23739b31f90a2b7be5838a99bc1f229e2d90970b Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sat, 29 Dec 2018 19:41:18 -0600 Subject: [PATCH] Added first pass of an api key system +API Keys are kept in the database and are 25 characters long +Each key will have certain permission allowed to it +Only permission right now is for using bot commands --- admin.py | 3 +- api/commands.py | 69 +++++++++++++++++++++---------------------- api/models.py | 1 + api/views.py | 38 +++++++++++++++++++----- models.py | 17 +++++++++++ test/test_commands.py | 40 ++++++++++++++----------- test/test_key.py | 7 +++++ util.py | 12 ++++++++ views.py | 1 + 9 files changed, 126 insertions(+), 62 deletions(-) create mode 100644 api/models.py create mode 100644 test/test_key.py create mode 100644 util.py diff --git a/admin.py b/admin.py index a149091..8028060 100644 --- a/admin.py +++ b/admin.py @@ -6,4 +6,5 @@ admin.site.register(Player) admin.site.register(Base) admin.site.register(Shop) admin.site.register(Tunnel) -admin.site.register(ItemListing) \ No newline at end of file +admin.site.register(ItemListing) +admin.site.register(APIToken) diff --git a/api/commands.py b/api/commands.py index 09fe633..cfd6a55 100644 --- a/api/commands.py +++ b/api/commands.py @@ -7,7 +7,16 @@ import inspect command_dict = {"GET": {}, "POST": {}, "DELETE": {}} -def getRequiredArgs(func): +def objects_list_to_json(obj_list): + json_list = [] + + for obj in obj_list: + json_list.append(obj.json) + + return json_list + + +def get_required_args(func): args = inspect.getfullargspec(func) return args.args + args.kwonlyargs @@ -16,7 +25,8 @@ def getRequiredArgs(func): def command(type): def command_dec(func): def add_command(): - command_dict[type][func.__name__] = {"func": func, "params": getRequiredArgs(func)} + command_dict[type][func.__name__] = {"func": func, "params": get_required_args(func)} + return func return add_command() @@ -82,7 +92,7 @@ def add_location(x_pos, z_pos, name=None, discord_uuid=None, mc_uuid=None, loc_t try: get_location(player, name, loc_type=loc_type) raise EntryNameNotUniqueError - except Location.DoesNotExist: + except (Location.DoesNotExist, NoLocationsInDatabase): if name is None: name = "{}'s {}".format(player.name, loc_type.__name__) @@ -106,23 +116,19 @@ def add_tunnel(tunnel_direction, tunnel_number, location_name=None, discord_uuid tunnel = Tunnel.objects.create(tunnel_direction=tunnel_direction, tunnel_number=tunnel_number, location=get_location(player, location_name)) - return tunnel + return tunnel.json @command("GET") def find_location(search): limit = 25 - locations_obj = Location.objects.filter(Q(name__icontains=search) | Q(owner__name__icontains=search)).all()[:limit] + locations = Location.objects.filter(Q(name__icontains=search) | Q(owner__name__icontains=search)).all()[:limit] - if len(locations_obj) == 0: + if len(locations) == 0: raise LocationLookUpError - locations_json = [] - for location in locations_obj: - locations_json.append(location.json) - - return locations_json + return objects_list_to_json(locations) @command("DELETE") @@ -133,10 +139,10 @@ def delete(name, discord_uuid=None, mc_uuid=None): @command("GET") def find_around(x_pos, z_pos, radius=200): - locations = Location.objects.get(x_coord__range=(x_pos - radius, x_pos + radius), - z_coord__range=(z_pos - radius, z_pos + radius), dimension='O') + locations = Location.objects.filter(x_coord__range=(x_pos - radius, x_pos + radius), + z_coord__range=(z_pos - radius, z_pos + radius)).all() - return locations + return objects_list_to_json(locations) @command("POST") @@ -147,7 +153,7 @@ def add_item(item_name, quantity, diamond_price, shop_name=None, discord_uuid=No item_listing = ItemListing.objects.create(shop=shop, amount=quantity, price=diamond_price, item_name=item_name) - return item_listing + return item_listing.json # TODO Re-implement selling shop search @@ -167,9 +173,9 @@ def selling(item_name): shop = Shop.objects.get(pk=shop_id['shop_id']).json item_query = ItemListing.objects.annotate(normalized_price=F('price') / F('amount')) \ - .filter(item_name__icontains=item_name, shop_id=shop_id) \ + .filter(item_name__icontains=item_name, shop_id=shop_id['shop_id']) \ .order_by('normalized_price') \ - .values("item_name", "amount", "price") + .values("item_name", "price", "amount") item_list = [] for item in item_query: @@ -196,17 +202,12 @@ def info(location_name): @command("GET") def tunnel(player_name): - tunnels_obj = Tunnel.objects.filter(location__owner__name__icontains=player_name).all() + tunnels = Tunnel.objects.filter(location__owner__name__icontains=player_name).all() - if len(tunnels_obj) == 0: + if len(tunnels) == 0: raise LocationLookUpError - tunnels_json = [] - - for t in tunnels_obj: - tunnels_json.append(t.json) - - return tunnels_json + return objects_list_to_json(tunnel) @command("POST") @@ -219,7 +220,7 @@ def edit_pos(x, z, loc_name, discord_uuid=None, mc_uuid=None): location.z_coord = z location.save() - return location + return location.json @command("POST") @@ -233,7 +234,7 @@ def edit_tunnel(tunnel_direction, tunnel_number, loc_name, discord_uuid=None, mc else: Tunnel.objects.create(tunnel_direction=tunnel_direction, tunnel_number=tunnel_number, location=location) - return location + return location.json @command("POST") @@ -244,7 +245,7 @@ def edit_name(new_name, loc_name, discord_uuid=None, mc_uuid=None): location.name = new_name location.save() - return location + return location.json @command("DELETE") @@ -255,20 +256,16 @@ def delete_item(item, shop_name, discord_uuid=None, mc_uuid=None): ItemListing.objects.filter(item_name__iexact=item, shop=shop).delete() - return shop + return shop.json @command("GET") def me(discord_uuid=None, mc_uuid=None): player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) - locations_obj = Location.objects.filter(owner=player).all() + locations = Location.objects.filter(owner=player).all() - if len(locations_obj) == 0: + if len(locations) == 0: raise PlayerNotFound - locations_json = [] - for location in locations_obj: - locations_json.append(location.json) - - return locations_json + return objects_list_to_json(locations) diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..137941f --- /dev/null +++ b/api/models.py @@ -0,0 +1 @@ +from django.db import models diff --git a/api/views.py b/api/views.py index 740b462..279a8b4 100644 --- a/api/views.py +++ b/api/views.py @@ -1,21 +1,33 @@ from django.views.generic import View from django.http import JsonResponse from django.conf import settings -import inspect import GeoffreyApp.api.commands as commands from GeoffreyApp.errors import * +from GeoffreyApp.models import APIToken + + +def check_token(request, **kwargs): + if "api" in request: + key = request["api"] + + if APIToken.objects.filter(key=key, **kwargs).exists(): + return True + + return False def run_command(request, command, req_type): command = command.lower() try: if command in commands.command_dict[req_type]: - response = commands.command_dict[req_type][command]["func"](**request.dict()) + params = request.dict() + params.pop("api") + response = commands.command_dict[req_type][command]["func"](**params) else: raise CommandNotFound except Exception as e: - response = {"error": e.__class__.__name__} + response = {"error": e.__class__.__name__, "error_message": str(e)} return JsonResponse(response, safe=False) @@ -23,18 +35,28 @@ def run_command(request, command, req_type): class CommandAPI(View): def get(self, request, command): get = request.GET - if command.lower() == "commands": - return JsonResponse(command.commands_dict) + + if check_token(get, commands_perm=True): + if command.lower() == "commands": + return JsonResponse(command.commands_dict) + else: + return run_command(get, command, "GET") else: - return run_command(get, command, "GET") + return JsonResponse({}) def post(self, request, command): post = request.POST - return run_command(post, command, "POST") + if check_token(post, commands_perm=True): + return run_command(post, command, "POST") + else: + return JsonResponse({}) def delete(self, request, command): delete = request.DELETE - return run_command(delete, command, "DELETE") + if check_token(delete, commands_perm=True): + return run_command(delete, command, "DELETE") + else: + return JsonResponse({}) class SettingsAPI(View): diff --git a/models.py b/models.py index 90cef67..2e39ea7 100644 --- a/models.py +++ b/models.py @@ -2,9 +2,23 @@ from django.db import models from django.conf import settings from sys import maxsize +from GeoffreyApp.util import create_token + # Create your models here. +class APIToken(models.Model): + key = models.CharField(default=create_token, max_length=25, unique=True) + name = models.CharField(max_length=50, blank=True) + commands_perm = models.BooleanField(default=False) + + def __str__(self): + if len(self.name): + return "{}: {}".format(self.name, self.key) + else: + return self.key + + class Player(models.Model): name = models.CharField(max_length=30, unique=True) mc_uuid = models.CharField(max_length=36, unique=True) @@ -38,6 +52,9 @@ class Location(models.Model): return {"Type": self.__class__.__name__, "Name": self.name, "x_coord": self.x_coord, "z_coord": self.z_coord, "dimension": self.dimension, "Owner": self.owner.json} + def __str__(self): + return self.name + class Shop(Location): def __str__(self): diff --git a/test/test_commands.py b/test/test_commands.py index 55a8e9f..d83f23a 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -22,37 +22,38 @@ class CommandsAPITestCase(TestCase): Tunnel.objects.all().delete() def populate(self): - base = Base.objects.create(owner=self.player, name="test", x_coord=0, z_coord=0) - shop = Shop.objects.create(owner=self.player, name="test shop", x_coord=0, z_coord=0) + base = Base.objects.create(owner=self.player, name="test", x_coord=0, z_coord=0, dimension="O") + shop = Shop.objects.create(owner=self.player, name="test shop", x_coord=500, z_coord=500, + dimension="O") item = ItemListing.objects.create(shop=shop, price=1, amount=5, item_name="sed") tunnel = Tunnel.objects.create(tunnel_number="42", tunnel_direction="S", location=base) def test_register(self): - command_dict["POST"]["register"]["func"](player_name="Vakky", discord_uuid="229423434256351233") + register(player_name="Vakky", discord_uuid="229423434256351233") count = Player.objects.filter(mc_uuid__iexact="7afbf6632bf049ef915f22e81b298d17").count() self.assertEqual(count, 1) def test_add_base(self): - command_dict["POST"]["add_base"]["func"](x_pos=0, z_pos=0, name=None, discord_uuid=DISCORD_UUID) + add_base(x_pos=0, z_pos=0, name=None, discord_uuid=DISCORD_UUID) base = Base.objects.filter(name__icontains=USERNAME).all().first() self.assertEqual(base.owner.name, "ZeroHD") def test_add_shop(self): - command_dict["POST"]["add_shop"]["func"](x_pos=0, z_pos=0, name=None, discord_uuid=DISCORD_UUID) + add_shop(x_pos=0, z_pos=0, name=None, discord_uuid=DISCORD_UUID) shop = Shop.objects.filter(name__icontains=USERNAME).all().first() - self.assertEqual(shop.owner.name, "zerohd") + self.assertEqual(shop.owner.name, "ZeroHD") def test_add_tunnel(self): base = Base.objects.create(owner=self.player, name="Test", x_coord=0, z_coord=0) - command_dict["POST"]["add_tunnel"]["func"](tunnel_direction="N", tunnel_number=500, discord_uuid=DISCORD_UUID) + add_tunnel(tunnel_direction="N", tunnel_number=500, discord_uuid=DISCORD_UUID) qbase = Base.objects.filter(tunnel_location__tunnel_direction="N").all().first() @@ -60,8 +61,7 @@ class CommandsAPITestCase(TestCase): def test_add_item(self): shop = Shop.objects.create(owner=self.player, name="Test", x_coord=0, z_coord=0) - command_dict["POST"]["add_item"]["func"](item_name="sed", quantity=5, diamond_price=5, - discord_uuid=DISCORD_UUID) + add_item(item_name="sed", quantity=5, diamond_price=5, discord_uuid=DISCORD_UUID) item = ItemListing.objects.filter(shop_id=shop.id).all().first() @@ -70,7 +70,7 @@ class CommandsAPITestCase(TestCase): def test_edit_post(self): shop = Shop.objects.create(owner=self.player, name="Test", x_coord=0, z_coord=0) - command_dict["POST"]["edit_pos"]["func"](x=500, z=500, loc_name="Test", discord_uuid=DISCORD_UUID) + edit_pos(x=500, z=500, loc_name="Test", discord_uuid=DISCORD_UUID) qshop = Shop.objects.filter(name__exact="Test").all().first() @@ -78,43 +78,49 @@ class CommandsAPITestCase(TestCase): def test_find_location(self): self.populate() - locations = command_dict["GET"]["find_location"]["func"](search="Test") + locations = find_location(search="Test") count = len(locations) self.assertEqual(count, 2) + def test_find_around(self): + self.populate() + locations = find_around(x_pos=0, z_pos=0, radius=50) + + self.assertEqual(len(locations), 1) + def test_selling(self): self.populate() - items = command_dict["GET"]["selling"]["func"](item_name="sED") + items = selling(item_name="sED") self.assertEqual(len(items), 1) def test_info(self): self.populate() - location = command_dict["GET"]["info"]["func"](location_name="test") + location = info(location_name="test") self.assertEqual(location["Name"], "test") def test_tunnel(self): self.populate() - tunnels = command_dict["GET"]["tunnel"]["func"](player_name=USERNAME) + tunnels = tunnel(player_name=USERNAME) self.assertEqual(len(tunnels), 1) def test_me(self): self.populate() - locations = command_dict["GET"]["me"]["func"](discord_uuid=DISCORD_UUID) + locations = me(discord_uuid=DISCORD_UUID) self.assertEqual(len(locations), 2) def test_delete(self): self.populate() - command_dict["DELETE"]["delete"]["func"](name="test", discord_uuid=DISCORD_UUID) + delete(name="test", discord_uuid=DISCORD_UUID) locations = Location.objects.filter(name__icontains="test").all() @@ -123,7 +129,7 @@ class CommandsAPITestCase(TestCase): def delete_item(self): self.populate() - command_dict["DELETE"]["delete_item"]["func"](item="sed", shop_name="test shop", discord_uuid=DISCORD_UUID) + delete_item(item="sed", shop_name="test shop", discord_uuid=DISCORD_UUID) items = ItemListing.objects.filter(item_name__iexact="sed").all() diff --git a/test/test_key.py b/test/test_key.py new file mode 100644 index 0000000..580e454 --- /dev/null +++ b/test/test_key.py @@ -0,0 +1,7 @@ +from django.test import TestCase +from GeoffreyApp.util import create_token + + +class TokenAPITestCase(TestCase): + def test_create_token(self): + print(create_token()) diff --git a/util.py b/util.py new file mode 100644 index 0000000..28d5288 --- /dev/null +++ b/util.py @@ -0,0 +1,12 @@ +import string +import random + + +def create_token(length=25): + token = '' + + for i in range(0, length): + d = random.choice(string.digits + string.ascii_lowercase) + token += d + + return token diff --git a/views.py b/views.py index 5537359..cfc1629 100644 --- a/views.py +++ b/views.py @@ -45,6 +45,7 @@ class ShopList(generic.ListView): class BaseList(generic.ListView): model = Base + class ItemListingList(generic.ListView): model = ItemListing