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