Skip to content

Commit ae947ba

Browse files
authored
Merge pull request #660 from MicroPyramid/sf_import
cleanup
2 parents 3076b16 + bfa40ca commit ae947ba

12 files changed

Lines changed: 445 additions & 848 deletions

File tree

backend/common/middleware/get_company.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ def __call__(self, request):
2828
def process_request(self, request):
2929
# Skip JWT validation for authentication endpoints that don't need org context
3030
auth_skip_paths = [
31-
"/api/auth/login/",
3231
"/api/auth/google/",
33-
"/api/auth/register/",
3432
"/api/auth/refresh-token/",
3533
"/api/auth/me/",
3634
"/api/auth/switch-org/",

backend/common/middleware/rls_context.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ class RequireOrgContext:
107107

108108
# Paths that don't require org context
109109
EXEMPT_PATHS = [
110-
"/api/auth/login/",
111-
"/api/auth/register/",
112110
"/api/auth/refresh-token/",
113111
"/api/auth/me/",
114112
"/api/auth/switch-org/",

backend/common/serializer.py

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import re
22

3-
from django.contrib.auth import authenticate
43
from drf_spectacular.utils import extend_schema_field
54
from rest_framework import serializers
65
from rest_framework_simplejwt.tokens import RefreshToken
76

7+
from disposable_email_domains import blocklist as disposable_domains
8+
89
from common.utils import CURRENCY_SYMBOLS
910
from common.models import (
1011
Activity,
@@ -588,33 +589,18 @@ class UserUpdateStatusSwaggerSerializer(serializers.Serializer):
588589
# JWT Authentication Serializers for SvelteKit Integration
589590

590591

591-
class LoginSerializer(serializers.Serializer):
592-
"""Serializer for user login with email and password"""
593-
594-
email = serializers.EmailField(required=True)
595-
password = serializers.CharField(required=True, write_only=True)
596-
597-
def validate(self, attrs):
598-
email = attrs.get("email")
599-
password = attrs.get("password")
600-
601-
if email and password:
602-
user = authenticate(username=email, password=password)
603-
if not user:
604-
raise serializers.ValidationError("Invalid email or password")
605-
if not user.is_active:
606-
raise serializers.ValidationError("User account is disabled")
607-
else:
608-
raise serializers.ValidationError('Must include "email" and "password"')
609-
610-
attrs["user"] = user
611-
return attrs
612-
613-
614592
class MagicLinkRequestSerializer(serializers.Serializer):
615593
"""Serializer for requesting a magic link."""
616594
email = serializers.EmailField(required=True)
617595

596+
def validate_email(self, value):
597+
domain = value.rsplit("@", 1)[-1].lower()
598+
if domain in disposable_domains:
599+
raise serializers.ValidationError(
600+
"Disposable email addresses are not allowed."
601+
)
602+
return value
603+
618604

619605
class MagicLinkVerifySerializer(serializers.Serializer):
620606
"""Serializer for verifying a magic link token."""

backend/common/tests/test_auth.py

Lines changed: 1 addition & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Tests for authentication views: login, register, me, token refresh, org switch,
2+
Tests for authentication views: me, token refresh, org switch,
33
Google OAuth callback, Google ID token, and token refresh edge cases.
44
55
Run with: pytest common/tests/test_auth.py -v
@@ -18,141 +18,6 @@
1818
from common.serializer import OrgAwareRefreshToken
1919

2020

21-
@pytest.mark.django_db
22-
class TestLoginView:
23-
"""Tests for POST /api/auth/login/"""
24-
25-
url = "/api/auth/login/"
26-
27-
def test_login_success(self, unauthenticated_client, admin_user, admin_profile):
28-
response = unauthenticated_client.post(
29-
self.url,
30-
{"email": "[email protected]", "password": "testpass123"},
31-
format="json",
32-
)
33-
assert response.status_code == status.HTTP_200_OK
34-
assert "access_token" in response.data
35-
assert "refresh_token" in response.data
36-
assert "current_org" in response.data
37-
38-
def test_login_wrong_password(self, unauthenticated_client, admin_user, admin_profile):
39-
response = unauthenticated_client.post(
40-
self.url,
41-
{"email": "[email protected]", "password": "wrongpassword"},
42-
format="json",
43-
)
44-
assert response.status_code == status.HTTP_400_BAD_REQUEST
45-
46-
def test_login_nonexistent_user(self, unauthenticated_client):
47-
response = unauthenticated_client.post(
48-
self.url,
49-
{"email": "[email protected]", "password": "testpass123"},
50-
format="json",
51-
)
52-
assert response.status_code == status.HTTP_400_BAD_REQUEST
53-
54-
def test_login_returns_tokens_and_org(
55-
self, unauthenticated_client, admin_user, admin_profile, org_a
56-
):
57-
response = unauthenticated_client.post(
58-
self.url,
59-
{"email": "[email protected]", "password": "testpass123"},
60-
format="json",
61-
)
62-
assert response.status_code == status.HTTP_200_OK
63-
data = response.data
64-
assert "access_token" in data
65-
assert "refresh_token" in data
66-
assert "user" in data
67-
assert "current_org" in data
68-
assert data["current_org"]["id"] == str(org_a.id)
69-
70-
def test_login_with_specific_org(self, unauthenticated_client, admin_user, org_b):
71-
# Give admin_user access to org_b
72-
Profile.objects.create(
73-
user=admin_user, org=org_b, role="USER", is_active=True
74-
)
75-
response = unauthenticated_client.post(
76-
self.url,
77-
{
78-
"email": "[email protected]",
79-
"password": "testpass123",
80-
"org_id": str(org_b.id),
81-
},
82-
format="json",
83-
)
84-
assert response.status_code == status.HTTP_200_OK
85-
assert response.data["current_org"]["id"] == str(org_b.id)
86-
87-
def test_login_unauthorized_org(
88-
self, unauthenticated_client, admin_user, admin_profile, org_b
89-
):
90-
# admin_user does NOT have a profile in org_b
91-
response = unauthenticated_client.post(
92-
self.url,
93-
{
94-
"email": "[email protected]",
95-
"password": "testpass123",
96-
"org_id": str(org_b.id),
97-
},
98-
format="json",
99-
)
100-
assert response.status_code == status.HTTP_403_FORBIDDEN
101-
102-
def test_login_missing_email(self, unauthenticated_client):
103-
"""Login without email should fail."""
104-
response = unauthenticated_client.post(
105-
self.url,
106-
{"password": "testpass123"},
107-
format="json",
108-
)
109-
assert response.status_code == status.HTTP_400_BAD_REQUEST
110-
111-
def test_login_missing_password(self, unauthenticated_client, admin_user):
112-
"""Login without password should fail."""
113-
response = unauthenticated_client.post(
114-
self.url,
115-
{"email": "[email protected]"},
116-
format="json",
117-
)
118-
assert response.status_code == status.HTTP_400_BAD_REQUEST
119-
120-
def test_login_empty_body(self, unauthenticated_client):
121-
"""Login with empty body should fail."""
122-
response = unauthenticated_client.post(
123-
self.url,
124-
{},
125-
format="json",
126-
)
127-
assert response.status_code == status.HTTP_400_BAD_REQUEST
128-
129-
def test_login_user_data_returned(
130-
self, unauthenticated_client, admin_user, admin_profile
131-
):
132-
"""Login should return user details."""
133-
response = unauthenticated_client.post(
134-
self.url,
135-
{"email": "[email protected]", "password": "testpass123"},
136-
format="json",
137-
)
138-
assert response.status_code == status.HTTP_200_OK
139-
user_data = response.data["user"]
140-
assert "email" in user_data
141-
assert user_data["email"] == "[email protected]"
142-
143-
def test_login_user_no_org(self, unauthenticated_client):
144-
"""Login for user without any org should return tokens without current_org."""
145-
User.objects.create_user(email="[email protected]", password="testpass123")
146-
response = unauthenticated_client.post(
147-
self.url,
148-
{"email": "[email protected]", "password": "testpass123"},
149-
format="json",
150-
)
151-
assert response.status_code == status.HTTP_200_OK
152-
assert "access_token" in response.data
153-
assert "current_org" not in response.data
154-
155-
15621
@pytest.mark.django_db
15722
class TestMeView:
15823
"""Tests for GET /api/auth/me/"""

backend/common/tests/test_multitenancy.py

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -301,56 +301,6 @@ def test_switch_to_unauthorized_org_fails(self):
301301
self.assertEqual(response.status_code, 403)
302302

303303

304-
class TestLoginWithOrgContext(MultiTenancyBaseTestCase):
305-
"""Test login endpoint includes org context"""
306-
307-
def test_login_returns_current_org(self):
308-
"""Login should return current_org in response"""
309-
response = self.client.post(
310-
"/api/auth/login/", {"email": "[email protected]", "password": "testpass123"}
311-
)
312-
313-
self.assertEqual(response.status_code, 200)
314-
data = response.json()
315-
316-
self.assertIn("current_org", data)
317-
self.assertEqual(data["current_org"]["id"], str(self.org_a.id))
318-
319-
def test_login_with_specific_org(self):
320-
"""Login can specify which org to use"""
321-
# Give user_a access to org_b
322-
Profile.objects.create(
323-
user=self.user_a, org=self.org_b, role="USER", is_active=True
324-
)
325-
326-
response = self.client.post(
327-
"/api/auth/login/",
328-
{
329-
"email": "[email protected]",
330-
"password": "testpass123",
331-
"org_id": str(self.org_b.id),
332-
},
333-
)
334-
335-
self.assertEqual(response.status_code, 200)
336-
data = response.json()
337-
338-
self.assertEqual(data["current_org"]["id"], str(self.org_b.id))
339-
340-
def test_login_with_invalid_org_fails(self):
341-
"""Login with org user doesn't have access to should fail"""
342-
response = self.client.post(
343-
"/api/auth/login/",
344-
{
345-
"email": "[email protected]",
346-
"password": "testpass123",
347-
"org_id": str(self.org_b.id), # user_a doesn't have access
348-
},
349-
)
350-
351-
self.assertEqual(response.status_code, 403)
352-
353-
354304
class TestBaseOrgModel(TestCase):
355305
"""Test BaseOrgModel validation"""
356306

backend/common/urls.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from common.views.auth_views import (
44
GoogleIdTokenView,
55
GoogleOAuthCallbackView,
6-
LoginView,
76
MagicLinkRequestView,
87
MagicLinkVerifyView,
98
MeView,
@@ -35,7 +34,6 @@
3534
urlpatterns = [
3635
path("dashboard/", ApiHomeView.as_view()),
3736
# JWT Authentication endpoints for SvelteKit integration
38-
path("auth/login/", LoginView.as_view(), name="login"),
3937
path(
4038
"auth/refresh-token/",
4139
OrgAwareTokenRefreshView.as_view(),

0 commit comments

Comments
 (0)