diff --git a/app/settings/settings.py b/app/settings/settings.py index 3e2d32a..7942019 100644 --- a/app/settings/settings.py +++ b/app/settings/settings.py @@ -31,7 +31,7 @@ def load_insecure_key(): SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") or load_insecure_key() -ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") or "*" +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(" ") # Application definition diff --git a/app/users/admin.py b/app/users/admin.py index 3e2fc6c..a19ba80 100644 --- a/app/users/admin.py +++ b/app/users/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from .models import Player, Log +from .models import Player, Log, InviteCode admin.site.register(Log) -admin.site.register(Player) \ No newline at end of file +admin.site.register(Player) +admin.site.register(InviteCode) \ No newline at end of file diff --git a/app/users/forms.py b/app/users/forms.py index 65b5471..c0f55a0 100644 --- a/app/users/forms.py +++ b/app/users/forms.py @@ -8,7 +8,7 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit from users.models import Player -from users.validators import usernameValidator, username_change_validator +from users.validators import username_validator, username_change_validator, invitecode_validator from bloonsa_game.models import Config as BloonsaConfig class UserRegisterForm(UserCreationForm): @@ -23,12 +23,20 @@ class UserRegisterForm(UserCreationForm): self.helper.form_action = '/users/register' self.helper.add_input(Submit('submit', 'Create')) + invite_code = forms.CharField(max_length=64, + label="invite_code", + empty_value="code", + # help_text="", + required=True, + validators=[invitecode_validator,]) + username = forms.CharField(min_length=3, max_length=16, label="Username", required=True, - validators=[usernameValidator,], - help_text=_("3-16 chars, alphanumeric and _- pls")) + validators=[username_validator,], + #help_text=_("3-16 chars, alphanumeric and _- pls") + ) password1 = forms.CharField( label="Password", diff --git a/app/users/migrations/0026_invitecode_alter_log_action_player_invite_code.py b/app/users/migrations/0026_invitecode_alter_log_action_player_invite_code.py new file mode 100644 index 0000000..a7bbe2d --- /dev/null +++ b/app/users/migrations/0026_invitecode_alter_log_action_player_invite_code.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.6 on 2025-02-20 21:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0025_alter_player_avatar'), + ] + + operations = [ + migrations.CreateModel( + name='InviteCode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField(max_length=64)), + ('code', models.TextField(max_length=64)), + ('active', models.BooleanField(default=True)), + ], + ), + migrations.AlterField( + model_name='log', + name='action', + field=models.IntegerField(choices=[(0, 'Logged in'), (1, 'Registered'), (2, 'Logged out'), (3, 'Edited account settings'), (100, 'Loaded a level via ingame ID box'), (101, 'Loaded a level via URL'), (102, 'Loaded a random level'), (103, 'Submitted a score on level_id'), (104, 'Rated a level')]), + ), + migrations.AddField( + model_name='player', + name='invite_code', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='users.invitecode'), + ), + ] diff --git a/app/users/models.py b/app/users/models.py index 2f32f25..2d5d4dd 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -4,10 +4,18 @@ from django.contrib.auth.models import User from django_resized import ResizedImageField +class InviteCode(models.Model): + title = models.TextField(max_length=64) + code = models.TextField(max_length=64) + active = models.BooleanField(default=True) + + def __str__(self): + return f"{self.title} | code: {self.code}" + class Player(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="player") # Profile - bio = models.TextField(max_length=128, null=True) + bio = models.TextField(max_length=128, null=True, blank=True) avatar = ResizedImageField(default="defaults/avatars.jpg", size=[256, 256], upload_to="avatars", @@ -21,6 +29,7 @@ class Player(models.Model): creation_date = models.DateTimeField(default=timezone.now) latest_activity = models.DateTimeField(default=timezone.now) suspected_cheater = models.BooleanField(default=False) # This should be set when tripping the anti-cheat + invite_code = models.ForeignKey(InviteCode, null=True, blank=True, on_delete=models.CASCADE) # States suspended = models.BooleanField(default=False) # This is a shadow-ban, stats will still save but not all will show up on leaderboards banned = models.BooleanField(default=False) # Account gets logged out upon logging in @@ -50,6 +59,8 @@ class Player(models.Model): return f"{states}{self.user} - {self.latest_ip}".strip(" ") + + class Log(models.Model): class Actions(models.IntegerChoices): login = 0, "Logged in" @@ -68,4 +79,5 @@ class Log(models.Model): note = models.TextField(null=True) def __str__(self): - return f"{self.player.user.username} - {self.get_action_display()} <{self.note or ''}>" \ No newline at end of file + return f"{self.player.user.username} - {self.get_action_display()} <{self.note or ''}>" + diff --git a/app/users/util.py b/app/users/util.py index 6d4f36c..27b1f3a 100644 --- a/app/users/util.py +++ b/app/users/util.py @@ -25,6 +25,9 @@ class BloonsaUtil: if not request.user.is_authenticated: return if hasattr(request.user, "player"): + if not hasattr(request.user.player, "bloonsa_config"): + bloonsa_config = Config(player=request.user.player) + bloonsa_config.save() return request.user.player ip = self.get_ip(request=request) player = Player(user=request.user, diff --git a/app/users/validators.py b/app/users/validators.py index a9a8492..0cbac16 100644 --- a/app/users/validators.py +++ b/app/users/validators.py @@ -3,9 +3,10 @@ import string from django.contrib.auth.models import User from django.core.exceptions import ValidationError -from users.models import Player +from users.models import Player, InviteCode -def usernameValidator(username): + +def username_validator(username): if User.objects.filter(username__iexact=username).first() is not None: raise ValidationError("Sorry, this username is already in use") @@ -23,3 +24,10 @@ def username_change_validator(username): charset = set(string.ascii_letters + string.digits + "_-") if not all(x in charset for x in username): raise ValidationError("Username may only contain normal letters, numbers and _-") + +def invitecode_validator(invitecode): + if len(invitecode) > 64: + raise ValidationError("Invite code too long") + + if not InviteCode.objects.filter(code=invitecode).exists(): + raise ValidationError("Invite code doesn't exist") \ No newline at end of file diff --git a/app/users/views.py b/app/users/views.py index e40fed9..22ce437 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -5,7 +5,7 @@ from django.shortcuts import render, redirect from django.views.generic import TemplateView from users.forms import UserRegisterForm, UserLoginForm, PlayerUpdateForm, UserUpdateForm, BloonsaConfigUpdateForm -from users.models import Player +from users.models import Player, InviteCode from users.util import bloonsa_util, actions @@ -37,9 +37,14 @@ class RegisterView(TemplateView): form = UserRegisterForm(request.POST) if not form.is_valid(): return render(request=request, template_name="users/register.html", context={"form": form}) + + invite_code = InviteCode.objects.get(code=form.cleaned_data["invite_code"]) user = form.save() - player = bloonsa_util.init_player(request=request) login(request=request, user=user) + player = bloonsa_util.init_player(request=request) + player.invite_code = invite_code + player.save() + bloonsa_util.log(player=player, action=actions.login) return redirect("bloonsa_game:game")