Skip to content
Merged
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
8 changes: 0 additions & 8 deletions auth/.dockerignore

This file was deleted.

14 changes: 0 additions & 14 deletions auth/Dockerfile

This file was deleted.

Binary file modified auth/account/migrations/__pycache__/0001_initial.cpython-310.pyc
Binary file not shown.
151 changes: 27 additions & 124 deletions auth/account/utils.py
Original file line number Diff line number Diff line change
@@ -1,162 +1,65 @@
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, Email, To, Content
from django.conf import settings
import logging

logger = logging.getLogger(__name__)

def send_otp_email(email, otp, purpose="verification"):
"""
Send OTP email using SendGrid HTTP API (works on Render Free tier)
"""
try:
sendgrid_api_key = getattr(settings, 'SENDGRID_API_KEY', None)
if not sendgrid_api_key:
logger.error("SENDGRID_API_KEY not configured in settings")
return False

from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@tripsync.com')
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

if purpose == "verification":
subject = "Email Verification OTP - TripSync"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #4F46E5; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }}
.content {{ background-color: #f9fafb; padding: 30px; border: 1px solid #e5e7eb; }}
.otp-box {{ background-color: #fff; border: 2px dashed #4F46E5; padding: 20px; text-align: center; margin: 20px 0; border-radius: 5px; }}
.otp-code {{ font-size: 32px; font-weight: bold; color: #4F46E5; letter-spacing: 5px; }}
.footer {{ background-color: #f3f4f6; padding: 15px; text-align: center; font-size: 12px; color: #6b7280; border-radius: 0 0 5px 5px; }}
.warning {{ color: #dc2626; font-weight: bold; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Welcome to TripSync!</h1>
</div>
<div class="content">
<h2>Email Verification</h2>
<p>Hello,</p>
<p>Thank you for registering with TripSync - Your Travel Planning Companion!</p>
<p>To verify your email address, please use the following OTP code:</p>

<div class="otp-box">
<div class="otp-code">{otp}</div>
</div>

<p><strong>This OTP is valid for 10 minutes.</strong></p>
<p class="warning">⚠️ Please do not share this OTP with anyone.</p>
<p>If you did not request this verification, please ignore this email.</p>
</div>
<div class="footer">
<p>© 2024 TripSync. All rights reserved.</p>
<p>This is an automated email, please do not reply.</p>
</div>
</div>
<h2>Welcome to TripSync!</h2>
<p>Your email verification OTP is:</p>
<h1 style="color: #4CAF50; font-size: 32px; letter-spacing: 5px;">{otp}</h1>
<p>This OTP is valid for 10 minutes.</p>
<p><small>If you didn't request this, please ignore this email.</small></p>
<br>
<p>Best regards,<br>TripSync Team</p>
</body>
</html>
"""
plain_content = f"""
Hello,

Welcome to TripSync - Your Travel Planning Companion!

Your OTP for email verification is: {otp}

This OTP is valid for 10 minutes. Please do not share this OTP with anyone.

If you did not request this, please ignore this email.

Best regards,
TripSync Team
"""
else:
subject = "Password Reset OTP - TripSync"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #dc2626; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }}
.content {{ background-color: #f9fafb; padding: 30px; border: 1px solid #e5e7eb; }}
.otp-box {{ background-color: #fff; border: 2px dashed #dc2626; padding: 20px; text-align: center; margin: 20px 0; border-radius: 5px; }}
.otp-code {{ font-size: 32px; font-weight: bold; color: #dc2626; letter-spacing: 5px; }}
.footer {{ background-color: #f3f4f6; padding: 15px; text-align: center; font-size: 12px; color: #6b7280; border-radius: 0 0 5px 5px; }}
.warning {{ color: #dc2626; font-weight: bold; }}
.security-notice {{ background-color: #fef2f2; border-left: 4px solid #dc2626; padding: 10px; margin: 15px 0; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Password Reset Request</h1>
</div>
<div class="content">
<h2>Reset Your Password</h2>
<p>Hello,</p>
<p>You have requested to reset your password for your TripSync account.</p>
<p>Your OTP for password reset is:</p>

<div class="otp-box">
<div class="otp-code">{otp}</div>
</div>

<p><strong>This OTP is valid for 10 minutes.</strong></p>

<div class="security-notice">
<p class="warning">🔒 Security Notice</p>
<p>If you did not request this password reset, please secure your account immediately and contact our support team.</p>
</div>

<p class="warning">⚠️ Never share this OTP with anyone, including TripSync staff.</p>
</div>
<div class="footer">
<p>© 2024 TripSync. All rights reserved.</p>
<p>This is an automated email, please do not reply.</p>
</div>
</div>
<h2>Password Reset Request</h2>
<p>Your password reset OTP is:</p>
<h1 style="color: #FF5722; font-size: 32px; letter-spacing: 5px;">{otp}</h1>
<p>This OTP is valid for 10 minutes.</p>
<p><small>If you didn't request this, please secure your account immediately.</small></p>
<br>
<p>Best regards,<br>TripSync Team</p>
</body>
</html>
"""
plain_content = f"""
Hello,

You have requested to reset your password for your TripSync account.

Your OTP for password reset is: {otp}

This OTP is valid for 10 minutes. Please do not share this OTP with anyone.

If you did not request this, please secure your account immediately and contact support.

Best regards,
TripSync Team
"""

message = Mail(
from_email=Email(from_email),
to_emails=To(email),
from_email=settings.DEFAULT_FROM_EMAIL,
to_emails=email,
subject=subject,
plain_text_content=Content("text/plain", plain_content),
html_content=Content("text/html", html_content)
html_content=html_content
)

sg = SendGridAPIClient(sendgrid_api_key)
sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
response = sg.send(message)

if response.status_code in [200, 201, 202]:
logger.info(f"OTP email sent successfully to {email} via SendGrid")
logger.info(f"OTP email sent successfully to {email} via SendGrid API")
return True
else:
logger.error(f"SendGrid returned status code {response.status_code}")
logger.error(f"SendGrid API returned status {response.status_code}")
return False

except Exception as e:
logger.error(f"Error sending email via SendGrid: {str(e)}")
import traceback
logger.error(traceback.format_exc())
logger.error(f"✗ SendGrid API error for {email}: {str(e)}")
logger.exception("Full traceback:")
return False
6 changes: 2 additions & 4 deletions auth/auth/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""

import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'auth.settings')

from chat.routing import websocket_urlpatterns

application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket':AllowedHostsOriginValidator( AuthMiddlewareStack(
'websocket':AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
)),
),
})
16 changes: 11 additions & 5 deletions auth/auth/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,25 @@

ASGI_APPLICATION = 'auth.asgi.application'

REDIS_HOST = config('REDIS_HOST', default='127.0.0.1')
REDIS_PORT = config('REDIS_PORT', default=6379, cast=int)

CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', 6379)],
'hosts': [(REDIS_HOST, REDIS_PORT)],
},
},
}

database_url = os.environ.get("DATABASE_URL","postgresql://arnav_db_user:FupuhQQOsNLTkJKNEp2EA6Q8Kia7hvCu@dpg-d3mk6mvdiees73c95o2g-a.singapore-postgres.render.com/arnav_db")
if database_url.startswith("postgres://"):
database_url = database_url.replace("postgres://", "postgresql://", 1)
DATABASES = {'default': dj_database_url.parse(database_url, conn_max_age=600, ssl_require=True)}
database_url = os.environ.get("DATABASE_URL")
if database_url:
if database_url.startswith("postgres://"):
database_url = database_url.replace("postgres://", "postgresql://", 1)
DATABASES = {'default': dj_database_url.parse(database_url, conn_max_age=600, ssl_require=True)}
else:
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3'}}

REST_FRAMEWORK = {'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework_simplejwt.authentication.JWTAuthentication',), 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer']}

Expand Down
5 changes: 0 additions & 5 deletions auth/auth/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.http import JsonResponse
from django.conf import settings
from django.conf.urls.static import static

@api_view(['GET'])
def root_redirect(request):
Expand All @@ -27,6 +25,3 @@ def health_check(request):
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root = settings.STATIC_URL)
2 changes: 1 addition & 1 deletion auth/chat/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from .models import *

