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 306d3be..02c78a6 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/users/migrations/0002_alter_profile_id.py b/users/migrations/0002_alter_profile_id.py new file mode 100644 index 0000000..231b056 --- /dev/null +++ b/users/migrations/0002_alter_profile_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2026-02-19 10:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ]