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 .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(' <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='form-row'
@ -156,7 +157,6 @@ class InviteSettingsForm(forms.ModelForm):
css_class='col-xl-6'),
Column(
HTML("<h3>Message templates</h3>"),
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

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
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)

View File

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