admin.site.register(Conversation)
admin.site.register(Message)
admin.site.register(Message)
33 changes: 17 additions & 16 deletions auth/chat/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings
from urllib.parse import parse_qs
from channels.db import database_sync_to_async
from django.utils import timezone

class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
Expand Down Expand Up @@ -49,22 +50,6 @@ async def connect(self):
}
)

async def disconnect(self, close_code):
if hasattr(self, 'room_group_name') and hasattr(self, 'user'):
user_data = await self.get_user_data(self.user)
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_status',
'user': user_data,
'status': 'offline',
}
)
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)

async def receive(self, text_data):
try:
data = json.loads(text_data)
Expand All @@ -88,6 +73,22 @@ async def receive(self, text_data):
print(f"Error in receive: {e}")
await self.send_error("Internal error")

async def disconnect(self, close_code):
if hasattr(self, 'room_group_name') and hasattr(self, 'user'):
user_data = await self.get_user_data(self.user)
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_status',
'user': user_data,
'status': 'offline',
}
)
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)

async def handle_chat_message(self, data):
message_content = data.get('message', '').strip()

Expand Down
25 changes: 24 additions & 1 deletion auth/chat/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-10-19 16:37
# Generated by Django 5.2.7 on 2025-10-24 11:43

import django.db.models.deletion
from django.conf import settings
Expand All @@ -18,18 +18,41 @@ class Migration(migrations.Migration):
name='Conversation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=255, null=True)),
('is_group', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('participants', models.ManyToManyField(related_name='conversations', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-updated_at'],
},
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('is_read', models.BooleanField(default=False)),
('edited_at', models.DateTimeField(blank=True, null=True)),
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chat.conversation')),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['timestamp'],
},
),
migrations.AddIndex(
model_name='conversation',
index=models.Index(fields=['-updated_at'], name='chat_conver_updated_1f6ffe_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['conversation', 'timestamp'], name='chat_messag_convers_cd68de_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['conversation', '-timestamp'], name='chat_messag_convers_dca7ce_idx'),
),
]
Loading