diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index 837ef1c..64dc273 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -131,7 +131,13 @@ class AppLoginForm(forms.Form): username = cleaned_data.get('username') password = cleaned_data.get('password') if username and password: - self.user_cache = authenticate(self.request, username=username, password=password) + login_value = (username or '').strip() + auth_username = login_value + user_model = get_user_model() + matched_user = user_model.objects.filter(email__iexact=login_value).first() + if matched_user: + auth_username = matched_user.username + self.user_cache = authenticate(self.request, username=auth_username, password=password) if self.user_cache is None: raise ValidationError(self.error_messages['invalid_login'], code='invalid_login') if not self.user_cache.is_active: diff --git a/backend/workflows/migrations/0059_userprofile_temporary_role_fields.py b/backend/workflows/migrations/0059_userprofile_temporary_role_fields.py new file mode 100644 index 0000000..633e663 --- /dev/null +++ b/backend/workflows/migrations/0059_userprofile_temporary_role_fields.py @@ -0,0 +1,62 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0058_alter_formsectionconfig_options_and_more'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunSQL( + sql=( + "ALTER TABLE workflows_userprofile " + "ADD COLUMN IF NOT EXISTS temporary_role_key varchar(64) NOT NULL DEFAULT '';" + ), + reverse_sql=( + "ALTER TABLE workflows_userprofile " + "DROP COLUMN IF EXISTS temporary_role_key;" + ), + ), + migrations.RunSQL( + sql=( + "ALTER TABLE workflows_userprofile " + "ADD COLUMN IF NOT EXISTS temporary_role_expires_at timestamptz NULL;" + ), + reverse_sql=( + "ALTER TABLE workflows_userprofile " + "DROP COLUMN IF EXISTS temporary_role_expires_at;" + ), + ), + migrations.RunSQL( + sql=( + "ALTER TABLE workflows_userprofile " + "ADD COLUMN IF NOT EXISTS temporary_role_reason text NOT NULL DEFAULT '';" + ), + reverse_sql=( + "ALTER TABLE workflows_userprofile " + "DROP COLUMN IF EXISTS temporary_role_reason;" + ), + ), + ], + state_operations=[ + migrations.AddField( + model_name='userprofile', + name='temporary_role_key', + field=models.CharField(blank=True, default='', max_length=64), + ), + migrations.AddField( + model_name='userprofile', + name='temporary_role_expires_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='userprofile', + name='temporary_role_reason', + field=models.TextField(blank=True, default=''), + ), + ], + ), + ] diff --git a/backend/workflows/model_account.py b/backend/workflows/model_account.py index 7abd427..c6c0875 100644 --- a/backend/workflows/model_account.py +++ b/backend/workflows/model_account.py @@ -61,6 +61,9 @@ class UserProfile(models.Model): totp_secret = models.CharField(max_length=64, blank=True, default='') totp_enabled = models.BooleanField(default=False) totp_confirmed_at = models.DateTimeField(null=True, blank=True) + temporary_role_key = models.CharField(max_length=64, blank=True, default='') + temporary_role_expires_at = models.DateTimeField(null=True, blank=True) + temporary_role_reason = models.TextField(blank=True, default='') totp_recovery_codes = models.JSONField(default=list, blank=True) notification_preferences = models.JSONField(default=dict, blank=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/workflows/tests/test_account_ui.py b/backend/workflows/tests/test_account_ui.py index 90faa78..5fe358c 100644 --- a/backend/workflows/tests/test_account_ui.py +++ b/backend/workflows/tests/test_account_ui.py @@ -32,6 +32,10 @@ class AccountUISmokeTests(TestCase): def test_user_profile_is_created_automatically(self): self.assertTrue(UserProfile.objects.filter(user=self.user).exists()) + profile = UserProfile.objects.get(user=self.user) + self.assertEqual(profile.temporary_role_key, '') + self.assertIsNone(profile.temporary_role_expires_at) + self.assertEqual(profile.temporary_role_reason, '') def test_notification_preferences_can_be_updated(self): response = self.client.post( @@ -179,3 +183,14 @@ class AccountUISmokeTests(TestCase): self.assertEqual(response.status_code, 302) profile.refresh_from_db() self.assertEqual(profile.totp_recovery_codes, []) + + def test_login_accepts_email_after_password_is_set(self): + client = Client() + + response = client.post( + '/accounts/login/', + {'username': 'profile@example.com', 'password': 'secret-12345'}, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302)