diff --git a/.gitignore b/.gitignore index 5594918..2c7502f 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,10 @@ 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/ + +# database +db.sqlite3 diff --git a/BH/settings.py b/BH/settings.py index ffaa220..9179c00 100644 --- a/BH/settings.py +++ b/BH/settings.py @@ -25,11 +25,9 @@ DEFAULT_AUTO_FIELD='django.db.models.AutoField' # Load the Env load_dotenv() -# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('SECRET_KEY') -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = os.environ.get('DEBUG', 'False') == 'True' ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'beyond-heroes.com', 'www.beyond-heroes.com'] @@ -40,17 +38,25 @@ INSTALLED_APPS = [ 'crispy_forms', 'crispy_bootstrap4', 'blog.apps.BlogConfig', + 'users', + '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', ] 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', @@ -58,6 +64,8 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +CORS_ORIGIN_WHITELIST = ( 'http://localhost:3000', 'http://www.beyond-heroes.com') + ROOT_URLCONF = 'BH.urls' TEMPLATES = [ @@ -112,6 +120,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/ @@ -130,11 +149,15 @@ 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/' CRISPY_TEMPLATE_PACK = 'bootstrap4' -LOGIN_REDIRECT_URL = 'News' +LOGIN_REDIRECT_URL = 'Home' LOGIN_URL = 'Login' + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # prints to console +SITE_ID = 1 # for django-allauth diff --git a/BH/urls.py b/BH/urls.py index ce5310a..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/ @@ -21,6 +21,11 @@ from django.urls import path, include urlpatterns = [ path('', include('blog.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/Dockerfile b/Dockerfile index 3e9c5a0..71405da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,5 +30,5 @@ EXPOSE 3030 # Define environment variable for Gunicorn ENV GUNICORN_CMD_ARGS="--bind 0.0.0.0:3030" -# Run Gunicorn server with your Django application -CMD ["gunicorn", "BH.wsgi:application"] +# Collect static files and run Gunicorn +CMD ["sh", "-c", "python manage.py collectstatic --noinput && gunicorn BH.wsgi:application"] 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/filters.py b/api/filters.py new file mode 100644 index 0000000..b250d48 --- /dev/null +++ b/api/filters.py @@ -0,0 +1,8 @@ +from rest_framework.filters import BaseFilterBackend +from users.models import * + +class UserDataFilterBackend(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + queryset = queryset.filter(user__id=request.user.id) + + return queryset 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..9ef600d --- /dev/null +++ b/api/permissions.py @@ -0,0 +1,24 @@ +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 + + +class IsSuperUserOrAuthReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + if request.method in permissions.SAFE_METHODS: + return request.user != None + return request.user.is_superuser + + +class IsSuperUser(permissions.BasePermission): + def has_permission(self, request, view): + return request.user.is_superuser diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..1f0cca8 --- /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', '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..8a07d97 --- /dev/null +++ b/api/urls.py @@ -0,0 +1,15 @@ +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()), + path('user_data/', UserDataView.as_view()), + path('user_data//', UserDatumView.as_view()), +] diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..4886b3c --- /dev/null +++ b/api/views.py @@ -0,0 +1,67 @@ +from rest_framework import generics, permissions +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from .models import * +from .filters import * +from .serializers import * +from .permissions import * +from users.models import * +from users.serializers 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 + + +class UserDatumView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = (IsSuperUser,) + queryset = UserData.objects.all() + serializer_class = UserDataSerializer + # filter_backends = [UserDataFilterBackend] + +class UserDataView(generics.ListCreateAPIView): + permission_classes = (IsSuperUserOrAuthReadOnly,) + queryset = UserData.objects.all() + serializer_class = UserDataSerializer + filter_backends = [UserDataFilterBackend] \ No newline at end of file diff --git a/blog/templatetags/__pycache__/markdown_extras.cpython-310.pyc b/blog/templatetags/__pycache__/markdown_extras.cpython-310.pyc deleted file mode 100644 index 9c90861..0000000 Binary files a/blog/templatetags/__pycache__/markdown_extras.cpython-310.pyc and /dev/null differ diff --git a/requirements.txt b/requirements.txt index d4a27d4..f13c9fc 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/server.bat b/server.bat new file mode 100644 index 0000000..0095694 --- /dev/null +++ b/server.bat @@ -0,0 +1,5 @@ +@ echo off +echo The devlopment server will start in a few seconds... +python manage.py makemigrations +python manage.py migrate +python manage.py runserver 3000 diff --git a/templates/base.html b/templates/base.html index 981d92d..2ce3abc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,51 +3,571 @@ - Beyond Heroes + Beyond Heroes - Official Website + + + + + + - + + + -