From b9238599982d5a8dd74d84bd7e1c981c540c8ff0 Mon Sep 17 00:00:00 2001 From: HanilJain Date: Sun, 30 Jun 2024 03:31:16 +0530 Subject: [PATCH 01/11] commit 1 --- Dockerfile | 5 +- blt/asgi.py | 28 ++++ blt/settings.py | 13 ++ blt/urls.py | 2 + docker-compose.yml | 17 ++- notification_app/__init__.py | 0 notification_app/admin.py | 5 + notification_app/apps.py | 6 + notification_app/consumers.py | 33 +++++ notification_app/migrations/__init__.py | 0 notification_app/models.py | 7 + notification_app/routing.py | 7 + notification_app/tests.py | 1 + notification_app/views.py | 1 + pyproject.toml | 2 +- website/api/views.py | 28 ++++ website/templates/includes/header.html | 177 ++++++++++++++++++++++++ website/templates/new_home.html | 10 ++ website/views.py | 21 +++ 19 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 blt/asgi.py create mode 100644 notification_app/__init__.py create mode 100644 notification_app/admin.py create mode 100644 notification_app/apps.py create mode 100644 notification_app/consumers.py create mode 100644 notification_app/migrations/__init__.py create mode 100644 notification_app/models.py create mode 100644 notification_app/routing.py create mode 100644 notification_app/tests.py create mode 100644 notification_app/views.py diff --git a/Dockerfile b/Dockerfile index fcaaacda1..c9c4a124d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,15 +16,16 @@ RUN apt-get update && apt-get install -y \ libmemcached11 \ libmemcachedutil2 \ libmemcached-dev \ - libz-dev + libz-dev \ + redis RUN pip install poetry RUN poetry config virtualenvs.create false +RUN poetry lock --no-update RUN poetry install RUN python manage.py migrate RUN python manage.py loaddata website/fixtures/initial_data.json # RUN python manage.py collectstatic RUN python manage.py initsuperuser - diff --git a/blt/asgi.py b/blt/asgi.py new file mode 100644 index 000000000..9752f88ce --- /dev/null +++ b/blt/asgi.py @@ -0,0 +1,28 @@ +""" +ASGI config for channels_celery_project project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blt.settings") +django.setup() + +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from django.core.asgi import get_asgi_application + +from notification_app.routing import websocket_urlpatterns + +application = ProtocolTypeRouter( + { + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)), + } +) diff --git a/blt/settings.py b/blt/settings.py index 1108bf4cf..f7fead42f 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -67,6 +67,8 @@ # Application definition INSTALLED_APPS = ( + "notification_app", + "daphne", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -197,6 +199,16 @@ WSGI_APPLICATION = "blt.wsgi.application" +ASGI_APPLICATION = "blt.asgi.application" + +CHANNEL_LAYER = { + "default": { + "BACKEND": "channels_redis.core.RedisChaneelLayer", + "CONFIG": { + "hosts": [("cache", 8000)], + }, + } +} # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases @@ -317,6 +329,7 @@ "0.0.0.0", "blt.owasp.org", "." + DOMAIN_NAME_PREVIOUS, + "blt.onrender.com", ] # Static files (CSS, JavaScript, Images) diff --git a/blt/urls.py b/blt/urls.py index 2f220cdb8..611ae8c85 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -22,6 +22,7 @@ BugHuntApiViewsetV2, CompanyViewSet, DomainViewSet, + FetchNotificationApiView, FlagIssueApiView, InviteFriendApiViewset, IssueViewSet, @@ -356,6 +357,7 @@ InviteFriendApiViewset.as_view(), name="api_invite_friend", ), + path("api/notification/", FetchNotificationApiView.as_view(), name="notification"), re_path(r"^scoreboard/$", ScoreboardView.as_view(), name="scoreboard"), re_path(r"^issue/$", IssueCreate.as_view(), name="issue"), re_path( diff --git a/docker-compose.yml b/docker-compose.yml index 96b049d42..d6b5ca6c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,24 @@ version: "3" services: app: - command: "poetry run python manage.py runserver 0.0.0.0:8000" + command: "poetry run uvicorn blt.asgi:application --host 0.0.0.0 --port 8000" build: . volumes: - .:/blt ports: - "8000:8000" + environment: + - REDIS_HOST=cache + - REDIS_PORT=6379 + cache: + command: "redis-server" + image: redis:6.2-alpine + restart: always + ports: + - '6379:6379' + volumes: + - cache:/data + +volumes: + cache: + driver: local \ No newline at end of file diff --git a/notification_app/__init__.py b/notification_app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notification_app/admin.py b/notification_app/admin.py new file mode 100644 index 000000000..c966af731 --- /dev/null +++ b/notification_app/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import Notification + +admin.site.register(Notification) diff --git a/notification_app/apps.py b/notification_app/apps.py new file mode 100644 index 000000000..a8aee2a49 --- /dev/null +++ b/notification_app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NotificationAppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "notification_app" diff --git a/notification_app/consumers.py b/notification_app/consumers.py new file mode 100644 index 000000000..ee1aa91b9 --- /dev/null +++ b/notification_app/consumers.py @@ -0,0 +1,33 @@ +import json + +from asgiref.sync import sync_to_async +from channels.generic.websocket import AsyncWebsocketConsumer + +from notification_app.models import Notification + + +class NotificationConsumer(AsyncWebsocketConsumer): + async def connect(self): + self.room_name = self.scope["url_route"]["kwargs"]["room_name"] + self.room_group_name = "notification_%s" % self.room_name + + # Join room group + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + + await self.accept() + + async def disconnect(self, close_code): + # Leave room group + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + async def receive(self, text_data): + data = json.loads(text_data) + notification_id = data["notification_id"] + notification = await sync_to_async(Notification.objects.get)(id=notification_id) + await sync_to_async(notification.delete)() + + # Receive message from room group + async def send_notification(self, event): + message = event + # Send message to WebSocket + await self.send(text_data=json.dumps(message)) diff --git a/notification_app/migrations/__init__.py b/notification_app/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notification_app/models.py b/notification_app/models.py new file mode 100644 index 000000000..1d6485863 --- /dev/null +++ b/notification_app/models.py @@ -0,0 +1,7 @@ +from django.contrib.auth.models import User +from django.db import models + + +class Notification(models.Model): + user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE) + message = models.TextField(null=True, blank=True, max_length=50) diff --git a/notification_app/routing.py b/notification_app/routing.py new file mode 100644 index 000000000..099baccc5 --- /dev/null +++ b/notification_app/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path + +from . import consumers + +websocket_urlpatterns = [ + re_path(r"ws/notification/(?P\w+)/$", consumers.NotificationConsumer.as_asgi()), +] diff --git a/notification_app/tests.py b/notification_app/tests.py new file mode 100644 index 000000000..a39b155ac --- /dev/null +++ b/notification_app/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/notification_app/views.py b/notification_app/views.py new file mode 100644 index 000000000..60f00ef0e --- /dev/null +++ b/notification_app/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/pyproject.toml b/pyproject.toml index e7e5f7c82..2c15b5c39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["OWASP BLT "] license = "AGPLv3" [tool.poetry.dependencies] -python = "3.11.2" +python = "3.12.3" toml = "^0.10.2" Django = "^5.0.6" dj-database-url = "^2.2.0" diff --git a/website/api/views.py b/website/api/views.py index 1650f35cb..9f9180fc4 100644 --- a/website/api/views.py +++ b/website/api/views.py @@ -630,3 +630,31 @@ class CompanyViewSet(viewsets.ModelViewSet): filter_backends = (filters.SearchFilter,) search_fields = ("id", "name") http_method_names = ("get", "post", "put") + + +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer + + +class FetchNotificationApiView(APIView): + def get(self, request, *args, **kwargs): + user_id = request.query_params.get("user_id") + if user_id is None or not user_id.isdigit(): + return Response("Invalid User Id, ID should be integer", status=400) + user_not_exist = User.objects.filter(id=user_id).exists() + if not user_not_exist: + return Response("User Does Not Exist", status=400) + + notification = Notification.objects.filter(user__id=user_id).all() + messages = [n.message for n in notification] + notification_id = [n.id for n in notification] + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + "notification_" + str(user_id), + { + "type": "send_notification", + "notification_id": notification_id, + "message": messages, + }, + ) + return Response("OK") diff --git a/website/templates/includes/header.html b/website/templates/includes/header.html index 30237028d..c6f6d7262 100644 --- a/website/templates/includes/header.html +++ b/website/templates/includes/header.html @@ -11,6 +11,115 @@ a { color: black; } + ul { + list-style: none; + margin: 0; + padding: 0; + } + + .notification-drop { + font-family: 'Ubuntu', sans-serif; + color: #444; + } + .notification-drop .item { + padding: 10px; + font-size: 18px; + position: relative; + border-bottom: 1px solid #ddd; + } + .notification-drop .item:hover { + cursor: pointer; + } + .notification-drop .item i { + margin-left: 10px; + } + .notification-drop .item ul { + display: none; + position: absolute; + top: 100%; + background: #fff; + left: -200px; + right: 0; + z-index: 1; + border-top: 1px solid #ddd; + } + .notification-drop .item ul li { + font-size: 16px; + padding: 15px 0 15px 25px; + } + .notification-drop .item ul li:hover { + background: #ddd; + color: rgba(0, 0, 0, 0.8); + } + + @media screen and (min-width: 500px) { + .notification-drop { + display: flex; + justify-content: flex-end; + } + .notification-drop .item { + border: none; + } + } + + + + .notification-bell{ + font-size: 20px; + } + + .btn__badge { + background: #FF5D5D; + color: white; + font-size: 12px; + position: absolute; + top: 0; + right: 0px; + padding: 2px 8px; + border-radius: 50%; + } + + .pulse-button { + box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.5); + -webkit-animation: pulse 1.5s infinite; + } + + .pulse-button:hover { + -webkit-animation: none; + } + + @-webkit-keyframes pulse { + 0% { + -moz-transform: scale(0.9); + -ms-transform: scale(0.9); + -webkit-transform: scale(0.9); + transform: scale(0.9); + } + 70% { + -moz-transform: scale(1); + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); + box-shadow: 0 0 0 50px rgba(255, 0, 0, 0); + } + 100% { + -moz-transform: scale(0.9); + -ms-transform: scale(0.9); + -webkit-transform: scale(0.9); + transform: scale(0.9); + box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); + } + } + + .notification-text{ + font-size: 14px; + font-weight: bold; + } + + .notification-text span{ + float: right; + } + + diff --git a/website/templates/new_home.html b/website/templates/new_home.html index fba052f96..ba535d61b 100644 --- a/website/templates/new_home.html +++ b/website/templates/new_home.html @@ -25,6 +25,16 @@ \ No newline at end of file + From df73ca7318ef53f39c709ada35a6f013d3b51d4d Mon Sep 17 00:00:00 2001 From: H4N1L <119354421+HanilJain@users.noreply.github.com> Date: Thu, 4 Jul 2024 02:08:35 +0530 Subject: [PATCH 10/11] Apply suggestions from code review suggested changes Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com> --- website/api/views.py | 4 ++-- website/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/api/views.py b/website/api/views.py index 6fd78c1ac..d4e0f4b48 100644 --- a/website/api/views.py +++ b/website/api/views.py @@ -642,7 +642,7 @@ def get(self, request, *args, **kwargs): user_id = request.query_params.get("user_id") if user_id is None or not user_id.isdigit(): return Response("Invalid User Id, ID should be integer", status=400) - user_not_exist = User.objects.filter(id=user_id).exists() + user_exists = User.objects.filter(id=user_id).exists() if not user_not_exist: return Response("User Does Not Exist", status=400) @@ -651,7 +651,7 @@ def get(self, request, *args, **kwargs): notification_id = [n.id for n in notification] channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( - "notification_" + str(user_id), + f"notification_{user_id}", { "type": "send_notification", "notification_id": notification_id, diff --git a/website/views.py b/website/views.py index cbab9641f..1768a4e92 100644 --- a/website/views.py +++ b/website/views.py @@ -326,7 +326,7 @@ def notification(request): notification_id = [n.id for n in notification] channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( - "notification_" + str(request.user.id), + f"notification_{request.user.id}", { "type": "send_notification", "notification_id": notification_id, From 585b69f191384fd5050e8b7441af3af2203516cc Mon Sep 17 00:00:00 2001 From: HanilJain Date: Fri, 5 Jul 2024 05:07:05 +0530 Subject: [PATCH 11/11] comments revoked --- blt/settings.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/blt/settings.py b/blt/settings.py index ef4ed3663..444a5c9d9 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -522,17 +522,11 @@ # Twitter -# BEARER_TOKEN = os.environ.get("BEARER_TOKEN") -# APP_KEY = os.environ.get("APP_KEY") -# APP_KEY_SECRET = os.environ.get("APP_KEY_SECRET") -# ACCESS_TOKEN = os.environ.get("ACCESS_TOKEN") -# ACCESS_TOKEN_SECRET = os.environ.get("ACCESS_TOKEN_SECRET") - -BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAACY4swEAAAAACCwbesyUMRhqJcZ5IQKJ%2FeAWpYY%3DWhxvFboQKFlJtKIJ1WWlL2fWYzKSfF383NMBsgRFTg9h8Y5jBF" -APP_KEY = "hBSe9kWzsWvrZTjbm5326j9TE" -APP_KEY_SECRET = "mbHK5fNCkIppsO8ErswLzGDXMdRS74XltkHmnFF2WXtxB60AIE" -ACCESS_TOKEN = "1562852714412793857-CHEfVrO4shDhoWJOsBNCN7Z0d0d3Kw" -ACCESS_TOKEN_SECRET = "jKfqv8FaIuYcyYdy6jdGprh2WHJtonR4ziHgETkC81hYq" +BEARER_TOKEN = os.environ.get("BEARER_TOKEN") +APP_KEY = os.environ.get("APP_KEY") +APP_KEY_SECRET = os.environ.get("APP_KEY_SECRET") +ACCESS_TOKEN = os.environ.get("ACCESS_TOKEN") +ACCESS_TOKEN_SECRET = os.environ.get("ACCESS_TOKEN_SECRET") # USPTO