2026-03-15 17:08:58 -03:00
|
|
|
import re
|
|
|
|
|
|
2026-03-12 09:22:00 -03:00
|
|
|
import pyotp
|
2026-03-15 17:08:58 -03:00
|
|
|
from argon2 import PasswordHasher
|
2026-03-12 15:22:58 -03:00
|
|
|
from crispy_forms.bootstrap import PrependedText
|
2026-03-12 09:22:00 -03:00
|
|
|
from crispy_forms.helper import FormHelper
|
2026-03-15 17:08:58 -03:00
|
|
|
from crispy_forms.layout import Layout, Submit, HTML, Div
|
2026-03-12 09:22:00 -03:00
|
|
|
from django import forms
|
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
|
|
|
|
from gatekeeper.models import GatekeeperUser, GatekeeperGroup, AuthMethod, AuthMethodAllowedDomain, \
|
2026-03-12 09:58:08 -03:00
|
|
|
AuthMethodAllowedEmail, GatekeeperIPAddress
|
2026-03-12 09:22:00 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class GatekeeperUserForm(forms.ModelForm):
|
2026-03-15 17:08:58 -03:00
|
|
|
password = forms.CharField(
|
|
|
|
|
label=_('Password'),
|
|
|
|
|
required=False,
|
|
|
|
|
widget=forms.PasswordInput(render_value=False),
|
|
|
|
|
help_text=_('Minimum 8 characters, with at least one uppercase letter, one lowercase letter, and one number.'),
|
|
|
|
|
)
|
|
|
|
|
password_confirm = forms.CharField(
|
|
|
|
|
label=_('Confirm Password'),
|
|
|
|
|
required=False,
|
|
|
|
|
widget=forms.PasswordInput(render_value=False),
|
|
|
|
|
)
|
|
|
|
|
totp_pin = forms.CharField(
|
|
|
|
|
label=_('TOTP Validation PIN'),
|
|
|
|
|
max_length=6,
|
|
|
|
|
required=False,
|
|
|
|
|
help_text=_('Enter a 6-digit PIN generated by your authenticator app to validate the secret.'),
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-12 09:22:00 -03:00
|
|
|
class Meta:
|
|
|
|
|
model = GatekeeperUser
|
2026-03-15 17:08:58 -03:00
|
|
|
fields = ['username', 'email', 'totp_secret']
|
2026-03-12 09:22:00 -03:00
|
|
|
labels = {
|
|
|
|
|
'username': _('Username'),
|
|
|
|
|
'email': _('Email'),
|
|
|
|
|
'totp_secret': _('TOTP Secret'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
2026-03-15 17:08:58 -03:00
|
|
|
is_edit = bool(self.instance and self.instance.pk)
|
|
|
|
|
self.fields['password'].required = not is_edit
|
|
|
|
|
self.fields['password_confirm'].required = not is_edit
|
|
|
|
|
|
2026-03-12 09:22:00 -03:00
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('username', css_class='col-xl-6'),
|
|
|
|
|
Div('email', css_class='col-xl-6'),
|
2026-03-12 09:22:00 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-15 17:08:58 -03:00
|
|
|
Div('password', css_class='col-xl-6'),
|
|
|
|
|
Div('password_confirm', css_class='col-xl-6'),
|
|
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('totp_secret', css_class='col-xl-6'),
|
2026-03-15 17:08:58 -03:00
|
|
|
Div('totp_pin', css_class='col-xl-6'),
|
2026-03-12 09:22:00 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-14 08:52:31 -03:00
|
|
|
css_class='col-xl-12'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-15 17:08:58 -03:00
|
|
|
def clean(self):
|
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
password = cleaned_data.get('password')
|
|
|
|
|
password_confirm = cleaned_data.get('password_confirm')
|
|
|
|
|
totp_secret = cleaned_data.get('totp_secret')
|
|
|
|
|
is_new = not (self.instance and self.instance.pk)
|
|
|
|
|
|
|
|
|
|
if password or is_new:
|
|
|
|
|
if not password:
|
|
|
|
|
self.add_error('password', _('Password is required.'))
|
|
|
|
|
else:
|
|
|
|
|
if len(password) < 8:
|
|
|
|
|
self.add_error('password', _('Password must be at least 8 characters long.'))
|
|
|
|
|
elif not re.search(r'[a-z]', password):
|
|
|
|
|
self.add_error('password', _('Password must contain at least one lowercase letter.'))
|
|
|
|
|
elif not re.search(r'[A-Z]', password):
|
|
|
|
|
self.add_error('password', _('Password must contain at least one uppercase letter.'))
|
|
|
|
|
elif not re.search(r'[0-9]', password):
|
|
|
|
|
self.add_error('password', _('Password must contain at least one number.'))
|
|
|
|
|
elif password != password_confirm:
|
|
|
|
|
self.add_error('password_confirm', _('Passwords do not match.'))
|
|
|
|
|
|
|
|
|
|
if totp_secret:
|
|
|
|
|
totp_pin = cleaned_data.get('totp_pin')
|
|
|
|
|
if not totp_pin:
|
|
|
|
|
self.add_error('totp_pin', _('Please provide a PIN to validate the TOTP secret.'))
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
totp = pyotp.TOTP(totp_secret)
|
|
|
|
|
if not totp.verify(totp_pin):
|
|
|
|
|
self.add_error('totp_pin', _('Invalid TOTP PIN.'))
|
|
|
|
|
except Exception:
|
|
|
|
|
self.add_error('totp_secret', _('Invalid TOTP secret format. Must be a valid Base32 string.'))
|
|
|
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
|
def save(self, commit=True):
|
|
|
|
|
user = super().save(commit=False)
|
|
|
|
|
password = self.cleaned_data.get('password')
|
|
|
|
|
if password:
|
|
|
|
|
ph = PasswordHasher()
|
|
|
|
|
user.password = ph.hash(password)
|
|
|
|
|
if commit:
|
|
|
|
|
user.save()
|
|
|
|
|
return user
|
|
|
|
|
|
2026-03-12 09:22:00 -03:00
|
|
|
|
|
|
|
|
class GatekeeperGroupForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = GatekeeperGroup
|
2026-03-16 16:33:12 -03:00
|
|
|
fields = ['display_name', 'users']
|
2026-03-12 09:22:00 -03:00
|
|
|
labels = {
|
2026-03-16 16:33:12 -03:00
|
|
|
'display_name': _('Group Name'),
|
2026-03-12 09:22:00 -03:00
|
|
|
'users': _('Members'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
2026-03-16 16:33:12 -03:00
|
|
|
self.fields['display_name'].required = True
|
2026-03-12 09:22:00 -03:00
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-16 16:33:12 -03:00
|
|
|
Div('display_name', css_class='col-xl-12'),
|
2026-03-12 09:22:00 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('users', css_class='col-xl-12'),
|
2026-03-12 09:22:00 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-14 08:52:31 -03:00
|
|
|
css_class='col-xl-12'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AuthMethodForm(forms.ModelForm):
|
|
|
|
|
totp_pin = forms.CharField(
|
|
|
|
|
label=_('TOTP Validation PIN'),
|
|
|
|
|
max_length=6,
|
|
|
|
|
required=False,
|
|
|
|
|
help_text=_('Enter a 6-digit PIN generated by your authenticator app to validate the secret.')
|
|
|
|
|
)
|
2026-03-16 09:49:29 -03:00
|
|
|
session_expiration_value = forms.IntegerField(
|
|
|
|
|
label=_('Session Expiration'),
|
|
|
|
|
min_value=1,
|
|
|
|
|
required=False,
|
|
|
|
|
initial=12,
|
|
|
|
|
)
|
|
|
|
|
session_expiration_unit = forms.ChoiceField(
|
|
|
|
|
label=_('Unit'),
|
|
|
|
|
choices=[('hours', _('Hour(s)')), ('days', _('Day(s)'))],
|
|
|
|
|
required=False,
|
|
|
|
|
initial='hours',
|
|
|
|
|
)
|
2026-03-12 09:22:00 -03:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = AuthMethod
|
|
|
|
|
fields = [
|
2026-03-16 16:33:12 -03:00
|
|
|
'display_name', 'auth_type', 'totp_secret',
|
2026-03-12 09:22:00 -03:00
|
|
|
'oidc_provider', 'oidc_client_id', 'oidc_client_secret'
|
|
|
|
|
]
|
|
|
|
|
labels = {
|
2026-03-16 16:33:12 -03:00
|
|
|
'display_name': _('Name'),
|
2026-03-12 09:22:00 -03:00
|
|
|
'auth_type': _('Authentication Type'),
|
|
|
|
|
'totp_secret': _('Global TOTP Secret'),
|
|
|
|
|
'oidc_provider': _('OIDC Provider URL'),
|
|
|
|
|
'oidc_client_id': _('OIDC Client ID'),
|
|
|
|
|
'oidc_client_secret': _('OIDC Client Secret'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
2026-03-16 16:33:12 -03:00
|
|
|
self.fields['display_name'].required = True
|
2026-03-12 09:22:00 -03:00
|
|
|
if self.instance and self.instance.pk:
|
|
|
|
|
self.fields['auth_type'].disabled = True
|
2026-03-16 09:49:29 -03:00
|
|
|
exp_min = self.instance.session_expiration_minutes
|
|
|
|
|
if exp_min % 1440 == 0:
|
|
|
|
|
self.initial['session_expiration_value'] = exp_min // 1440
|
|
|
|
|
self.initial['session_expiration_unit'] = 'days'
|
|
|
|
|
else:
|
|
|
|
|
self.initial['session_expiration_value'] = max(1, round(exp_min / 60))
|
|
|
|
|
self.initial['session_expiration_unit'] = 'hours'
|
2026-03-12 09:22:00 -03:00
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-16 16:33:12 -03:00
|
|
|
Div('display_name', css_class='col-xl-6'),
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('auth_type', css_class='col-xl-6'),
|
2026-03-12 10:26:47 -03:00
|
|
|
css_class='row auth-type-group'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('totp_secret', css_class='col-xl-6'),
|
|
|
|
|
Div('totp_pin', css_class='col-xl-6'),
|
2026-03-12 10:26:47 -03:00
|
|
|
css_class='row totp-group'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('oidc_provider', css_class='col-xl-12'),
|
2026-03-12 10:26:47 -03:00
|
|
|
css_class='row oidc-group'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('oidc_client_id', css_class='col-xl-6'),
|
|
|
|
|
Div('oidc_client_secret', css_class='col-xl-6'),
|
2026-03-12 10:26:47 -03:00
|
|
|
css_class='row oidc-group'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
2026-03-16 09:49:29 -03:00
|
|
|
Div(
|
|
|
|
|
Div('session_expiration_value', css_class='col-xl-6'),
|
|
|
|
|
Div('session_expiration_unit', css_class='col-xl-6'),
|
|
|
|
|
css_class='row expiration-group'
|
|
|
|
|
),
|
2026-03-12 09:22:00 -03:00
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-14 08:52:31 -03:00
|
|
|
css_class='col-xl-12'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
auth_type = cleaned_data.get('auth_type')
|
|
|
|
|
totp_secret = cleaned_data.get('totp_secret')
|
|
|
|
|
oidc_provider = cleaned_data.get('oidc_provider')
|
|
|
|
|
oidc_client_id = cleaned_data.get('oidc_client_id')
|
|
|
|
|
oidc_client_secret = cleaned_data.get('oidc_client_secret')
|
|
|
|
|
|
|
|
|
|
if auth_type == 'local_password':
|
|
|
|
|
if totp_secret:
|
|
|
|
|
self.add_error('totp_secret', _('TOTP secret must be empty for Local Password authentication.'))
|
|
|
|
|
if cleaned_data.get('totp_pin'):
|
|
|
|
|
self.add_error('totp_pin', _('TOTP validation PIN must be empty for Local Password authentication.'))
|
|
|
|
|
if oidc_provider or oidc_client_id or oidc_client_secret:
|
|
|
|
|
self.add_error(None, _('OIDC fields must be empty for Local Password authentication.'))
|
2026-03-16 09:49:29 -03:00
|
|
|
|
2026-03-12 09:22:00 -03:00
|
|
|
existing_local = AuthMethod.objects.filter(auth_type='local_password')
|
|
|
|
|
if self.instance and self.instance.pk:
|
|
|
|
|
existing_local = existing_local.exclude(pk=self.instance.pk)
|
|
|
|
|
if existing_local.exists():
|
|
|
|
|
self.add_error('auth_type', _('Only one Local Password authentication method can be configured.'))
|
|
|
|
|
elif auth_type == 'totp':
|
|
|
|
|
if oidc_provider or oidc_client_id or oidc_client_secret:
|
|
|
|
|
self.add_error(None, _('OIDC fields must be empty for TOTP authentication.'))
|
|
|
|
|
if not totp_secret:
|
|
|
|
|
self.add_error('totp_secret', _('TOTP secret is required for TOTP authentication.'))
|
|
|
|
|
else:
|
|
|
|
|
totp_pin = cleaned_data.get('totp_pin')
|
|
|
|
|
if not totp_pin:
|
|
|
|
|
self.add_error('totp_pin', _('Please provide a PIN to validate the TOTP secret.'))
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
totp = pyotp.TOTP(totp_secret)
|
|
|
|
|
if not totp.verify(totp_pin):
|
|
|
|
|
self.add_error('totp_pin', _('Invalid TOTP PIN.'))
|
|
|
|
|
except Exception:
|
|
|
|
|
self.add_error('totp_secret', _('Invalid TOTP secret format. Must be a valid Base32 string.'))
|
|
|
|
|
elif auth_type == 'oidc':
|
2026-03-16 15:59:05 -03:00
|
|
|
self.add_error('auth_type', _('OIDC authentication is temporarily unavailable and will be available soon.'))
|
2026-03-12 09:22:00 -03:00
|
|
|
if totp_secret:
|
|
|
|
|
self.add_error('totp_secret', _('TOTP secret must be empty for OIDC authentication.'))
|
|
|
|
|
if cleaned_data.get('totp_pin'):
|
|
|
|
|
self.add_error('totp_pin', _('TOTP validation PIN must be empty for OIDC authentication.'))
|
|
|
|
|
|
2026-03-16 09:49:29 -03:00
|
|
|
if auth_type in ('local_password', 'oidc'):
|
|
|
|
|
value = cleaned_data.get('session_expiration_value') or 12
|
|
|
|
|
unit = cleaned_data.get('session_expiration_unit') or 'hours'
|
|
|
|
|
if unit == 'days':
|
|
|
|
|
cleaned_data['_session_expiration_minutes'] = value * 1440
|
|
|
|
|
else:
|
|
|
|
|
cleaned_data['_session_expiration_minutes'] = value * 60
|
|
|
|
|
|
2026-03-12 09:22:00 -03:00
|
|
|
return cleaned_data
|
|
|
|
|
|
2026-03-16 09:49:29 -03:00
|
|
|
def save(self, commit=True):
|
|
|
|
|
instance = super().save(commit=False)
|
|
|
|
|
if instance.auth_type in ('local_password', 'oidc'):
|
|
|
|
|
instance.session_expiration_minutes = self.cleaned_data.get('_session_expiration_minutes', 720)
|
|
|
|
|
if commit:
|
|
|
|
|
instance.save()
|
|
|
|
|
return instance
|
|
|
|
|
|
2026-03-12 09:58:08 -03:00
|
|
|
class GatekeeperIPAddressForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = GatekeeperIPAddress
|
|
|
|
|
fields = ['auth_method', 'address', 'prefix_length', 'action', 'description']
|
|
|
|
|
labels = {
|
|
|
|
|
'auth_method': _('Authentication Method'),
|
2026-03-12 14:58:36 -03:00
|
|
|
'address': _('IP Address'),
|
2026-03-12 09:58:08 -03:00
|
|
|
'prefix_length': _('Prefix Length'),
|
|
|
|
|
'action': _('Action'),
|
|
|
|
|
'description': _('Description'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('auth_method', css_class='col-xl-12'),
|
2026-03-12 09:58:08 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-12 14:58:36 -03:00
|
|
|
Div('address', css_class='col-xl-6'),
|
2026-03-12 15:22:58 -03:00
|
|
|
Div(PrependedText('prefix_length', '/'), css_class='col-xl-6'),
|
2026-03-12 09:58:08 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-12 14:58:36 -03:00
|
|
|
Div('action', css_class='col-xl-4'),
|
|
|
|
|
Div('description', css_class='col-xl-8'),
|
2026-03-12 09:58:08 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-14 08:52:31 -03:00
|
|
|
css_class='col-xl-12'
|
2026-03-12 09:58:08 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-12 09:22:00 -03:00
|
|
|
class AuthMethodAllowedDomainForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = AuthMethodAllowedDomain
|
|
|
|
|
fields = ['auth_method', 'domain']
|
|
|
|
|
labels = {
|
|
|
|
|
'auth_method': _('Authentication Method'),
|
|
|
|
|
'domain': _('Domain'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('auth_method', css_class='col-xl-6'),
|
2026-03-12 15:22:58 -03:00
|
|
|
Div(PrependedText('domain', '@'), css_class='col-xl-6'),
|
2026-03-12 09:22:00 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-14 08:52:31 -03:00
|
|
|
css_class='col-xl-12'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AuthMethodAllowedEmailForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = AuthMethodAllowedEmail
|
|
|
|
|
fields = ['auth_method', 'email']
|
|
|
|
|
labels = {
|
|
|
|
|
'auth_method': _('Authentication Method'),
|
|
|
|
|
'email': _('Email'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-14 08:52:31 -03:00
|
|
|
Div('auth_method', css_class='col-xl-6'),
|
|
|
|
|
Div('email', css_class='col-xl-6'),
|
2026-03-12 09:22:00 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-14 08:52:31 -03:00
|
|
|
css_class='col-xl-12'
|
2026-03-12 09:22:00 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|