diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py index e69de29bb..a500fab34 100644 --- a/backend/backend/__init__.py +++ b/backend/backend/__init__.py @@ -0,0 +1,2 @@ +from .celery import app as celery_app +_all_ = ('celery_app',) \ No newline at end of file diff --git a/backend/backend/celery.py b/backend/backend/celery.py new file mode 100644 index 000000000..badc78c56 --- /dev/null +++ b/backend/backend/celery.py @@ -0,0 +1,12 @@ +# backend/celery.py + +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') + +app = Celery('backend') + +app.config_from_object('django.conf:settings', namespace='CELERY') + +app.autodiscover_tasks() \ No newline at end of file diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 9de4f024a..b39a23562 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -24,8 +24,14 @@ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] -FRONTEND_URL = os.environ["FRONTEND_URL"] + +from decouple import config +SECRET_KEY=config("DJANGO_SECRET_KEY") +FRONTEND_URL=config("FRONTEND_URL", default="http://127.0.0.1:3000") + + +# SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] +# FRONTEND_URL = os.environ["FRONTEND_URL"] # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -47,8 +53,17 @@ "authentication", "chat", "gpt", + #task 3 + 'django_filters' ] +# task 3 +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, +} + MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -85,9 +100,13 @@ # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'bhavani', + 'USER': 'postgres', + 'PASSWORD': '123456', + 'HOST': 'localhost', + 'PORT': '5432' } } @@ -149,3 +168,25 @@ SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True CSRF_COOKIE_SAMESITE = "None" + +# CELERY SETTINGS +CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' + +# CELERY BEAT SETTINGS +INSTALLED_APPS += ['django_celery_beat'] + +#task 4 +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { 'class': 'logging.StreamHandler' }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} + diff --git a/backend/backend/urls.py b/backend/backend/urls.py index fa154c7fb..6470161da 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -17,4 +17,6 @@ def root_view(request): path("gpt/", include("gpt.urls")), path("auth/", include("authentication.urls")), path("", root_view), + #task 3 + path('api/',include('chat.urls')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/backend/chat/admin.py b/backend/chat/admin.py index a4e7d15fc..e4828340c 100644 --- a/backend/chat/admin.py +++ b/backend/chat/admin.py @@ -4,7 +4,35 @@ from chat.models import Conversation, Message, Role, Version - +#task 3.2 +from .models import FileUpload +from django import forms +from django.contrib import messages +from django.core.exceptions import ValidationError + +# Custom form to show only user and file (hide hash/size in input) +class FileUploadForm(forms.ModelForm): + class Meta: + model = FileUpload + fields = ['user', 'file'] # only show user and file fields +@admin.register(FileUpload) +class FileUploadAdmin(admin.ModelAdmin): + form = FileUploadForm + list_display = ("id", "file_name", "file_size", "file_hash", "uploaded_at", "user") + readonly_fields = ("file_name", "file_size", "file_hash", "uploaded_at") + + def save_model(self, request, obj, form, change): + try: + obj.save() # Triggers full_clean() with validation + self.message_user(request, "File uploaded successfully.", level=messages.SUCCESS) + except ValidationError as e: + self.message_user( + request, + f"Upload failed: {e.messages[0]}", + level=messages.WARNING + ) + +# task3.2 end-- class RoleAdmin(NestedModelAdmin): list_display = ["id", "name"] @@ -51,7 +79,7 @@ def queryset(self, request, queryset): class ConversationAdmin(NestedModelAdmin): actions = ["undelete_selected", "soft_delete_selected"] inlines = [VersionInline] - list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user") + list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user","summary") list_filter = (DeletedListFilter,) ordering = ("-modified_at",) diff --git a/backend/chat/apps.py b/backend/chat/apps.py index 5f75238d2..ff459da89 100644 --- a/backend/chat/apps.py +++ b/backend/chat/apps.py @@ -4,3 +4,6 @@ class ChatConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "chat" + + def ready(self): + import chat.signals # 👈 Load signals \ No newline at end of file diff --git a/backend/chat/management/commands/cleanup_old_conversations.py b/backend/chat/management/commands/cleanup_old_conversations.py new file mode 100644 index 000000000..c9d7d0b6a --- /dev/null +++ b/backend/chat/management/commands/cleanup_old_conversations.py @@ -0,0 +1,20 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from chat.models import Conversation +from datetime import timedelta + +class Command(BaseCommand): + help = 'Soft-deletes conversations older than 30 days' + + def handle(self, *args, **kwargs): + days = 30 + cutoff_date = timezone.now() - timedelta(days=days) + + deleted_count = Conversation.objects.filter( + created_at__lt=cutoff_date, + deleted_at__isnull=True + ).update(deleted_at=timezone.now()) + + self.stdout.write( + self.style.SUCCESS(f"{deleted_count} conversations soft-deleted.") + ) \ No newline at end of file diff --git a/backend/chat/migrations/0002_conversation_summary.py b/backend/chat/migrations/0002_conversation_summary.py new file mode 100644 index 000000000..ac8620a13 --- /dev/null +++ b/backend/chat/migrations/0002_conversation_summary.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-07-05 09:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='conversation', + name='summary', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/backend/chat/migrations/0003_fileupload.py b/backend/chat/migrations/0003_fileupload.py new file mode 100644 index 000000000..013d7dc4e --- /dev/null +++ b/backend/chat/migrations/0003_fileupload.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.4 on 2025-07-07 16:53 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0002_conversation_summary'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='FileUpload', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='uploads/')), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('file_name', models.CharField(max_length=255)), + ('file_size', models.PositiveIntegerField()), + ('file_hash', models.CharField(max_length=64, unique=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/chat/migrations/0004_alter_fileupload_file_hash_and_more.py b/backend/chat/migrations/0004_alter_fileupload_file_hash_and_more.py new file mode 100644 index 000000000..40aa1b1a6 --- /dev/null +++ b/backend/chat/migrations/0004_alter_fileupload_file_hash_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.4 on 2025-07-07 17:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0003_fileupload'), + ] + + operations = [ + migrations.AlterField( + model_name='fileupload', + name='file_hash', + field=models.CharField(blank=True, max_length=64, unique=True), + ), + migrations.AlterField( + model_name='fileupload', + name='file_name', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AlterField( + model_name='fileupload', + name='file_size', + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/backend/chat/migrations/0005_filelog_alter_fileupload_file_hash_and_more.py b/backend/chat/migrations/0005_filelog_alter_fileupload_file_hash_and_more.py new file mode 100644 index 000000000..6fd48a78b --- /dev/null +++ b/backend/chat/migrations/0005_filelog_alter_fileupload_file_hash_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 5.2.4 on 2025-07-08 11:52 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0004_alter_fileupload_file_hash_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='FileLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file_name', models.CharField(max_length=255)), + ('action', models.CharField(choices=[('UPLOAD', 'Upload'), ('DELETE', 'Delete'), ('ACCESS', 'Access')], max_length=10)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.AlterField( + model_name='fileupload', + name='file_hash', + field=models.CharField(max_length=64), + ), + migrations.AlterField( + model_name='fileupload', + name='file_name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='fileupload', + name='file_size', + field=models.PositiveIntegerField(default=0), + preserve_default=False, + ), + migrations.AddConstraint( + model_name='fileupload', + constraint=models.UniqueConstraint(fields=('user', 'file_hash'), name='unique_user_file_hash'), + ), + migrations.AddField( + model_name='filelog', + name='user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/chat/models.py b/backend/chat/models.py index 242788f14..5d47ab879 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -3,6 +3,12 @@ from django.db import models from authentication.models import CustomUser +# task-3 +import hashlib +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +import hashlib +from django.contrib.auth.models import User class Role(models.Model): @@ -22,6 +28,7 @@ class Conversation(models.Model): ) deleted_at = models.DateTimeField(null=True, blank=True) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + summary = models.TextField(blank=True, null=True) def __str__(self): return self.title @@ -32,6 +39,48 @@ def version_count(self): version_count.short_description = "Number of versions" +# task-3 file uploadwith duplicate check +User = get_user_model() + +class FileUpload(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + file = models.FileField(upload_to='uploads/') + uploaded_at = models.DateTimeField(auto_now_add=True) + file_name = models.CharField(max_length=255) + file_size = models.PositiveIntegerField() + file_hash = models.CharField(max_length=64) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['user', 'file_hash'], name='unique_user_file_hash') + ] + + def clean(self): + # Check for duplicate files for the same user + if FileUpload.objects.filter(user=self.user, file_hash=self.file_hash).exclude(pk=self.pk).exists(): + raise ValidationError("Duplicate file. Already uploaded by this user.") + + def save(self, *args, **kwargs): + # Only calculate hash if not already set + if not self.file_hash: + hasher = hashlib.sha256() + for chunk in self.file.chunks(): + hasher.update(chunk) + self.file_hash = hasher.hexdigest() + + # Set file name and size automatically + self.file_name = self.file.name + self.file_size = self.file.size + + # Run validation and save + self.full_clean() # Calls clean() + super().save(*args, **kwargs) + + def _str_(self): + return f"{self.user.email} - {self.file_name}" + + + class Version(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) conversation = models.ForeignKey("Conversation", related_name="versions", on_delete=models.CASCADE) @@ -63,3 +112,17 @@ def save(self, *args, **kwargs): def __str__(self): return f"{self.role}: {self.content[:20]}..." + +class FileLog(models.Model): + ACTION_CHOICES= [ + ('UPLOAD', 'Upload'), + ('DELETE', 'Delete'), + ('ACCESS', 'Access'), +] + user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True) + file_name = models.CharField(max_length=255) + action = models.CharField(max_length=10, choices=ACTION_CHOICES) + timestamp = models.DateTimeField(auto_now_add=True) + + def _str_(self): + return f"{self.timestamp} - {self.user} - {self.action} - {self.file_name}" \ No newline at end of file diff --git a/backend/chat/serializers.py b/backend/chat/serializers.py index 0c721c061..78b03a502 100644 --- a/backend/chat/serializers.py +++ b/backend/chat/serializers.py @@ -1,6 +1,9 @@ from django.core.exceptions import ValidationError from django.utils import timezone +# task-3 file upload with duplicate check +import hashlib from rest_framework import serializers +from .models import FileUpload from chat.models import Conversation, Message, Role, Version @@ -8,6 +11,31 @@ def should_serialize(validated_data, field_name) -> bool: if validated_data.get(field_name) is not None: return True + +# Task -3 changes +class ConversationSummarySerializer(serializers.ModelSerializer): + class Meta: + model = Conversation + fields = ['id', 'title', 'summary', 'created_at'] + +# task-3 file upload with duplicate check +class UploadedFileSerializer(serializers.ModelSerializer): + class Meta: + model = FileUpload + fields = '__all__' + + def create(self, validated_data): + file = validated_data['file'] + content = file.read() + file.seek(0) + checksum = hashlib.sha256(content).hexdigest() + + if FileUpload.objects.filter(checksum=checksum).exists(): + raise serializers.ValidationError("Duplicate file detected.") + + validated_data['checksum'] = checksum + validated_data['filename'] = file.name + return super().create(validated_data) class TitleSerializer(serializers.Serializer): @@ -149,4 +177,4 @@ def update(self, instance, validated_data): if version_serializer.is_valid(): version_serializer.save(conversation=instance) - return instance + return instance \ No newline at end of file diff --git a/backend/chat/signals.py b/backend/chat/signals.py new file mode 100644 index 000000000..5e5d82852 --- /dev/null +++ b/backend/chat/signals.py @@ -0,0 +1,28 @@ +# chat/signals.py +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Conversation, Message +import openai + +@receiver(post_save, sender=Conversation) +def generate_summary(sender, instance, created, **kwargs): + if created or not instance.summary: + messages = Message.objects.filter(conversation=instance).order_by('created_at') + text = "\n".join([msg.content for msg in messages]) + + if text.strip(): + try: + # Use OpenAI or a simple logic to summarize + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": "Summarize this conversation briefly."}, + {"role": "user", "content": text[:3000]} # avoid token limit + ] + ) + summary = response.choices[0].message.content.strip() + except Exception as e: + summary = "Summary generation failed." + + instance.summary = summary + instance.save(update_fields=["summary"]) \ No newline at end of file diff --git a/backend/chat/tasks.py b/backend/chat/tasks.py new file mode 100644 index 000000000..ab4eb5cc4 --- /dev/null +++ b/backend/chat/tasks.py @@ -0,0 +1,10 @@ +from celery import shared_task +from django.utils import timezone +from chat.models import Conversation +from datetime import timedelta + +@shared_task +def cleanup_old_conversations(): + cutoff = timezone.now() - timedelta(days=30) + deleted = Conversation.objects.filter(created_at__lt=cutoff, deleted_at__isnull=True).update(deleted_at=timezone.now()) + return f"{deleted} conversations soft-deleted." \ No newline at end of file diff --git a/backend/chat/urls.py b/backend/chat/urls.py index bd8ceadc0..71d3f2421 100644 --- a/backend/chat/urls.py +++ b/backend/chat/urls.py @@ -1,6 +1,11 @@ from django.urls import path from chat import views +# task-3 import +from .views import ConversationSummaryListView +from .views import FileUploadView +from .views import FileListView +# from .views import FileDeleteView urlpatterns = [ path("", views.chat_root_view, name="chat_root_view"), @@ -19,4 +24,14 @@ ), path("conversations//delete/", views.conversation_soft_delete, name="conversation_delete"), path("versions//add_message/", views.version_add_message, name="version_add_message"), -] + # task 3 + path('summary-conversations/', ConversationSummaryListView.as_view(), name='summary-conversations'), + path('upload/', FileUploadView.as_view(), name='file-upload'), + path('files/', FileListView.as_view(), name='file-list'), + # path('files//delete', FileDeleteView.as_view(), name='file-delete'), + # task-4 + path('delete//', views.delete_uploaded_file, name='delete-file'), + path('conversation//summary/', views.get_conversation_summary, name='conversation-summary'), + +path('conversation//', views.conversation_manage, name='conversation-manage'), +] \ No newline at end of file diff --git a/backend/chat/views.py b/backend/chat/views.py index 0d18f7a69..6f71d63c3 100644 --- a/backend/chat/views.py +++ b/backend/chat/views.py @@ -1,12 +1,58 @@ from django.contrib.auth.decorators import login_required from django.utils import timezone from rest_framework import status -from rest_framework.decorators import api_view -from rest_framework.response import Response +from rest_framework.decorators import api_view, permission_classes + + from chat.models import Conversation, Message, Version from chat.serializers import ConversationSerializer, MessageSerializer, TitleSerializer, VersionSerializer from chat.utils.branching import make_branched_conversation +# task-3 imports-1.0 Destroyapi view -3.4 delete file end point +from rest_framework.generics import ListAPIView, DestroyAPIView +from rest_framework.filters import SearchFilter +from django_filters.rest_framework import DjangoFilterBackend +from chat.serializers import ConversationSummarySerializer + +# task-3.2file upload with duplicate check +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from .serializers import UploadedFileSerializer +# task-3.3 List Uploaded Files with Metadata +from .models import FileUpload + +class FileUploadView(APIView): + def post(self, request): + serializer = UploadedFileSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=201) + return Response(serializer.errors, status=400) + +class FileListView(ListAPIView): + queryset = FileUpload.objects.all().order_by('-uploaded_at') + serializer_class = UploadedFileSerializer + + +# task-3.4 Delete file endpoint +# class FileDeleteView(DestroyAPIView): + queryset = FileUpload.objects.all() + serializer_class = UploadedFileSerializer + lookup_field = 'id' + + +# task 3-1.0 +class ConversationSummaryListView(ListAPIView): + queryset = Conversation.objects.filter(deleted_at__isnull=True).order_by('-created_at') + serializer_class = ConversationSummarySerializer + filter_backends = [SearchFilter, DjangoFilterBackend] + search_fields = ['title', 'summary'] + + def get_queryset(self): + user = self.request.user + return Conversation.objects.filter(user=user, deleted_at__isnull=True).order_by('-created_at') +# end---------------------- @api_view(["GET"]) @@ -230,3 +276,91 @@ def version_add_message(request, pk): status=status.HTTP_201_CREATED, ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +# task-4 +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated + + +from .models import FileUpload +from .serializers import UploadedFileSerializer + +import logging +from .models import FileLog + +logger = logging.getLogger(__name__) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def upload_file_view(request): + if not request.user.is_staff: + return Response({'detail': 'Permission denied. Admins only.'}, status=403) + + if 'file' not in request.FILES: + return Response({'error': 'No file provided.'}, status=400) + + file_obj = request.FILES['file'] + uploaded_file =FileUpload.objects.create(user=request.user, file=file_obj) + + # ✅ Log to console + logger.info(f"{request.user.email} uploaded file: {file_obj.name}") + + # ✅ Log to DB + FileLog.objects.create(user=request.user, file_name=file_obj.name, action='UPLOAD') + + return Response({'message': 'File uploaded successfully.'}, status=201) + +@api_view(['DELETE']) +@permission_classes([IsAuthenticated]) +def delete_uploaded_file(request, file_id): + try: + uploaded_file = FileUpload.objects.get(id=file_id) + file_name = uploaded_file.file.name + uploaded_file.delete() + + # ✅ Create a FileLog entry + FileLog.objects.create( + user=request.user, + action='DELETE', + file_name=file_name + ) + + logger.info(f"{request.user.email} deleted file: {file_name}") + return Response({'message': 'File deleted successfully.'}, status=200) + except FileUpload.DoesNotExist: + return Response({'error': 'File not found.'}, status=404) + + +from django.core.cache import cache +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from chat.models import Conversation + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_conversation_summary(request, id): + cache_key = f"conversation_summary_{id}" + summary = cache.get(cache_key) + + if summary is None: + try: + conversation = Conversation.objects.get(id=id, user=request.user) + except Conversation.DoesNotExist: + return Response({"error": "Conversation not found"}, status=404) + + summary = conversation.summary or "No summary available" + cache.set(cache_key, summary, timeout=3600) # cache for 1 hour + + return Response({"id": id, "summary": summary}) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { 'class': 'logging.StreamHandler' }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} \ No newline at end of file diff --git a/backend/uploads/Certificate.jpeg b/backend/uploads/Certificate.jpeg new file mode 100644 index 000000000..785879d48 Binary files /dev/null and b/backend/uploads/Certificate.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-01_at_4.28.39_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-01_at_4.28.39_PM.jpeg new file mode 100644 index 000000000..81f6a9647 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-01_at_4.28.39_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_3HA84ve.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_3HA84ve.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_3HA84ve.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_5T40AvK.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_5T40AvK.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_5T40AvK.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_5wmqwDI.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_5wmqwDI.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_5wmqwDI.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_EyFBJq4.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_EyFBJq4.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_EyFBJq4.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_HfPNgf6.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_HfPNgf6.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_HfPNgf6.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_KO28SKp.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_KO28SKp.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_KO28SKp.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_Mkfermx.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_Mkfermx.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_Mkfermx.39.45_PM.jpeg differ diff --git a/backend/uploads/WhatsApp_Image_2025-07-04_at_12_ef1vVQd.39.45_PM.jpeg b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_ef1vVQd.39.45_PM.jpeg new file mode 100644 index 000000000..31f82c319 Binary files /dev/null and b/backend/uploads/WhatsApp_Image_2025-07-04_at_12_ef1vVQd.39.45_PM.jpeg differ