From 319d4374e8d55bed1488a5324c7034eda3644944 Mon Sep 17 00:00:00 2001 From: Rishit-Pandit Date: Tue, 24 Feb 2026 20:09:41 +0530 Subject: [PATCH] Added API API has auth --- .gitignore | 5 +- BH/settings.py | 40 +++++++++- BH/urls.py | 8 +- api/__init__.py | 0 api/admin.py | 9 +++ api/apps.py | 5 ++ api/migrations/0001_initial.py | 71 ++++++++++++++++++ api/migrations/__init__.py | 0 api/models.py | 70 +++++++++++++++++ api/permissions.py | 12 +++ api/serializers.py | 25 ++++++ api/templatetags/api_markdown_extras.py | 12 +++ api/tests.py | 3 + api/urls.py | 13 ++++ api/views.py | 48 ++++++++++++ .../0002_alter_blog_id_alter_post_id.py | 23 ++++++ db.sqlite3 | Bin 155648 -> 294912 bytes users/migrations/0002_alter_profile_id.py | 18 +++++ 18 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 api/__init__.py create mode 100644 api/admin.py create mode 100644 api/apps.py create mode 100644 api/migrations/0001_initial.py create mode 100644 api/migrations/__init__.py create mode 100644 api/models.py create mode 100644 api/permissions.py create mode 100644 api/serializers.py create mode 100644 api/templatetags/api_markdown_extras.py create mode 100644 api/tests.py create mode 100644 api/urls.py create mode 100644 api/views.py create mode 100644 blog/migrations/0002_alter_blog_id_alter_post_id.py create mode 100644 users/migrations/0002_alter_profile_id.py diff --git a/.gitignore b/.gitignore index 56bf8f4..cffd79f 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -.idea/ \ No newline at end of file +.idea/ + +# static +staticfiles/ diff --git a/BH/settings.py b/BH/settings.py index 6370527..83ff08e 100644 --- a/BH/settings.py +++ b/BH/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os +from dotenv import load_dotenv # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -23,9 +24,9 @@ TEMPLATES_DIR = os.path.join(BASE_DIR + '/templates') SECRET_KEY = '!2g)+m+_h9fq9%il5+t5#qnj^9502or6$=2!$==v=i2*c#7q*m' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'beyond-heroes.com', 'www.beyond-heroes.com'] # Application definition @@ -33,26 +34,43 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ 'crispy_forms', 'crispy_bootstrap4', + 'blog.apps.BlogConfig', 'users.apps.UsersConfig', + 'api.apps.APIConfig', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', # for django-allauth + + 'corsheaders', + 'dj_rest_auth', + 'rest_framework', + 'rest_framework.authtoken', + # 'allauth', + # 'allauth.account', + # 'allauth.socialaccount', + # 'dj_rest_auth.registration', # for api side user registration ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', # cors-headers 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + # 'allauth.account.middleware.AccountMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +CORS_ORIGIN_WHITELIST = ( 'http://localhost:3000', 'http://www.beyond-heroes.com') + ROOT_URLCONF = 'BH.urls' TEMPLATES = [ @@ -103,6 +121,17 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + # 'rest_framework.permissions.AllowAny', + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + # 'rest_framework.authentication.SessionAuthentication', # causes CSRF Token conflicts in API + 'rest_framework.authentication.TokenAuthentication', + ) +} + # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ @@ -121,6 +150,8 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ +# remember to `python manage.py collectstatic` +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATIC_URL = '/static/' MEDIA_ROOT = os.path.join(BASE_DIR + '/media') MEDIA_URL = '/media/' @@ -128,3 +159,8 @@ MEDIA_URL = '/media/' CRISPY_TEMPLATE_PACK = 'bootstrap4' LOGIN_REDIRECT_URL = 'Home' LOGIN_URL = 'Login' + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # prints to console +# SITE_ID = 1 # for django-allauth + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/BH/urls.py b/BH/urls.py index 13801d3..888ad1c 100644 --- a/BH/urls.py +++ b/BH/urls.py @@ -1,4 +1,4 @@ -"""TechBlog URL Configuration +"""BH URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.0/topics/http/urls/ @@ -20,8 +20,12 @@ from django.urls import path, include urlpatterns = [ path('', include('blog.urls')), - path('users/', include('users.urls')), path('admin/', admin.site.urls), + path('api/v1/', include('api.urls')), + path('users/', include('users.urls')), + path('api-auth/', include('rest_framework.urls')), + path('api/v1/dj-rest-auth/', include('dj_rest_auth.urls')), + # path('api/v1/dj-rest-auth/register/', include('dj_rest_auth.registration.urls')), ] if settings.DEBUG: diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..1da1144 --- /dev/null +++ b/api/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import * + + +# Register your models here. +admin.site.register(Province) +admin.site.register(AssaultTroop) +admin.site.register(Player) +admin.site.register(Server) diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..93127e4 --- /dev/null +++ b/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class APIConfig(AppConfig): + name = 'api' diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..674c694 --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,71 @@ +# Generated by Django 5.2.9 on 2026-02-19 10:09 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Player', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=50)), + ('faction', models.IntegerField()), + ('server', models.CharField(max_length=20)), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Province', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('faction', models.IntegerField()), + ('map', models.CharField(max_length=255)), + ('mov_speed', models.IntegerField()), + ('ats', models.JSONField(null=True)), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Server', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('players', models.IntegerField()), + ('capacity', models.IntegerField()), + ('region', models.CharField(max_length=3)), + ('address', models.CharField(max_length=20)), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='AssaultTroop', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('faction', models.IntegerField()), + ('type', models.IntegerField()), + ('province', models.IntegerField()), + ('orders', models.JSONField(null=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ats', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['id'], + }, + ), + ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..9fae33b --- /dev/null +++ b/api/models.py @@ -0,0 +1,70 @@ +from django.db import models +from django.contrib.auth.models import User +from django.db.models.fields import CharField, IntegerField + + +# Create your models here. +class Province(models.Model): + id = models.IntegerField(primary_key = True) + name = models.CharField(max_length = 100, blank = False) + faction = models.IntegerField() # 0 - Neutral, 1 - Allies, 2 - Axis + map = models.CharField(max_length=255) + mov_speed = models.IntegerField() + ats = models.JSONField(null=True) + + class Meta: + ordering = ['id'] + + def __str__(self): + return f"{self.name} - {self.faction}" + + +class AssaultTroop(models.Model): + id = models.IntegerField(primary_key = True) + name = models.CharField(max_length = 100, blank = False) + faction = models.IntegerField() # 0 - Neutral, 1 - Allies, 2 - Axis + type = models.IntegerField() + province = models.IntegerField() # Province ID (-1 for not deployed) + orders = models.JSONField(null=True) + owner = models.ForeignKey( + 'auth.User', + related_name='ats', + on_delete=models.CASCADE + ) + + class Meta: + ordering = ['id'] + + def __str__(self): + return f"{self.name} - {self.province},{self.faction}" + + + +class Player(models.Model): + id = models.IntegerField(primary_key = True) + name = models.CharField(max_length = 50, blank = False) + faction = models.IntegerField() + server = CharField(max_length = 20, blank = False) + + class Meta: + ordering = ['id'] + + def __str__(self): + return self.name + + + +class Server(models.Model): + id = IntegerField(primary_key = True) + players = IntegerField() # total current players + capacity = IntegerField() # max player capacity + region = CharField(max_length = 3, blank = False) # 3 letter abb. for region + address = CharField(max_length = 20, blank = False) + + class Meta: + ordering = ['id'] + + def __str__(self): + return f"{self.address} - {self.region}" + + diff --git a/api/permissions.py b/api/permissions.py new file mode 100644 index 0000000..26cdde2 --- /dev/null +++ b/api/permissions.py @@ -0,0 +1,12 @@ +from rest_framework import permissions + +class IsSuperUserOrReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + if request.method in permissions.SAFE_METHODS: + return True + return request.user.is_superuser + + +class IsStaff(permissions.BasePermission): + def has_permission(self, request, view): + return request.user.is_staff diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..400fa05 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,25 @@ +from rest_framework import serializers +from .models import * + +class ProvinceSerializer(serializers.ModelSerializer): + class Meta: + model = Province + fields = ['id', 'name', 'faction', 'map', 'mov_speed', 'ats'] + + +class AssaultTroopSerializer(serializers.ModelSerializer): + class Meta: + model = AssaultTroop + fields = ['id', 'name', 'faction', 'type', 'province', 'deployed', 'orders', 'owner'] + + +class PlayerSerializer(serializers.ModelSerializer): + class Meta: + model = Player + fields = ['id', 'name', 'faction', 'server'] + + +class ServerSerializer(serializers.ModelSerializer): + class Meta: + model = Server + fields = ['id', 'players', 'capacity', 'region', 'address'] diff --git a/api/templatetags/api_markdown_extras.py b/api/templatetags/api_markdown_extras.py new file mode 100644 index 0000000..23b1077 --- /dev/null +++ b/api/templatetags/api_markdown_extras.py @@ -0,0 +1,12 @@ +from django import template +from django.template.defaultfilters import stringfilter + +import markdown as md + + +register = template.Library() + +@register.filter() +@stringfilter +def markdown(value): + return md.markdown(value, extensions=['markdown.extensions.fenced_code']) diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..327296f --- /dev/null +++ b/api/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from .views import * + +urlpatterns = [ + path('provinces/', ProvincesView.as_view()), + path('provinces//', ProvinceView.as_view()), + path('assault_troops/', AssaultTroopsView.as_view()), + path('assault_troops//', AssaultTroopView.as_view()), + path('players/', PlayersView.as_view()), + path('players//', PlayerView.as_view()), + path('servers/', ServersView.as_view()), + path('servers//', ServerView.as_view()), +] diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..30ac0d5 --- /dev/null +++ b/api/views.py @@ -0,0 +1,48 @@ +from rest_framework import generics, permissions +from .models import * +from .serializers import * +from .permissions import * + + +class ProvincesView(generics.ListCreateAPIView): + permission_classes = (IsSuperUserOrReadOnly,) + queryset = Province.objects.all() + serializer_class = ProvinceSerializer + +class ProvinceView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = (IsSuperUserOrReadOnly,) + queryset = Province.objects.all() + serializer_class = ProvinceSerializer + + +class AssaultTroopsView(generics.ListCreateAPIView): + permission_classes = (IsSuperUserOrReadOnly,) + queryset = AssaultTroop.objects.all() + serializer_class = AssaultTroopSerializer + +class AssaultTroopView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = (IsSuperUserOrReadOnly,) + queryset = AssaultTroop.objects.all() + serializer_class = AssaultTroopSerializer + + +class PlayersView(generics.ListCreateAPIView): + permission_classes = (IsStaff,) # Only Staff can see player info, i.e. authorized servers + queryset = Player.objects.all() + serializer_class = PlayerSerializer + +class PlayerView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = (IsStaff) + queryset = Player.objects.all() + serializer_class = PlayerSerializer + + +class ServersView(generics.ListCreateAPIView): + permission_classes = (IsSuperUserOrReadOnly,) + queryset = Server.objects.all() + serializer_class = ServerSerializer + +class ServerView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = (IsSuperUserOrReadOnly,) + queryset = Server.objects.all() + serializer_class = ServerSerializer diff --git a/blog/migrations/0002_alter_blog_id_alter_post_id.py b/blog/migrations/0002_alter_blog_id_alter_post_id.py new file mode 100644 index 0000000..2bdeec9 --- /dev/null +++ b/blog/migrations/0002_alter_blog_id_alter_post_id.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.9 on 2026-02-19 10:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='blog', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='post', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/db.sqlite3 b/db.sqlite3 index 306d3be008db700a33950c1c53c580a76d77d4a1..02c78a60e66d4c67e5be3a6d8e697cc1df91608c 100644 GIT binary patch literal 294912 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