• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / InvenTree / 6079611922

05 Sep 2023 02:43AM UTC coverage: 88.545% (-0.01%) from 88.556%
6079611922

push

github

web-flow
Login form fix (#5502) (#5504)

* Handle login without supplier user

- Use custom login form
- Redirect back to login page
- No longer throws error

* Fix method return

(cherry picked from commit 71ad4a1c9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>

5 of 5 new or added lines in 2 files covered. (100.0%)

26846 of 30319 relevant lines covered (88.55%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.74
/InvenTree/InvenTree/forms.py
1
"""Helper forms which subclass Django forms to provide additional functionality."""
2

3
import logging
4
from urllib.parse import urlencode
5

6
from django import forms
7
from django.conf import settings
8
from django.contrib.auth.models import Group, User
9
from django.contrib.sites.models import Site
10
from django.http import HttpResponseRedirect
11
from django.urls import reverse
12
from django.utils.translation import gettext_lazy as _
13

14
from allauth.account.adapter import DefaultAccountAdapter
15
from allauth.account.forms import LoginForm, SignupForm, set_form_field_order
16
from allauth.exceptions import ImmediateHttpResponse
17
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
18
from allauth_2fa.adapter import OTPAdapter
19
from allauth_2fa.utils import user_has_valid_totp_device
20
from crispy_forms.bootstrap import (AppendedText, PrependedAppendedText,
21
                                    PrependedText)
22
from crispy_forms.helper import FormHelper
23
from crispy_forms.layout import Field, Layout
24
from dj_rest_auth.registration.serializers import RegisterSerializer
25
from rest_framework import serializers
26

27
from common.models import InvenTreeSetting
28
from InvenTree.exceptions import log_error
29

30
logger = logging.getLogger('inventree')
31

32

33
class HelperForm(forms.ModelForm):
34
    """Provides simple integration of crispy_forms extension."""
35

36
    # Custom field decorations can be specified here, per form class
37
    field_prefix = {}
1✔
38
    field_suffix = {}
1✔
39
    field_placeholder = {}
1✔
40

41
    def __init__(self, *args, **kwargs):
1✔
42
        """Setup layout."""
43
        super(forms.ModelForm, self).__init__(*args, **kwargs)
44
        self.helper = FormHelper()
45

46
        self.helper.form_tag = False
47
        self.helper.form_show_errors = True
48

49
        """
50
        Create a default 'layout' for this form.
×
51
        Ref: https://django-crispy-forms.readthedocs.io/en/latest/layouts.html
×
52
        This is required to do fancy things later (like adding PrependedText, etc).
×
53

54
        Simply create a 'blank' layout for each available field.
×
55
        """
×
56

57
        self.rebuild_layout()
×
58

59
    def rebuild_layout(self):
1✔
60
        """Build crispy layout out of current fields."""
61
        layouts = []
62

63
        for field in self.fields:
64
            prefix = self.field_prefix.get(field, None)
65
            suffix = self.field_suffix.get(field, None)
66
            placeholder = self.field_placeholder.get(field, '')
67

68
            # Look for font-awesome icons
69
            if prefix and prefix.startswith('fa-'):
70
                prefix = r"<i class='fas {fa}'/>".format(fa=prefix)
71

72
            if suffix and suffix.startswith('fa-'):
73
                suffix = r"<i class='fas {fa}'/>".format(fa=suffix)
74

75
            if prefix and suffix:
76
                layouts.append(
77
                    Field(
78
                        PrependedAppendedText(
79
                            field,
80
                            prepended_text=prefix,
81
                            appended_text=suffix,
82
                            placeholder=placeholder
83
                        )
84
                    )
85
                )
86

87
            elif prefix:
88
                layouts.append(
89
                    Field(
90
                        PrependedText(
91
                            field,
92
                            prefix,
93
                            placeholder=placeholder
94
                        )
95
                    )
96
                )
97

98
            elif suffix:
99
                layouts.append(
100
                    Field(
101
                        AppendedText(
102
                            field,
103
                            suffix,
104
                            placeholder=placeholder
105
                        )
106
                    )
107
                )
108

109
            else:
110
                layouts.append(Field(field, placeholder=placeholder))
111

112
        self.helper.layout = Layout(*layouts)
113

114

115
class EditUserForm(HelperForm):
116
    """Form for editing user information."""
117

118
    class Meta:
1✔
119
        """Metaclass options."""
120

121
        model = User
122
        fields = [
123
            'first_name',
124
            'last_name',
125
        ]
126

127

128
class SetPasswordForm(HelperForm):
129
    """Form for setting user password."""
130

131
    class Meta:
1✔
132
        """Metaclass options."""
133

134
        model = User
135
        fields = [
136
            'enter_password',
137
            'confirm_password',
138
            'old_password',
139
        ]
140

141
    enter_password = forms.CharField(
142
        max_length=100,
143
        min_length=8,
144
        required=True,
145
        initial='',
146
        widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
147
        label=_('Enter password'),
148
        help_text=_('Enter new password')
149
    )
150

151
    confirm_password = forms.CharField(
152
        max_length=100,
153
        min_length=8,
154
        required=True,
155
        initial='',
156
        widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
157
        label=_('Confirm password'),
158
        help_text=_('Confirm new password')
159
    )
160

161
    old_password = forms.CharField(
162
        label=_("Old password"),
163
        strip=False,
164
        widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}),
165
    )
166

167

168
# override allauth
169
class CustomLoginForm(LoginForm):
170
    """Custom login form to override default allauth behaviour"""
171

172
    def login(self, request, redirect_url=None):
1✔
173
        """Perform login action.
174

175
        First check that:
176
        - A valid user has been supplied
177
        """
178

179
        if not self.user:
1✔
180
            # No user supplied - redirect to the login page
181
            return HttpResponseRedirect(reverse('account_login'))
×
182

183
        # Now perform default login action
184
        return super().login(request, redirect_url)
1✔
185

186

187
class CustomSignupForm(SignupForm):
1✔
188
    """Override to use dynamic settings."""
189

190
    def __init__(self, *args, **kwargs):
191
        """Check settings to influence which fields are needed."""
192
        kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED')
×
193

194
        super().__init__(*args, **kwargs)
×
195

196
        # check for two mail fields
197
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
×
198
            self.fields["email2"] = forms.EmailField(
199
                label=_("Email (again)"),
200
                widget=forms.TextInput(
201
                    attrs={
202
                        "type": "email",
203
                        "placeholder": _("Email address confirmation"),
204
                    }
205
                ),
206
            )
207

208
        # check for two password fields
209
        if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'):
210
            self.fields.pop("password2")
211

212
        # reorder fields
213
        set_form_field_order(self, ["username", "email", "email2", "password1", "password2", ])
214

215
    def clean(self):
1✔
216
        """Make sure the supllied emails match if enabled in settings."""
217
        cleaned_data = super().clean()
218

219
        # check for two mail fields
220
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
221
            email = cleaned_data.get("email")
222
            email2 = cleaned_data.get("email2")
223
            if (email and email2) and email != email2:
224
                self.add_error("email2", _("You must type the same email each time."))
225

226
        return cleaned_data
227

228

229
def registration_enabled():
230
    """Determine whether user registration is enabled."""
231
    return settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'))
232

233

234
class RegistratonMixin:
1✔
235
    """Mixin to check if registration should be enabled."""
236

237
    def is_open_for_signup(self, request, *args, **kwargs):
238
        """Check if signup is enabled in settings.
239

240
        Configure the class variable `REGISTRATION_SETTING` to set which setting should be used, default: `LOGIN_ENABLE_REG`.
241
        """
242
        if registration_enabled():
243
            return super().is_open_for_signup(request, *args, **kwargs)
244
        return False
245

246
    def clean_email(self, email):
1✔
247
        """Check if the mail is valid to the pattern in LOGIN_SIGNUP_MAIL_RESTRICTION (if enabled in settings)."""
248
        mail_restriction = InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', None)
249
        if not mail_restriction:
250
            return super().clean_email(email)
251

252
        split_email = email.split('@')
253
        if len(split_email) != 2:
254
            logger.error(f'The user {email} has an invalid email address')
255
            raise forms.ValidationError(_('The provided primary email address is not valid.'))
256

257
        mailoptions = mail_restriction.split(',')
258
        for option in mailoptions:
259
            if not option.startswith('@'):
260
                log_error('LOGIN_SIGNUP_MAIL_RESTRICTION is not configured correctly')
261
                raise forms.ValidationError(_('The provided primary email address is not valid.'))
262
            else:
263
                if split_email[1] == option[1:]:
264
                    return super().clean_email(email)
265

266
        logger.info(f'The provided email domain for {email} is not approved')
267
        raise forms.ValidationError(_('The provided email domain is not approved.'))
268

269
    def save_user(self, request, user, form, commit=True):
270
        """Check if a default group is set in settings."""
271
        # Create the user
272
        user = super().save_user(request, user, form)
273

274
        # Check if a default group is set in settings
275
        start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP')
276
        if start_group:
277
            try:
278
                group = Group.objects.get(id=start_group)
279
                user.groups.add(group)
280
            except Group.DoesNotExist:
281
                logger.error('The setting `SIGNUP_GROUP` contains an non existent group', start_group)
282
        user.save()
283
        return user
284

285

286
class CustomUrlMixin:
1✔
287
    """Mixin to set urls."""
288

289
    def get_email_confirmation_url(self, request, emailconfirmation):
290
        """Custom email confirmation (activation) url."""
291
        url = reverse("account_confirm_email", args=[emailconfirmation.key])
292
        return Site.objects.get_current().domain + url
293

294

295
class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
1✔
296
    """Override of adapter to use dynamic settings."""
297

298
    def send_mail(self, template_prefix, email, context):
299
        """Only send mail if backend configured."""
300
        if settings.EMAIL_HOST:
301
            try:
302
                result = super().send_mail(template_prefix, email, context)
303
            except Exception:
304
                # An exception occurred while attempting to send email
305
                # Log it (for admin users) and return silently
306
                log_error('account email')
307
                result = False
308

309
            return result
310

311
        return False
312

313
    def get_email_confirmation_url(self, request, emailconfirmation):
1✔
314
        """Construct the email confirmation url"""
315

316
        from InvenTree.helpers_model import construct_absolute_url
317

318
        url = super().get_email_confirmation_url(request, emailconfirmation)
319
        url = construct_absolute_url(url)
320
        return url
321

322

323
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
324
    """Override of adapter to use dynamic settings."""
325

326
    def is_auto_signup_allowed(self, request, sociallogin):
1✔
327
        """Check if auto signup is enabled in settings."""
328
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True):
329
            return super().is_auto_signup_allowed(request, sociallogin)
