VPN invite settings form and validation logic

This commit is contained in:
Eduardo Silva 2025-02-27 22:41:20 -03:00
parent 0277892305
commit dc85a76715
6 changed files with 146 additions and 16 deletions

View File

@ -1,8 +1,9 @@
from crispy_forms.templatetags.crispy_forms_field import css_class
from django import forms from django import forms
from .models import InviteSettings from django.core.exceptions import ValidationError
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Row, Column, Submit, HTML 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): class InviteSettingsForm(forms.ModelForm):
@ -148,7 +149,7 @@ class InviteSettingsForm(forms.ModelForm):
Row( Row(
Column( Column(
Submit('submit', 'Save', css_class='btn btn-success'), Submit('submit', 'Save', css_class='btn btn-success'),
HTML(' <a class="btn btn-secondary" href="/configurations/list/">Back</a> '), HTML(' <a class="btn btn-secondary" href="/vpn_invite/">Back</a> '),
css_class='col-md-12' css_class='col-md-12'
), ),
css_class='form-row' css_class='form-row'
@ -156,7 +157,6 @@ class InviteSettingsForm(forms.ModelForm):
css_class='col-xl-6'), css_class='col-xl-6'),
Column( Column(
HTML("<h3>Message templates</h3>"), HTML("<h3>Message templates</h3>"),
Column( css_class='form-group col-md-12 mb-0'),
Row( Row(
Column('download_instructions', css_class='form-group col-md-12 mb-0'), Column('download_instructions', css_class='form-group col-md-12 mb-0'),
css_class='form-row' css_class='form-row'
@ -188,3 +188,68 @@ class InviteSettingsForm(forms.ModelForm):
css_class='col-xl-6'), css_class='col-xl-6'),
css_class='row'), 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

View File

@ -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),
),
]

View File

@ -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.'),
),
]

View File

@ -18,7 +18,7 @@ class InviteSettings(models.Model):
) )
) )
invite_expiration = models.IntegerField(default=30) # minutes 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_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_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) 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_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_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_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) invite_whatsapp_enabled = models.BooleanField(default=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False) uuid = models.UUIDField(default=uuid.uuid4, editable=False)

View File

@ -4,10 +4,12 @@ from user_manager.models import UserAcl
from .models import InviteSettings, PeerInvite from .models import InviteSettings, PeerInvite
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from .forms import InviteSettingsForm
from django.contrib import messages
@login_required @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(): if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=50).exists():
return render(request, 'access_denied.html', {'page_title': 'Access Denied'}) return render(request, 'access_denied.html', {'page_title': 'Access Denied'})
if request.GET.get('invite') and request.GET.get('action') == 'delete': 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://'): if invite_settings.invite_url.startswith('http://'):
invite_settings.invite_url = invite_settings.invite_url.replace('http://', 'https://') invite_settings.invite_url = invite_settings.invite_url.replace('http://', 'https://')
invite_settings.save() invite_settings.save()
peer_invite_list = PeerInvite.objects.all().order_by('invite_expiration') peer_invite_list = PeerInvite.objects.all().order_by('invite_expiration')
peer_invite_list.filter(invite_expiration__lt=timezone.now()).delete() peer_invite_list.filter(invite_expiration__lt=timezone.now()).delete()
data = { data = {
'page_title': 'VPN Invite', 'page_title': 'VPN Invite',
'peer_invite_list': peer_invite_list, 'peer_invite_list': peer_invite_list,
} }
return render(request, 'vpn_invite/invite_settings.html', context=data) 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)

View File

@ -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 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 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 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 = [ urlpatterns = [
path('admin/', admin.site.urls), 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/generate_firewall_script/', view_generate_iptables_script, name='generate_iptables_script'),
path('firewall/reset_to_default/', view_reset_firewall, name='reset_firewall'), path('firewall/reset_to_default/', view_reset_firewall, name='reset_firewall'),
path('firewall/migration_required/', view_firewall_migration_required, name='firewall_migration_required'), 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'),
] ]