diff --git a/trojstenid/settings.py b/trojstenid/settings.py index edfb670..0f9a337 100644 --- a/trojstenid/settings.py +++ b/trojstenid/settings.py @@ -142,6 +142,7 @@ "reset_password_from_key": "trojstenid.users.forms.allauth.OurResetPasswordKeyForm", "set_password": "trojstenid.users.forms.allauth.OurSetPasswordForm", } +ACCOUNT_USERNAME_VALIDATORS = "trojstenid.users.models.username_validators" SOCIALACCOUNT_PROVIDERS = { "openid_connect": { diff --git a/trojstenid/users/management/__init__.py b/trojstenid/users/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trojstenid/users/management/commands/__init__.py b/trojstenid/users/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trojstenid/users/management/commands/normalize_usernames.py b/trojstenid/users/management/commands/normalize_usernames.py new file mode 100644 index 0000000..a4f9c50 --- /dev/null +++ b/trojstenid/users/management/commands/normalize_usernames.py @@ -0,0 +1,53 @@ +import re +import unicodedata + +from django.core.management.base import BaseCommand + +from trojstenid.users.models import User, UsernameValidator + + +def normalize_username(username: str) -> str: + normalized = ( + unicodedata.normalize("NFKD", username).encode("ASCII", "ignore").decode() + ) + + if "@" in normalized: + parts = normalized.split("@", 1) + if parts[0]: + normalized = parts[0] + else: + normalized = parts[1] + + normalized = re.sub(r"[^\w.-]", "", normalized) + + return normalized + + +class Command(BaseCommand): + help = "Validate and normalize usernames" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Run without making changes to the database", + ) + + def handle(self, *args, **options): + dry_run = options["dry_run"] + validator = UsernameValidator() + + users = User.objects.all() + + for user in users: + try: + validator(user.username) + except Exception: + new_username = normalize_username(user.username) + + if new_username != user.username: + self.stdout.write(f"{user.username} ({user.id}) -> {new_username}") + + if not dry_run: + user.username = new_username + user.save() diff --git a/trojstenid/users/models.py b/trojstenid/users/models.py index ae4f8f7..c205524 100644 --- a/trojstenid/users/models.py +++ b/trojstenid/users/models.py @@ -1,13 +1,16 @@ +import re from datetime import date from pathlib import PurePath from typing import TYPE_CHECKING from django.contrib.auth.models import AbstractUser, Group +from django.core import validators from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models import Q from django.urls import reverse from django.utils import timezone +from django.utils.deconstruct import deconstructible from oauth2_provider.models import AbstractApplication from ulid import ULID @@ -40,6 +43,16 @@ def get_db_converters(self, connection): return [] +@deconstructible() +class UsernameValidator(validators.RegexValidator): + regex = r"^[\w.-]+\Z" + message = "Používateľské meno môže obsahovať len písmená, čísla a znaky ./-/_" + flags = re.ASCII + + +username_validators = [UsernameValidator()] + + class User(AbstractUser): id: int