330
        return False
331

332
    # from OTPAdapter
333
    def has_2fa_enabled(self, user):
334
        """Returns True if the user has 2FA configured."""
335
        return user_has_valid_totp_device(user)
336

337
    def login(self, request, user):
1✔
338
        """Ensure user is send to 2FA before login if enabled."""
339
        # Require two-factor authentication if it has been configured.
340
        if self.has_2fa_enabled(user):
341
            # Cast to string for the case when this is not a JSON serializable
342
            # object, e.g. a UUID.
343
            request.session['allauth_2fa_user_id'] = str(user.id)
344

345
            redirect_url = reverse('two-factor-authenticate')
346
            # Add GET parameters to the URL if they exist.
347
            if request.GET:
348
                redirect_url += '?' + urlencode(request.GET)
349

350
            raise ImmediateHttpResponse(
351
                response=HttpResponseRedirect(redirect_url)
352
            )
353

354
        # Otherwise defer to the original allauth adapter.
355
        return super().login(request, user)
356

357

358
# override dj-rest-auth
359
class CustomRegisterSerializer(RegisterSerializer):
360
    """Override of serializer to use dynamic settings."""
361
    email = serializers.EmailField()
1✔
362

363
    def __init__(self, instance=None, data=..., **kwargs):
1✔
364
        """Check settings to influence which fields are needed."""
365
        kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED')
366
        super().__init__(instance, data, **kwargs)
367

368
    def save(self, request):
369
        """Override to check if registration is open."""
370
        if registration_enabled():
371
            return super().save(request)
372
        raise forms.ValidationError(_('Registration is disabled.'))
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc