From dc85a7671599595f28695cd3fefa6e2d7e1bb469 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 27 Feb 2025 22:41:20 -0300 Subject: [PATCH] VPN invite settings form and validation logic --- vpn_invite/forms.py | 73 ++++++++++++++++++- ...5_alter_invitesettings_download_1_label.py | 18 +++++ ...vitesettings_invite_email_body_and_more.py | 28 +++++++ vpn_invite/models.py | 8 +- vpn_invite/views.py | 30 ++++++-- wireguard_webadmin/urls.py | 5 +- 6 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 vpn_invite/migrations/0005_alter_invitesettings_download_1_label.py create mode 100644 vpn_invite/migrations/0006_alter_invitesettings_invite_email_body_and_more.py diff --git a/vpn_invite/forms.py b/vpn_invite/forms.py index 47151ce..378e877 100644 --- a/vpn_invite/forms.py +++ b/vpn_invite/forms.py @@ -1,8 +1,9 @@ -from crispy_forms.templatetags.crispy_forms_field import css_class from django import forms -from .models import InviteSettings +from django.core.exceptions import ValidationError from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Row, Column, Submit, HTML +from crispy_forms.templatetags.crispy_forms_field import css_class +from .models import InviteSettings class InviteSettingsForm(forms.ModelForm): @@ -148,7 +149,7 @@ class InviteSettingsForm(forms.ModelForm): Row( Column( Submit('submit', 'Save', css_class='btn btn-success'), - HTML(' Back '), + HTML(' Back '), css_class='col-md-12' ), css_class='form-row' @@ -156,7 +157,6 @@ class InviteSettingsForm(forms.ModelForm): css_class='col-xl-6'), Column( HTML("

Message templates

"), - Column( css_class='form-group col-md-12 mb-0'), Row( Column('download_instructions', css_class='form-group col-md-12 mb-0'), css_class='form-row' @@ -188,3 +188,68 @@ class InviteSettingsForm(forms.ModelForm): css_class='col-xl-6'), css_class='row'), ) + + def clean(self): + cleaned_data = super().clean() + + # Validate invite_url: it must start with 'https://' and end with '/invite/' + invite_url = cleaned_data.get('invite_url') + if invite_url: + if not invite_url.startswith("https://"): + self.add_error('invite_url', "Invite URL must start with 'https://'.") + if not invite_url.endswith("/invite/"): + self.add_error('invite_url', "Invite URL must end with '/invite/'.") + + # Validate invite_expiration: must be between 1 and 1440 minutes + invite_expiration = cleaned_data.get('invite_expiration') + if invite_expiration is not None: + if invite_expiration < 1 or invite_expiration > 1440: + self.add_error('invite_expiration', "Expiration (minutes) must be between 1 and 1440.") + + # Validate default_password based on enforce_random_password flag + default_password = cleaned_data.get('default_password', '') + enforce_random_password = cleaned_data.get('enforce_random_password') + random_password_length = cleaned_data.get('random_password_length') + if enforce_random_password is True: + if default_password: + self.add_error('default_password', + "Default password must not be provided when random password is enabled.") + if random_password_length < 6: + self.add_error('random_password_length', "Random password length must be at least 6 characters.") + else: + # When random password is disabled, default password must be provided and have at least 6 characters. + if not default_password: + self.add_error('default_password', + "Default password must be provided when random password is disabled.") + elif len(default_password) < 6: + self.add_error('default_password', "Default password must be at least 6 characters long.") + + # Validate download buttons: if enabled, the respective text and url fields must not be blank. + for i in range(1, 6): + enabled = cleaned_data.get(f'download_{i}_enabled') + label = (cleaned_data.get(f'download_{i}_label') or '').strip() + url = (cleaned_data.get(f'download_{i}_url') or '').strip() + if enabled: + if not label: + self.add_error(f'download_{i}_label', + "Text field must not be empty when download button is enabled.") + if not url: + self.add_error(f'download_{i}_url', "URL field must not be empty when download button is enabled.") + + # Validate that default_password is not contained in any message templates or the subject + message_fields = ['invite_text_body', 'invite_email_subject', 'invite_email_body', 'invite_whatsapp_body'] + if default_password: + for field in message_fields: + content = cleaned_data.get(field, '') + if default_password in content: + self.add_error('default_password', + f"Default password must not be contained in {field.replace('_', ' ')}.") + + # Validate that all message templates include the placeholder '{invite_url}' + for field in message_fields: + if field != 'invite_email_subject': + content = cleaned_data.get(field, '') + if '{invite_url}' not in content: + self.add_error(field, "The template must include the placeholder '{invite_url}'.") + + return cleaned_data diff --git a/vpn_invite/migrations/0005_alter_invitesettings_download_1_label.py b/vpn_invite/migrations/0005_alter_invitesettings_download_1_label.py new file mode 100644 index 0000000..65c6e70 --- /dev/null +++ b/vpn_invite/migrations/0005_alter_invitesettings_download_1_label.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-02-27 19:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vpn_invite', '0004_alter_invitesettings_required_user_level'), + ] + + operations = [ + migrations.AlterField( + model_name='invitesettings', + name='download_1_label', + field=models.CharField(blank=True, default='iOS', max_length=32, null=True), + ), + ] diff --git a/vpn_invite/migrations/0006_alter_invitesettings_invite_email_body_and_more.py b/vpn_invite/migrations/0006_alter_invitesettings_invite_email_body_and_more.py new file mode 100644 index 0000000..6d3f8e5 --- /dev/null +++ b/vpn_invite/migrations/0006_alter_invitesettings_invite_email_body_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.5 on 2025-02-28 00:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vpn_invite', '0005_alter_invitesettings_download_1_label'), + ] + + operations = [ + migrations.AlterField( + model_name='invitesettings', + name='invite_email_body', + field=models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}\n\nThis link expires in {expire_minutes} minutes.'), + ), + migrations.AlterField( + model_name='invitesettings', + name='invite_text_body', + field=models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}\n\nThis link expires in {expire_minutes} minutes.'), + ), + migrations.AlterField( + model_name='invitesettings', + name='invite_whatsapp_body', + field=models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}\n\nThis link expires in {expire_minutes} minutes.'), + ), + ] diff --git a/vpn_invite/models.py b/vpn_invite/models.py index d623fd6..a4f7dc4 100644 --- a/vpn_invite/models.py +++ b/vpn_invite/models.py @@ -18,7 +18,7 @@ class InviteSettings(models.Model): ) ) invite_expiration = models.IntegerField(default=30) # minutes - download_1_label = models.CharField(max_length=32, default='iPhone', blank=True, null=True) + download_1_label = models.CharField(max_length=32, default='iOS', blank=True, null=True) download_2_label = models.CharField(max_length=32, default='Android', blank=True, null=True) download_3_label = models.CharField(max_length=32, default='Windows', blank=True, null=True) download_4_label = models.CharField(max_length=32, default='macOS', blank=True, null=True) @@ -42,13 +42,13 @@ class InviteSettings(models.Model): invite_url = models.URLField(default='') - invite_text_body = models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}. The link expires in {expire_minutes} minutes.') + invite_text_body = models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}\n\nThis link expires in {expire_minutes} minutes.') invite_email_subject = models.CharField(max_length=64, default='WireGuard VPN Invite', blank=True, null=True) - invite_email_body = models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}. The link expires in {expire_minutes} minutes.') + invite_email_body = models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}\n\nThis link expires in {expire_minutes} minutes.') invite_email_enabled = models.BooleanField(default=True) - invite_whatsapp_body = models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}. The link expires in {expire_minutes} minutes.') + invite_whatsapp_body = models.TextField(default='Here is your WireGuard VPN invite link: {invite_url}\n\nThis link expires in {expire_minutes} minutes.') invite_whatsapp_enabled = models.BooleanField(default=True) uuid = models.UUIDField(default=uuid.uuid4, editable=False) diff --git a/vpn_invite/views.py b/vpn_invite/views.py index a424d64..f03d801 100644 --- a/vpn_invite/views.py +++ b/vpn_invite/views.py @@ -4,10 +4,12 @@ from user_manager.models import UserAcl from .models import InviteSettings, PeerInvite from django.conf import settings from django.utils import timezone +from .forms import InviteSettingsForm +from django.contrib import messages @login_required -def view_vpn_invite_settings(request): +def view_vpn_invite_list(request): if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=50).exists(): return render(request, 'access_denied.html', {'page_title': 'Access Denied'}) if request.GET.get('invite') and request.GET.get('action') == 'delete': @@ -26,15 +28,31 @@ def view_vpn_invite_settings(request): if invite_settings.invite_url.startswith('http://'): invite_settings.invite_url = invite_settings.invite_url.replace('http://', 'https://') invite_settings.save() - peer_invite_list = PeerInvite.objects.all().order_by('invite_expiration') peer_invite_list.filter(invite_expiration__lt=timezone.now()).delete() - - data = { 'page_title': 'VPN Invite', 'peer_invite_list': peer_invite_list, - } - return render(request, 'vpn_invite/invite_settings.html', context=data) \ No newline at end of file + return render(request, 'vpn_invite/invite_settings.html', context=data) + + +@login_required +def view_vpn_invite_settings(request): + if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=50).exists(): + return render(request, 'access_denied.html', {'page_title': 'Access Denied'}) + invite_settings = InviteSettings.objects.get(name='default_settings') + + form = InviteSettingsForm(request.POST or None, instance=invite_settings) + if form.is_valid(): + form.save() + messages.success(request, 'Invite Settings|Settings saved successfully.') + return redirect('/vpn_invite/') + data = { + 'invite_settings': invite_settings, + 'page_title': 'VPN Invite Settings', + 'form': form, + 'form_size': 'col-lg-12' + } + return render(request, 'generic_form.html', context=data) diff --git a/wireguard_webadmin/urls.py b/wireguard_webadmin/urls.py index edc02eb..bfe9782 100644 --- a/wireguard_webadmin/urls.py +++ b/wireguard_webadmin/urls.py @@ -27,7 +27,7 @@ from api.views import wireguard_status, cron_check_updates, cron_update_peer_lat from firewall.views import view_redirect_rule_list, manage_redirect_rule, view_firewall_rule_list, manage_firewall_rule, view_manage_firewall_settings, view_generate_iptables_script, view_reset_firewall, view_firewall_migration_required from dns.views import view_static_host_list, view_manage_static_host, view_manage_dns_settings, view_apply_dns_config from wgrrd.views import view_rrd_graph -from vpn_invite.views import view_vpn_invite_settings +from vpn_invite.views import view_vpn_invite_list, view_vpn_invite_settings urlpatterns = [ path('admin/', admin.site.urls), @@ -68,5 +68,6 @@ urlpatterns = [ path('firewall/generate_firewall_script/', view_generate_iptables_script, name='generate_iptables_script'), path('firewall/reset_to_default/', view_reset_firewall, name='reset_firewall'), path('firewall/migration_required/', view_firewall_migration_required, name='firewall_migration_required'), - path('vpn_invite/', view_vpn_invite_settings, name='vpn_invite_settings'), + path('vpn_invite/', view_vpn_invite_list, name='vpn_invite_list'), + path('vpn_invite/settings/', view_vpn_invite_settings, name='vpn_invite_settings'), ]