mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 11:03:04 +00:00 
			
		
		
		
	
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
db.sqlite3
 | 
			
		||||
*.sqlite3
 | 
			
		||||
*.log
 | 
			
		||||
*.pyc
 | 
			
		||||
*.mo
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								antispam/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								antispam/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										10
									
								
								antispam/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								antispam/admin.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from antispam.models import ToxicDomain
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(ToxicDomain)
 | 
			
		||||
class ToxicDomainAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ("domain", "is_externally_managed", "created")
 | 
			
		||||
    search_fields = ("domain", "is_externally_managed", "created")
 | 
			
		||||
    list_filter = ("is_externally_managed",)
 | 
			
		||||
							
								
								
									
										7
									
								
								antispam/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								antispam/apps.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AntispamConfig(AppConfig):
 | 
			
		||||
    default_auto_field = "django.db.models.BigAutoField"
 | 
			
		||||
    verbose_name = "antispam"
 | 
			
		||||
    name = "antispam"
 | 
			
		||||
							
								
								
									
										18
									
								
								antispam/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								antispam/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.core.validators import EmailValidator
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from antispam.models import ToxicDomain
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AntiSpamEmailField(forms.EmailField):
 | 
			
		||||
    """An email field that email addresses with a known toxic domain."""
 | 
			
		||||
 | 
			
		||||
    def run_validators(self, value: str):
 | 
			
		||||
        super().run_validators(value)
 | 
			
		||||
        # Domain part should exist since email validation is guaranteed to run first
 | 
			
		||||
        domain = re.search(EmailValidator.domain_regex, value)
 | 
			
		||||
        if ToxicDomain.objects.filter(domain=domain[0]).exists():
 | 
			
		||||
            raise forms.ValidationError(_("Email domain is not allowed."))
 | 
			
		||||
							
								
								
									
										0
									
								
								antispam/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								antispam/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										69
									
								
								antispam/management/commands/update_spam_database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								antispam/management/commands/update_spam_database.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
import requests
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management import BaseCommand
 | 
			
		||||
