Skip to content

Commit cc28cfb

Browse files
authored
mobile app
M1
2 parents c39e4e4 + 666c59a commit cc28cfb

File tree

200 files changed

+38014
-1979
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

200 files changed

+38014
-1979
lines changed

DESIGN_SYSTEM.md

Lines changed: 1075 additions & 0 deletions
Large diffs are not rendered by default.

backend/common/serializer.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,26 @@ def create(self, validated_data):
202202
return super().create(validated_data)
203203

204204

205+
class CommentUserSerializer(serializers.ModelSerializer):
206+
"""Simplified user serializer for comments"""
207+
208+
user_details = serializers.SerializerMethodField()
209+
210+
class Meta:
211+
model = Profile
212+
fields = ("id", "user_details")
213+
214+
def get_user_details(self, obj):
215+
if obj.user:
216+
return {"email": obj.user.email, "profile_pic": obj.user.profile_pic}
217+
return None
218+
219+
205220
class LeadCommentSerializer(serializers.ModelSerializer):
221+
"""Comment serializer with user details for display"""
222+
223+
commented_by = CommentUserSerializer(read_only=True)
224+
206225
class Meta:
207226
model = Comment
208227
fields = (

backend/common/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from rest_framework_simplejwt import views as jwt_views
33

44
from common.views.auth_views import (
5+
GoogleIdTokenView,
56
GoogleOAuthCallbackView,
67
LoginView,
78
MeView,
@@ -46,6 +47,8 @@
4647
path("auth/switch-org/", OrgSwitchView.as_view(), name="switch_org"),
4748
# Google OAuth callback with PKCE (secure implementation)
4849
path("auth/google/callback/", GoogleOAuthCallbackView.as_view()),
50+
# Google ID token auth for mobile apps
51+
path("auth/google/", GoogleIdTokenView.as_view(), name="google_id_token"),
4952
# Organization and profile management
5053
path("org/", OrgProfileCreateView.as_view()),
5154
path("org/settings/", OrgSettingsView.as_view(), name="org_settings"),

backend/common/views/auth_views.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,105 @@ def post(self, request):
144144
)
145145

146146

147+
class GoogleIdTokenView(APIView):
148+
"""
149+
Handle Google Sign-In from mobile apps using ID token.
150+
Mobile app sends Google ID token, backend verifies and returns JWT.
151+
"""
152+
153+
permission_classes = []
154+
authentication_classes = []
155+
156+
@extend_schema(
157+
tags=["auth"],
158+
request=inline_serializer(
159+
name="GoogleIdTokenRequest",
160+
fields={"idToken": serializers.CharField()},
161+
),
162+
responses={
163+
200: inline_serializer(
164+
name="GoogleIdTokenResponse",
165+
fields={
166+
"JWTtoken": serializers.CharField(),
167+
"user": serializers.DictField(),
168+
"organizations": serializers.ListField(),
169+
},
170+
)
171+
},
172+
)
173+
def post(self, request):
174+
from django.utils import timezone
175+
176+
from google.oauth2 import id_token
177+
from google.auth.transport import requests as google_requests
178+
179+
id_token_str = request.data.get("idToken")
180+
if not id_token_str:
181+
return Response(
182+
{"error": "Missing idToken"},
183+
status=status.HTTP_400_BAD_REQUEST,
184+
)
185+
186+
# Verify the ID token with Google
187+
try:
188+
idinfo = id_token.verify_oauth2_token(
189+
id_token_str,
190+
google_requests.Request(),
191+
settings.GOOGLE_CLIENT_ID,
192+
)
193+
email = idinfo.get("email")
194+
picture = idinfo.get("picture", "")
195+
except ValueError as e:
196+
return Response(
197+
{"error": f"Invalid token: {str(e)}"},
198+
status=status.HTTP_400_BAD_REQUEST,
199+
)
200+
201+
if not email:
202+
return Response(
203+
{"error": "No email in token"},
204+
status=status.HTTP_400_BAD_REQUEST,
205+
)
206+
207+
# Get or create user
208+
user, created = User.objects.get_or_create(
209+
email=email,
210+
defaults={
211+
"profile_pic": picture,
212+
"password": make_password(secrets.token_urlsafe(32)),
213+
},
214+
)
215+
user.last_login = timezone.now()
216+
user.save(update_fields=["last_login"])
217+
218+
# Get user's organizations
219+
profiles = Profile.objects.filter(user=user).select_related("org")
220+
organizations = [
221+
{
222+
"id": str(p.org.id),
223+
"name": p.org.name,
224+
"role": p.role,
225+
}
226+
for p in profiles
227+
]
228+
229+
# Generate JWT token
230+
token = OrgAwareRefreshToken.for_user_and_org(user, None)
231+
232+
return Response(
233+
{
234+
"JWTtoken": str(token.access_token),
235+
"user": {
236+
"id": str(user.id),
237+
"email": user.email,
238+
"name": email.split("@")[0],
239+
"profileImage": user.profile_pic,
240+
},
241+
"organizations": organizations,
242+
}
243+
)
244+
245+
147246
class LoginView(APIView):
148247
"""
149248
Login with email and password, returns JWT tokens

backend/leads/views/lead_views.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -482,13 +482,15 @@ def post(self, request, pk, **kwargs):
482482
},
483483
status=status.HTTP_403_FORBIDDEN,
484484
)
485-
comment_serializer = CommentSerializer(data=params)
486-
if comment_serializer.is_valid():
487-
if params.get("comment"):
488-
comment_serializer.save(
489-
lead_id=self.lead_obj.id,
490-
commented_by_id=self.request.profile.id,
491-
)
485+
if params.get("comment"):
486+
lead_content_type = ContentType.objects.get_for_model(Lead)
487+
Comment.objects.create(
488+
content_type=lead_content_type,
489+
object_id=self.lead_obj.id,
490+
comment=params.get("comment"),
491+
commented_by=self.request.profile,
492+
org=self.request.profile.org,
493+
)
492494

493495
if self.request.FILES.get("lead_attachment"):
494496
attachment = Attachments()

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pytz==2025.2
1717
requests==2.32.5
1818
faker==33.1.0
1919
python-dateutil>=2.8.0
20+
google-auth>=2.0.0
2021

2122
# PDF Generation
2223
weasyprint>=60.0

frontend/package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,29 @@
1515
},
1616
"devDependencies": {
1717
"@eslint/compat": "^2.0.0",
18-
"@eslint/js": "^9.39.1",
19-
"@internationalized/date": "^3.10.0",
20-
"@lucide/svelte": "^0.544.0",
18+
"@eslint/js": "^9.39.2",
19+
"@internationalized/date": "^3.10.1",
20+
"@lucide/svelte": "^0.562.0",
2121
"@sveltejs/adapter-node": "^5.4.0",
22-
"@sveltejs/kit": "^2.49.1",
22+
"@sveltejs/kit": "^2.49.2",
2323
"@sveltejs/vite-plugin-svelte": "^6.2.1",
2424
"@tailwindcss/typography": "^0.5.19",
25-
"@tailwindcss/vite": "^4.1.17",
25+
"@tailwindcss/vite": "^4.1.18",
2626
"bits-ui": "^2.14.4",
27-
"eslint": "^9.39.1",
27+
"eslint": "^9.39.2",
2828
"eslint-config-prettier": "^10.1.8",
2929
"eslint-plugin-svelte": "^3.13.1",
3030
"globals": "^16.5.0",
3131
"mode-watcher": "^1.1.0",
3232
"prettier": "^3.7.4",
33-
"prettier-plugin-svelte": "^3.4.0",
33+
"prettier-plugin-svelte": "^3.4.1",
3434
"prettier-plugin-tailwindcss": "^0.7.2",
35-
"svelte": "^5.45.5",
36-
"svelte-check": "^4.3.4",
35+
"svelte": "^5.46.1",
36+
"svelte-check": "^4.3.5",
3737
"svelte-sonner": "^1.0.7",
38-
"tailwindcss": "^4.1.17",
38+
"tailwindcss": "^4.1.18",
3939
"typescript": "^5.9.3",
40-
"vite": "^7.2.6"
40+
"vite": "^7.3.0"
4141
},
4242
"pnpm": {
4343
"onlyBuiltDependencies": [
@@ -48,8 +48,8 @@
4848
"axios": "^1.13.2",
4949
"clsx": "^2.1.1",
5050
"date-fns": "^4.1.0",
51-
"libphonenumber-js": "^1.12.31",
52-
"svelte-dnd-action": "^0.9.49",
51+
"libphonenumber-js": "^1.12.33",
52+
"svelte-dnd-action": "^0.9.68",
5353
"tailwind-merge": "^3.4.0",
5454
"tailwind-variants": "^3.2.2",
5555
"tw-animate-css": "^1.4.0"

0 commit comments

Comments
 (0)