Skip to content
Merged

M1 #574

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,075 changes: 1,075 additions & 0 deletions DESIGN_SYSTEM.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions backend/common/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,26 @@ def create(self, validated_data):
return super().create(validated_data)


class CommentUserSerializer(serializers.ModelSerializer):
"""Simplified user serializer for comments"""

user_details = serializers.SerializerMethodField()

class Meta:
model = Profile
fields = ("id", "user_details")

def get_user_details(self, obj):
if obj.user:
return {"email": obj.user.email, "profile_pic": obj.user.profile_pic}
return None


class LeadCommentSerializer(serializers.ModelSerializer):
"""Comment serializer with user details for display"""

commented_by = CommentUserSerializer(read_only=True)

class Meta:
model = Comment
fields = (
Expand Down
3 changes: 3 additions & 0 deletions backend/common/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rest_framework_simplejwt import views as jwt_views

from common.views.auth_views import (
GoogleIdTokenView,
GoogleOAuthCallbackView,
LoginView,
MeView,
Expand Down Expand Up @@ -46,6 +47,8 @@
path("auth/switch-org/", OrgSwitchView.as_view(), name="switch_org"),
# Google OAuth callback with PKCE (secure implementation)
path("auth/google/callback/", GoogleOAuthCallbackView.as_view()),
# Google ID token auth for mobile apps
path("auth/google/", GoogleIdTokenView.as_view(), name="google_id_token"),
# Organization and profile management
path("org/", OrgProfileCreateView.as_view()),
path("org/settings/", OrgSettingsView.as_view(), name="org_settings"),
Expand Down
99 changes: 99 additions & 0 deletions backend/common/views/auth_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,105 @@
)


class GoogleIdTokenView(APIView):
"""
Handle Google Sign-In from mobile apps using ID token.
Mobile app sends Google ID token, backend verifies and returns JWT.
"""

permission_classes = []
authentication_classes = []

@extend_schema(
tags=["auth"],
request=inline_serializer(
name="GoogleIdTokenRequest",
fields={"idToken": serializers.CharField()},
),
responses={
200: inline_serializer(
name="GoogleIdTokenResponse",
fields={
"JWTtoken": serializers.CharField(),
"user": serializers.DictField(),
"organizations": serializers.ListField(),
},
)
},
)
def post(self, request):
from django.utils import timezone

from google.oauth2 import id_token
from google.auth.transport import requests as google_requests

id_token_str = request.data.get("idToken")
if not id_token_str:
return Response(
{"error": "Missing idToken"},
status=status.HTTP_400_BAD_REQUEST,
)

# Verify the ID token with Google
try:
idinfo = id_token.verify_oauth2_token(
id_token_str,
google_requests.Request(),
settings.GOOGLE_CLIENT_ID,
)
email = idinfo.get("email")
picture = idinfo.get("picture", "")
except ValueError as e:
return Response(
{"error": f"Invalid token: {str(e)}"},

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 27 days ago

In general, to fix this issue you should avoid returning raw exception messages to the client. Instead, log the detailed error message (and optionally the stack trace) on the server, and send a generic, non-sensitive error message in the HTTP response. This preserves debuggability without exposing internal details or third‑party library messages.

For this specific code, the best minimal fix is:

  • Keep the try/except ValueError as e: block as is structurally.
  • Inside the except, add server-side logging of the exception (using Python’s standard logging module, which is already widely used and does not change existing functionality).
  • Replace {"error": f"Invalid token: {str(e)}"} with a generic message such as {"error": "Invalid token"} so the client no longer sees the raw exception text.

Concretely, in backend/common/views/auth_views.py:

  • Add import logging at the top alongside the other imports.
  • In the except ValueError as e: block in GoogleIdTokenView.post, log the exception using logging.exception (or logging.getLogger(__name__).exception) with a clear message, then return a generic error payload.

No behavior other than the specific error message content is changed: the status code remains 400, and the control flow stays identical.


Suggested changeset 1
backend/common/views/auth_views.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/backend/common/views/auth_views.py b/backend/common/views/auth_views.py
--- a/backend/common/views/auth_views.py
+++ b/backend/common/views/auth_views.py
@@ -1,6 +1,7 @@
 import json
 import secrets
 
+import logging
 import requests
 from django.conf import settings
 from django.contrib.auth.hashers import make_password
@@ -193,8 +194,9 @@
             email = idinfo.get("email")
             picture = idinfo.get("picture", "")
         except ValueError as e:
+            logging.exception("Failed to verify Google ID token")
             return Response(
-                {"error": f"Invalid token: {str(e)}"},
+                {"error": "Invalid token"},
                 status=status.HTTP_400_BAD_REQUEST,
             )
 
EOF
@@ -1,6 +1,7 @@
import json
import secrets

import logging
import requests
from django.conf import settings
from django.contrib.auth.hashers import make_password
@@ -193,8 +194,9 @@
email = idinfo.get("email")
picture = idinfo.get("picture", "")
except ValueError as e:
logging.exception("Failed to verify Google ID token")
return Response(
{"error": f"Invalid token: {str(e)}"},
{"error": "Invalid token"},
status=status.HTTP_400_BAD_REQUEST,
)

Copilot is powered by AI and may make mistakes. Always verify output.
status=status.HTTP_400_BAD_REQUEST,
)

if not email:
return Response(
{"error": "No email in token"},
status=status.HTTP_400_BAD_REQUEST,
)

# Get or create user
user, created = User.objects.get_or_create(
email=email,
defaults={
"profile_pic": picture,
"password": make_password(secrets.token_urlsafe(32)),
},
)
user.last_login = timezone.now()
user.save(update_fields=["last_login"])

# Get user's organizations
profiles = Profile.objects.filter(user=user).select_related("org")
organizations = [
{
"id": str(p.org.id),
"name": p.org.name,
"role": p.role,
}
for p in profiles
]

# Generate JWT token
token = OrgAwareRefreshToken.for_user_and_org(user, None)

return Response(
{
"JWTtoken": str(token.access_token),
"user": {
"id": str(user.id),
"email": user.email,
"name": email.split("@")[0],
"profileImage": user.profile_pic,
},
"organizations": organizations,
}
)


class LoginView(APIView):
"""
Login with email and password, returns JWT tokens
Expand Down
16 changes: 9 additions & 7 deletions backend/leads/views/lead_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,13 +482,15 @@ def post(self, request, pk, **kwargs):
},
status=status.HTTP_403_FORBIDDEN,
)
comment_serializer = CommentSerializer(data=params)
if comment_serializer.is_valid():
if params.get("comment"):
comment_serializer.save(
lead_id=self.lead_obj.id,
commented_by_id=self.request.profile.id,
)
if params.get("comment"):
lead_content_type = ContentType.objects.get_for_model(Lead)
Comment.objects.create(
content_type=lead_content_type,
object_id=self.lead_obj.id,
comment=params.get("comment"),
commented_by=self.request.profile,
org=self.request.profile.org,
)

if self.request.FILES.get("lead_attachment"):
attachment = Attachments()
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pytz==2025.2
requests==2.32.5
faker==33.1.0
python-dateutil>=2.8.0
google-auth>=2.0.0

# PDF Generation
weasyprint>=60.0
Expand Down
26 changes: 13 additions & 13 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,29 @@
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/js": "^9.39.1",
"@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.544.0",
"@eslint/js": "^9.39.2",
"@internationalized/date": "^3.10.1",
"@lucide/svelte": "^0.562.0",
"@sveltejs/adapter-node": "^5.4.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/kit": "^2.49.2",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"@tailwindcss/vite": "^4.1.18",
"bits-ui": "^2.14.4",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.13.1",
"globals": "^16.5.0",
"mode-watcher": "^1.1.0",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.45.5",
"svelte-check": "^4.3.4",
"svelte": "^5.46.1",
"svelte-check": "^4.3.5",
"svelte-sonner": "^1.0.7",
"tailwindcss": "^4.1.17",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.2.6"
"vite": "^7.3.0"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand All @@ -48,8 +48,8 @@
"axios": "^1.13.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"libphonenumber-js": "^1.12.31",
"svelte-dnd-action": "^0.9.49",
"libphonenumber-js": "^1.12.33",
"svelte-dnd-action": "^0.9.68",
"tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2",
"tw-animate-css": "^1.4.0"
Expand Down
Loading
Loading