from django.db.models import Max
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
from antispam.models import ToxicDomain
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    """Update blocked ips/mails database"""
 | 
			
		||||
 | 
			
		||||
    help = "Update blocked ips/mails database"
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--force", action="store_true", help="Force re-creation even if up to date"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _should_update(self, *, force: bool = False) -> bool:
 | 
			
		||||
        if force:
 | 
			
		||||
            return True
 | 
			
		||||
        oldest = ToxicDomain.objects.filter(is_externally_managed=True).aggregate(
 | 
			
		||||
            res=Max("created")
 | 
			
		||||
        )["res"]
 | 
			
		||||
        return not (oldest and timezone.now() < (oldest + timezone.timedelta(days=1)))
 | 
			
		||||
 | 
			
		||||
    def _download_domains(self, providers: list[str]) -> set[str]:
 | 
			
		||||
        domains = set()
 | 
			
		||||
        for provider in providers:
 | 
			
		||||
            res = requests.get(provider)
 | 
			
		||||
            if not res.ok:
 | 
			
		||||
                self.stderr.write(
 | 
			
		||||
                    f"Source {provider} responded with code {res.status_code}"
 | 
			
		||||
                )
 | 
			
		||||
                continue
 | 
			
		||||
            domains |= set(res.content.decode().splitlines())
 | 
			
		||||
        return domains
 | 
			
		||||
 | 
			
		||||
    def _update_domains(self, domains: set[str]):
 | 
			
		||||
        # Cleanup database
 | 
			
		||||
        ToxicDomain.objects.filter(is_externally_managed=True).delete()
 | 
			
		||||
 | 
			
		||||
        # Create database
 | 
			
		||||
        ToxicDomain.objects.bulk_create(
 | 
			
		||||
            [
 | 
			
		||||
                ToxicDomain(domain=domain, is_externally_managed=True)
 | 
			
		||||
                for domain in domains
 | 
			
		||||
            ],
 | 
			
		||||
            ignore_conflicts=True,
 | 
			
		||||
        )
 | 
			
		||||
        self.stdout.write("Domain database updated")
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        if not self._should_update(force=options["force"]):
 | 
			
		||||
            self.stdout.write("Domain database is up to date")
 | 
			
		||||
            return
 | 
			
		||||
        self.stdout.write("Updating domain database")
 | 
			
		||||
 | 
			
		||||
        domains = self._download_domains(settings.TOXIC_DOMAINS_PROVIDERS)
 | 
			
		||||
 | 
			
		||||
        if not domains:
 | 
			
		||||
            self.stderr.write(
 | 
			
		||||
                "No domains could be fetched from settings.TOXIC_DOMAINS_PROVIDERS. "
 | 
			
		||||
                "Please, have a look at your settings."
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self._update_domains(domains)
 | 
			
		||||
							
								
								
									
										35
									
								
								antispam/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								antispam/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
# Generated by Django 4.2.14 on 2024-08-03 23:05
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = []
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="ToxicDomain",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "domain",
 | 
			
		||||
                    models.URLField(
 | 
			
		||||
                        max_length=253,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="domain",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "is_externally_managed",
 | 
			
		||||
                    models.BooleanField(
 | 
			
		||||
                        default=False,
 | 
			
		||||
                        help_text="True if kept up-to-date using external toxic domain providers, else False",
 | 
			
		||||
                        verbose_name="is externally managed",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								antispam/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								antispam/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										19
									
								
								antispam/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								antispam/models.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ToxicDomain(models.Model):
 | 
			
		||||
    """Domain marked as spam in public databases"""
 | 
			
		||||
 | 
			
		||||
    domain = models.URLField(_("domain"), max_length=253, primary_key=True)
 | 
			
		||||
    created = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    is_externally_managed = models.BooleanField(
 | 
			
		||||
        _("is externally managed"),
 | 
			
		||||
        default=False,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "True if kept up-to-date using external toxic domain providers, else False"
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.domain
 | 
			
		||||
@@ -24,8 +24,10 @@ from django.core.mail import EmailMessage
 | 
			
		||||
from django.test import Client, TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from pytest_django.asserts import assertInHTML, assertRedirects
 | 
			
		||||
 | 
			
		||||
from antispam.models import ToxicDomain
 | 
			
		||||
from club.models import Membership
 | 
			
		||||
from core.markdown import markdown
 | 
			
		||||
from core.models import AnonymousUser, Group, Page, User
 | 
			
		||||
@@ -48,6 +50,10 @@ class TestUserRegistration:
 | 
			
		||||
            "captcha_1": "PASSED",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @pytest.fixture()
 | 
			
		||||
    def scam_domains(self):
 | 
			
		||||
        return [baker.make(ToxicDomain, domain="scammer.spam")]
 | 
			
		||||
 | 
			
		||||
    def test_register_user_form_ok(self, client, valid_payload):
 | 
			
		||||
        """Should register a user correctly."""
 | 
			
		||||
        assert not User.objects.filter(email=valid_payload["email"]).exists()
 | 
			
		||||
@@ -64,14 +70,25 @@ class TestUserRegistration:
 | 
			
		||||
                {"password2": "not the same as password1"},
 | 
			
		||||
                "Les deux mots de passe ne correspondent pas.",
 | 
			
		||||
            ),
 | 
			
		||||
            ({"email": "not-an-email"}, "Saisissez une adresse de courriel valide."),
 | 
			
		||||
            (
 | 
			
		||||
                {"email": "not-an-email"},
 | 
			
		||||
                "Saisissez une adresse de courriel valide.",
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                {"email": "not\\an@email.com"},
 | 
			
		||||
                "Saisissez une adresse de courriel valide.",
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                {"email": "legit@scammer.spam"},
 | 
			
		||||
                "Le domaine de l'addresse e-mail n'est pas autorisé.",
 | 
			
		||||
            ),
 | 
			
		||||
            ({"first_name": ""}, "Ce champ est obligatoire."),
 | 
			
		||||
            ({"last_name": ""}, "Ce champ est obligatoire."),
 | 
			
		||||
            ({"captcha_1": "WRONG_CAPTCHA"}, "CAPTCHA invalide"),
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
    def test_register_user_form_fail(
 | 
			
		||||
        self, client, valid_payload, payload_edit, expected_error
 | 
			
		||||
        self, client, scam_domains, valid_payload, payload_edit, expected_error
 | 
			
		||||
    ):
 | 
			
		||||
        """Should not register a user correctly."""
 | 
			
		||||
        payload = valid_payload | payload_edit
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
# details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License along with
 | 
			
		||||
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
 | 
			
		||||
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 | 
			
		||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
 | 
			
		||||
#
 | 
			
		||||
#
 | 
			
		||||
@@ -45,6 +45,7 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
 | 
			
		||||
from PIL import Image
 | 
			
		||||
 | 
			
		||||
from antispam.forms import AntiSpamEmailField
 | 
			
		||||
from core.models import Gift, Page, SithFile, User
 | 
			
		||||
from core.utils import resize_image
 | 
			
		||||
 | 
			
		||||
@@ -194,6 +195,9 @@ class RegisteringForm(UserCreationForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = User
 | 
			
		||||
        fields = ("first_name", "last_name", "email")
 | 
			
		||||
        field_classes = {
 | 
			
		||||
            "email": AntiSpamEmailField,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserProfileForm(forms.ModelForm):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								docs/reference/antispam/forms.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								docs/reference/antispam/forms.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
::: antispam.forms
 | 
			
		||||
							
								
								
									
										1
									
								
								docs/reference/antispam/models.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								docs/reference/antispam/models.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
::: antispam.models
 | 
			
		||||
							
								
								
									
										10
									
								
								docs/tutorial/install_advanced.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								docs/tutorial/install_advanced.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
## Mettre à jour la base de données antispam
 | 
			
		||||
 | 
			
		||||
L'anti spam nécessite d'être à jour par rapport à des bases de données externe.
 | 
			
		||||
Il existe une commande pour ça qu'il faut lancer régulièrement.
 | 
			
		||||
Lors de la mise en production, il est judicieux de configurer
 | 
			
		||||
un cron pour la mettre à jour au moins une fois par jour.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py update_spam_database
 | 
			
		||||
```
 | 
			
		||||
@@ -64,17 +64,19 @@ sith3/
 | 
			
		||||
│   └── ...
 | 
			
		||||
├── trombi/ (22)
 | 
			
		||||
│   └── ...
 | 
			
		||||
├── antispam/ (23)
 | 
			
		||||
│   └── ...
 | 
			
		||||
│
 | 
			
		||||
├── .coveragerc (23)
 | 
			
		||||
├── .envrc (24)
 | 
			
		||||
├── .coveragerc (24)
 | 
			
		||||
├── .envrc (25)
 | 
			
		||||
├── .gitattributes
 | 
			
		||||
├── .gitignore
 | 
			
		||||
├── .mailmap
 | 
			
		||||
├── .env.exemple
 | 
			
		||||
├── manage.py (25)
 | 
			
		||||
├── mkdocs.yml (26)
 | 
			
		||||
├── manage.py (26)
 | 
			
		||||
├── mkdocs.yml (27)
 | 
			
		||||
├── poetry.lock
 | 
			
		||||
├── pyproject.toml (27)
 | 
			
		||||
├── pyproject.toml (28)
 | 
			
		||||
└── README.md
 | 
			
		||||
```
 | 
			
		||||
</div>
 | 
			
		||||
@@ -112,15 +114,16 @@ sith3/
 | 
			
		||||
19. Application principale du projet, contenant sa configuration.
 | 
			
		||||
20. Gestion des stocks des comptoirs.
 | 
			
		||||
21. Gestion des cotisations des utilisateurs du site.
 | 
			
		||||
22. Gestion des trombinoscopes.
 | 
			
		||||
23. Fichier de configuration de coverage.
 | 
			
		||||
24. Fichier de configuration de direnv.
 | 
			
		||||
25. Fichier généré automatiquement par Django. C'est lui
 | 
			
		||||
22. Fonctionalitées pour gérer le spam.
 | 
			
		||||
23. Gestion des trombinoscopes.
 | 
			
		||||
24. Fichier de configuration de coverage.
 | 
			
		||||
25. Fichier de configuration de direnv.
 | 
			
		||||
26. Fichier généré automatiquement par Django. C'est lui
 | 
			
		||||
    qui permet d'appeler des commandes de gestion du projet
 | 
			
		||||
    avec la syntaxe `python ./manage.py <nom de la commande>`
 | 
			
		||||
26. Le fichier de configuration de la documentation,
 | 
			
		||||
27. Le fichier de configuration de la documentation,
 | 
			
		||||
    avec ses plugins et sa table des matières.
 | 
			
		||||
27. Le fichier où sont déclarés les dépendances et la configuration
 | 
			
		||||
28. Le fichier où sont déclarés les dépendances et la configuration
 | 
			
		||||
    de certaines d'entre elles.
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -66,6 +66,7 @@ nav:
 | 
			
		||||
    - Gestion des permissions: tutorial/perms.md
 | 
			
		||||
    - Gestion des groupes: tutorial/groups.md
 | 
			
		||||
    - Etransactions: tutorial/etransaction.md
 | 
			
		||||
    - Installer le projet (Avancé): tutorial/install_advanced.md
 | 
			
		||||
  - How-to:
 | 
			
		||||
    - L'ORM de Django: howto/querysets.md
 | 
			
		||||
    - Gérer les migrations: howto/migrations.md
 | 
			
		||||
@@ -80,6 +81,9 @@ nav:
 | 
			
		||||
    - accounting:
 | 
			
		||||
      - reference/accounting/models.md
 | 
			
		||||
      - reference/accounting/views.md
 | 
			
		||||
    - antispam:
 | 
			
		||||
      - reference/antispam/models.md
 | 
			
		||||
      - reference/antispam/forms.md
 | 
			
		||||
    - club:
 | 
			
		||||
      - reference/club/models.md
 | 
			
		||||
      - reference/club/views.md
 | 
			
		||||
 
 | 
			
		||||
@@ -98,6 +98,7 @@ INSTALLED_APPS = (
 | 
			
		||||
    "matmat",
 | 
			
		||||
    "pedagogy",
 | 
			
		||||
    "galaxy",
 | 
			
		||||
    "antispam",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = (
 | 
			
		||||
@@ -204,13 +205,12 @@ SASS_PRECISION = 8
 | 
			
		||||
WSGI_APPLICATION = "sith.wsgi.application"
 | 
			
		||||
 | 
			
		||||
# Database
 | 
			
		||||
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
 | 
			
		||||
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "ENGINE": "django.db.backends.sqlite3",
 | 
			
		||||
        "NAME": BASE_DIR / "db.sqlite3",
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
 | 
			
		||||
@@ -673,6 +673,10 @@ SITH_GIFT_LIST = [("AE Tee-shirt", _("AE tee-shirt"))]
 | 
			
		||||
SENTRY_DSN = ""
 | 
			
		||||
SENTRY_ENV = "production"
 | 
			
		||||
 | 
			
		||||
TOXIC_DOMAINS_PROVIDERS = [
 | 
			
		||||
    "https://www.stopforumspam.com/downloads/toxic_domains_whole.txt",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from .settings_custom import *
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user