diff --git a/web/api/serializers.py b/web/api/serializers.py
index 1fd0b7e91..1acbfacf6 100644
--- a/web/api/serializers.py
+++ b/web/api/serializers.py
@@ -1,6 +1,5 @@
from dashboard.models import *
-from django.contrib.humanize.templatetags.humanize import (naturalday,
- naturaltime)
+from django.contrib.humanize.templatetags.humanize import (naturalday, naturaltime)
from django.db.models import F, JSONField, Value
from recon_note.models import *
from reNgine.common_func import *
@@ -8,6 +7,29 @@
from scanEngine.models import *
from startScan.models import *
from targetApp.models import *
+from dashboard.models import InAppNotification
+
+
+class InAppNotificationSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = InAppNotification
+ fields = [
+ 'id',
+ 'title',
+ 'description',
+ 'icon',
+ 'is_read',
+ 'created_at',
+ 'notification_type',
+ 'status',
+ 'redirect_link',
+ 'open_in_new_tab',
+ 'project'
+ ]
+ read_only_fields = ['id', 'created_at']
+
+ def get_project_name(self, obj):
+ return obj.project.name if obj.project else None
class SearchHistorySerializer(serializers.ModelSerializer):
diff --git a/web/api/urls.py b/web/api/urls.py
index cfdd8f265..39ceb84a7 100644
--- a/web/api/urls.py
+++ b/web/api/urls.py
@@ -19,6 +19,7 @@
router.register(r'listIps', IpAddressViewSet)
router.register(r'listActivityLogs', ListActivityLogsViewSet)
router.register(r'listScanLogs', ListScanLogsViewSet)
+router.register(r'notifications', InAppNotificationManagerViewSet, basename='notification')
urlpatterns = [
url('^', include(router.urls)),
diff --git a/web/api/views.py b/web/api/views.py
index 8f23e1e18..740967753 100644
--- a/web/api/views.py
+++ b/web/api/views.py
@@ -14,7 +14,8 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
-from rest_framework.status import HTTP_400_BAD_REQUEST
+from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_204_NO_CONTENT
+from rest_framework.decorators import action
from recon_note.models import *
from reNgine.celery import app
@@ -33,6 +34,71 @@
logger = logging.getLogger(__name__)
+class InAppNotificationManagerViewSet(viewsets.ModelViewSet):
+ """
+ This class manages the notification model, provided CRUD operation on notif model
+ such as read notif, clear all, fetch all notifications etc
+ """
+ serializer_class = InAppNotificationSerializer
+ pagination_class = None
+
+ def get_queryset(self):
+ # we will see later if user based notif is needed
+ # return InAppNotification.objects.filter(user=self.request.user)
+ project_slug = self.request.query_params.get('project_slug')
+ queryset = InAppNotification.objects.all()
+ if project_slug:
+ queryset = queryset.filter(
+ Q(project__slug=project_slug) | Q(notification_type='system')
+ )
+ return queryset.order_by('-created_at')
+
+ @action(detail=False, methods=['post'])
+ def mark_all_read(self, request):
+ # marks all notification read
+ project_slug = self.request.query_params.get('project_slug')
+ queryset = self.get_queryset()
+
+ if project_slug:
+ queryset = queryset.filter(
+ Q(project__slug=project_slug) | Q(notification_type='system')
+ )
+ queryset.update(is_read=True)
+ return Response(status=HTTP_204_NO_CONTENT)
+
+ @action(detail=True, methods=['post'])
+ def mark_read(self, request, pk=None):
+ # mark individual notification read when cliked
+ notification = self.get_object()
+ notification.is_read = True
+ notification.save()
+ return Response(status=HTTP_204_NO_CONTENT)
+
+ @action(detail=False, methods=['get'])
+ def unread_count(self, request):
+ # this fetches the count for unread notif mainly for the badge
+ project_slug = self.request.query_params.get('project_slug')
+ queryset = self.get_queryset()
+ if project_slug:
+ queryset = queryset.filter(
+ Q(project__slug=project_slug) | Q(notification_type='system')
+ )
+ count = queryset.filter(is_read=False).count()
+ return Response({'count': count})
+
+ @action(detail=False, methods=['post'])
+ def clear_all(self, request):
+ # when clicked on the clear button this must be called to clear all notif
+ project_slug = self.request.query_params.get('project_slug')
+ queryset = self.get_queryset()
+ if project_slug:
+ queryset = queryset.filter(
+ Q(project__slug=project_slug) | Q(notification_type='system')
+ )
+ queryset.delete()
+ return Response(status=HTTP_204_NO_CONTENT)
+
+
class OllamaManager(APIView):
def get(self, request):
"""
@@ -943,8 +1009,21 @@ def get(self, request):
return_response['status'] = True
return_response['latest_version'] = latest_version
return_response['current_version'] = current_version
- return_response['update_available'] = version.parse(current_version) < version.parse(latest_version)
- if version.parse(current_version) < version.parse(latest_version):
+ is_version_update_available = version.parse(current_version) < version.parse(latest_version)
+
+ # if is_version_update_available then we should create inapp notification
+ create_inappnotification(
+ title='reNgine Update Available',
+ description=f'Update to version {latest_version} is available',
+ notification_type=SYSTEM_LEVEL_NOTIFICATION,
+ project_slug=None,
+ icon='mdi-update',
+ redirect_link='https://github.com/yogeshojha/rengine/releases',
+ open_in_new_tab=True
+ )
+
+ return_response['update_available'] = is_version_update_available
+ if is_version_update_available:
return_response['changelog'] = response[0]['body']
return Response(return_response)
diff --git a/web/dashboard/admin.py b/web/dashboard/admin.py
index be2a79a67..881b18acb 100644
--- a/web/dashboard/admin.py
+++ b/web/dashboard/admin.py
@@ -5,3 +5,4 @@
admin.site.register(Project)
admin.site.register(OpenAiAPIKey)
admin.site.register(NetlasAPIKey)
+admin.site.register(InAppNotification)
\ No newline at end of file
diff --git a/web/dashboard/migrations/0002_notification.py b/web/dashboard/migrations/0002_notification.py
new file mode 100644
index 000000000..6e9faf78d
--- /dev/null
+++ b/web/dashboard/migrations/0002_notification.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.2.23 on 2024-08-30 00:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('description', models.TextField()),
+ ('icon', models.CharField(max_length=50)),
+ ('is_read', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ options={
+ 'ordering': ['-created_at'],
+ },
+ ),
+ ]
diff --git a/web/dashboard/migrations/0003_auto_20240830_0135.py b/web/dashboard/migrations/0003_auto_20240830_0135.py
new file mode 100644
index 000000000..5e7628e00
--- /dev/null
+++ b/web/dashboard/migrations/0003_auto_20240830_0135.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.23 on 2024-08-30 01:35
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0002_notification'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='notification',
+ name='notification_type',
+ field=models.CharField(choices=[('system', 'System-wide'), ('project', 'Project-specific')], default='system', max_length=10),
+ ),
+ migrations.AddField(
+ model_name='notification',
+ name='project',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dashboard.project'),
+ ),
+ ]
diff --git a/web/dashboard/migrations/0004_rename_notification_inappnotification.py b/web/dashboard/migrations/0004_rename_notification_inappnotification.py
new file mode 100644
index 000000000..2bc145eed
--- /dev/null
+++ b/web/dashboard/migrations/0004_rename_notification_inappnotification.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.23 on 2024-08-30 01:40
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0003_auto_20240830_0135'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='Notification',
+ new_name='InAppNotification',
+ ),
+ ]
diff --git a/web/dashboard/migrations/0005_alter_inappnotification_notification_type.py b/web/dashboard/migrations/0005_alter_inappnotification_notification_type.py
new file mode 100644
index 000000000..49a5af804
--- /dev/null
+++ b/web/dashboard/migrations/0005_alter_inappnotification_notification_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.23 on 2024-08-31 01:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0004_rename_notification_inappnotification'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='inappnotification',
+ name='notification_type',
+ field=models.CharField(choices=[('system', 'system'), ('project', 'project')], default='system', max_length=10),
+ ),
+ ]
diff --git a/web/dashboard/migrations/0006_inappnotification_status.py b/web/dashboard/migrations/0006_inappnotification_status.py
new file mode 100644
index 000000000..c674b7844
--- /dev/null
+++ b/web/dashboard/migrations/0006_inappnotification_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.23 on 2024-08-31 02:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0005_alter_inappnotification_notification_type'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='inappnotification',
+ name='status',
+ field=models.CharField(choices=[('success', 'Success'), ('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=10),
+ ),
+ ]
diff --git a/web/dashboard/migrations/0007_auto_20240831_0608.py b/web/dashboard/migrations/0007_auto_20240831_0608.py
new file mode 100644
index 000000000..34acda1c6
--- /dev/null
+++ b/web/dashboard/migrations/0007_auto_20240831_0608.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.23 on 2024-08-31 06:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0006_inappnotification_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='inappnotification',
+ name='open_in_new_tab',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='inappnotification',
+ name='redirect_link',
+ field=models.URLField(blank=True, max_length=255, null=True),
+ ),
+ ]
diff --git a/web/dashboard/models.py b/web/dashboard/models.py
index 8ed77dd43..a43152b42 100644
--- a/web/dashboard/models.py
+++ b/web/dashboard/models.py
@@ -1,5 +1,5 @@
from django.db import models
-
+from reNgine.definitions import *
class SearchHistory(models.Model):
query = models.CharField(max_length=1000)
@@ -41,3 +41,30 @@ class NetlasAPIKey(models.Model):
def __str__(self):
return self.key
+
+
+class InAppNotification(models.Model):
+ project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True)
+ notification_type = models.CharField(max_length=10, choices=NOTIFICATION_TYPES, default='system')
+ status = models.CharField(max_length=10, choices=NOTIFICATION_STATUS_TYPES, default='info')
+ title = models.CharField(max_length=255)
+ description = models.TextField()
+ icon = models.CharField(max_length=50) # mdi icon class name
+ is_read = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ redirect_link = models.URLField(max_length=255, blank=True, null=True)
+ open_in_new_tab = models.BooleanField(default=False)
+
+ class Meta:
+ ordering = ['-created_at']
+
+ def __str__(self):
+ if self.notification_type == 'system':
+ return f"System wide notif: {self.title}"
+ else:
+ return f"Project wide notif: {self.project.name}: {self.title}"
+
+ @property
+ def is_system_wide(self):
+ # property to determine if the notification is system wide or project specific
+ return self.notification_type == 'system'
diff --git a/web/reNgine/common_func.py b/web/reNgine/common_func.py
index 35182fe9e..ce50b0e5b 100644
--- a/web/reNgine/common_func.py
+++ b/web/reNgine/common_func.py
@@ -1549,3 +1549,67 @@ def save_domain_info_to_db(target, domain_info):
domain.save()
return domain_info_obj
+
+
+def create_inappnotification(
+ title,
+ description,
+ notification_type=SYSTEM_LEVEL_NOTIFICATION,
+ project_slug=None,
+ icon="mdi-bell",
+ is_read=False,
+ status='info',
+ redirect_link=None,
+ open_in_new_tab=False
+):
+ """
+ This function will create an inapp notification
+ Inapp Notification not to be confused with Notification model
+ that is used for sending alerts on telegram, slack etc.
+ Inapp notification is used to show notification on the web app
+
+ Args:
+ title: str: Title of the notification
+ description: str: Description of the notification
+ notification_type: str: Type of the notification, it can be either
+ SYSTEM_LEVEL_NOTIFICATION or PROJECT_LEVEL_NOTIFICATION
+ project_slug: str: Slug of the project, if notification is PROJECT_LEVEL_NOTIFICATION
+ icon: str: Icon of the notification, only use mdi icons
+ is_read: bool: Whether the notification is read or not, default is False
+ status: str: Status of the notification (success, info, warning, error), default is info
+ redirect_link: str: Link to redirect when notification is clicked
+ open_in_new_tab: bool: Whether to open the redirect link in a new tab, default is False
+
+ Returns:
+ ValueError: if error
+ InAppNotification: InAppNotification object if successful
+ """
+ logger.info('Creating InApp Notification with title: %s', title)
+ if notification_type not in [SYSTEM_LEVEL_NOTIFICATION, PROJECT_LEVEL_NOTIFICATION]:
+ raise ValueError("Invalid notification type")
+
+ if status not in [choice[0] for choice in NOTIFICATION_STATUS_TYPES]:
+ raise ValueError("Invalid notification status")
+
+ project = None
+ if notification_type == PROJECT_LEVEL_NOTIFICATION:
+ if not project_slug:
+ raise ValueError("Project slug is required for project level notification")
+ try:
+ project = Project.objects.get(slug=project_slug)
+ except Project.DoesNotExist as e:
+ raise ValueError(f"No project exists: {e}")
+
+ notification = InAppNotification(
+ title=title,
+ description=description,
+ notification_type=notification_type,
+ project=project,
+ icon=icon,
+ is_read=is_read,
+ status=status,
+ redirect_link=redirect_link,
+ open_in_new_tab=open_in_new_tab
+ )
+ notification.save()
+ return notification
\ No newline at end of file
diff --git a/web/reNgine/definitions.py b/web/reNgine/definitions.py
index e90ece4f5..58fce2dd5 100644
--- a/web/reNgine/definitions.py
+++ b/web/reNgine/definitions.py
@@ -548,3 +548,18 @@
# OSINT GooFuzz Path
GOFUZZ_EXEC_PATH = '/usr/src/github/goofuzz/GooFuzz'
+
+
+# In App Notification Definitions
+SYSTEM_LEVEL_NOTIFICATION = 'system'
+PROJECT_LEVEL_NOTIFICATION = 'project'
+NOTIFICATION_TYPES = (
+ ('system', SYSTEM_LEVEL_NOTIFICATION),
+ ('project', PROJECT_LEVEL_NOTIFICATION),
+)
+NOTIFICATION_STATUS_TYPES = (
+ ('success', 'Success'),
+ ('info', 'Informational'),
+ ('warning', 'Warning'),
+ ('error', 'Error'),
+)
\ No newline at end of file
diff --git a/web/reNgine/tasks.py b/web/reNgine/tasks.py
index 12596e7a2..81c328f7f 100644
--- a/web/reNgine/tasks.py
+++ b/web/reNgine/tasks.py
@@ -3085,6 +3085,7 @@ def send_scan_notif(
url = get_scan_url(scan_history_id, subscan_id)
title = get_scan_title(scan_history_id, subscan_id)
fields = get_scan_fields(engine, scan, subscan, status, tasks)
+
severity = None
msg = f'{title} {status}\n'
msg += '\n🡆 '.join(f'**{k}:** {v}' for k, v in fields.items())
@@ -3098,12 +3099,64 @@ def send_scan_notif(
}
logger.warning(f'Sending notification "{title}" [{severity}]')
+ generate_inapp_notification(scan, subscan, status, engine, fields)
+
# Send notification
send_notif(
msg,
scan_history_id,
subscan_id,
**opts)
+
+def generate_inapp_notification(scan, subscan, status, engine, fields):
+ scan_type = "Subscan" if subscan else "Scan"
+ domain = subscan.domain.name if subscan else scan.domain.name
+ duration_msg = None
+ redirect_link = None
+
+ if status == 'RUNNING':
+ title = f"{scan_type} Started"
+ description = f"{scan_type} has been initiated for {domain}"
+ icon = "mdi-play-circle-outline"
+ notif_status = 'info'
+ elif status == 'SUCCESS':
+ title = f"{scan_type} Completed"
+ description = f"{scan_type} was successful for {domain}"
+ icon = "mdi-check-circle-outline"
+ notif_status = 'success'
+ duration_msg = f'Completed in {fields.get("Duration")}'
+ elif status == 'ABORTED':
+ title = f"{scan_type} Aborted"
+ description = f"{scan_type} was aborted for {domain}"
+ icon = "mdi-alert-circle-outline"
+ notif_status = 'warning'
+ duration_msg = f'Aborted in {fields.get("Duration")}'
+ elif status == 'FAILED':
+ title = f"{scan_type} Failed"
+ description = f"{scan_type} has failed for {domain}"
+ icon = "mdi-close-circle-outline"
+ notif_status = 'error'
+ duration_msg = f'Failed in {fields.get("Duration")}'
+
+ description += f"
Engine: {engine.engine_name if engine else 'N/A'}"
+ slug = scan.domain.project.slug if scan else subscan.history.domain.project.slug
+ if duration_msg:
+ description += f"
{duration_msg}"
+
+ if status != 'RUNNING':
+ redirect_link = f"/scan/{slug}/detail/{scan.id}" if scan else None
+
+ create_inappnotification(
+ title=title,
+ description=description,
+ notification_type='project',
+ project_slug=slug,
+ icon=icon,
+ is_read=False,
+ status=notif_status,
+ redirect_link=redirect_link,
+ open_in_new_tab=False
+ )
@app.task(name='send_task_notif', bind=False, queue='send_task_notif_queue')
diff --git a/web/reNgine/utilities.py b/web/reNgine/utilities.py
index 58a4503b0..9f9eee92f 100644
--- a/web/reNgine/utilities.py
+++ b/web/reNgine/utilities.py
@@ -161,6 +161,7 @@ def is_out_of_scope(self, subdomain):
return any(pattern.search(subdomain) for pattern in self.regex_patterns)
+
def sorting_key(subdomain):
# sort subdomains based on their http status code with priority 200 < 300 < 400 < rest
status = subdomain['http_status']
diff --git a/web/static/custom/custom.css b/web/static/custom/custom.css
index 52835d872..47fee0e0b 100644
--- a/web/static/custom/custom.css
+++ b/web/static/custom/custom.css
@@ -407,3 +407,178 @@ mark{
.scan-config-modal .badge {
margin-left: 5px;
}
+
+/*.notification-a-icon {
+
+}*/
+
+.badge-notif-count {
+ font-size: 10px !important;
+}
+
+.notif-counter {
+ top: 9px !important;
+ right: 5px !important;
+}
+
+.notification-panel-dropdown {
+ width: 400px;
+ border: none;
+ border-radius: 8px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ padding: 0;
+ overflow: hidden;
+}
+
+.notification-panel-header {
+ background-color: #f8f9fa;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.notification-panel-clear-link {
+ color: #6c757d;
+ text-decoration: none;
+ font-size: 0.875rem;
+ transition: color 0.2s ease;
+}
+
+.notification-panel-clear-link:hover {
+ color: #495057;
+}
+
+.notification-panel-body {
+ max-height: 400px;
+ /* overflow-y: auto; */
+}
+
+.notification-panel-item {
+ border-left: 4px solid transparent;
+ transition: all 0.3s ease;
+ margin-bottom: 2px;
+}
+
+.notification-panel-item:last-child {
+ border-bottom: none;
+}
+
+.notification-panel-item:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 2px 0px rgba(0, 0, 0, 0.10);
+}
+
+/* status */
+.notification-panel-status-success {
+ border-left-color: #10B981;
+}
+
+.notification-panel-status-info {
+ border-left-color: #3B82F6;
+}
+
+.notification-panel-status-warning {
+ border-left-color: #F59E0B;
+}
+
+.notification-panel-status-error {
+ border-left-color: #EF4444;
+}
+
+.notification-panel-unread {
+ background-color: #F3F4F6;
+}
+
+.notification-panel-unread:hover {
+ background-color: #E5E7EB;
+}
+
+.notification-panel-icon {
+ font-size: 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background-color: #E5E7EB;
+}
+
+.notification-panel-title {
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.notification-panel-status-success .notification-panel-title {
+ color: #10B981;
+}
+
+.notification-panel-status-info .notification-panel-title {
+ color: #3B82F6;
+}
+
+.notification-panel-status-warning .notification-panel-title {
+ color: #F59E0B;
+}
+
+.notification-panel-status-error .notification-panel-title {
+ color: #EF4444;
+}
+
+.notification-panel-description {
+ color: #4B5563;
+ font-size: 0.875rem;
+ margin-bottom: 0.25rem;
+ line-height: 1.4;
+}
+
+.notification-panel-time {
+ color: #6B7280;
+ font-size: 0.8rem;
+}
+
+.notification-panel-footer {
+ background-color: #f8f9fa;
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.notification-panel-view-all {
+ color: #007bff;
+ text-decoration: none;
+ font-size: 0.875rem;
+ font-weight: 600;
+ transition: color 0.2s ease;
+}
+
+.notification-panel-view-all:hover {
+ color: #0056b3;
+}
+
+.notification-panel-status-success .notification-panel-icon {
+ color: #10B981;
+}
+
+.notification-panel-status-info .notification-panel-icon {
+ color: #3B82F6;
+}
+
+.notification-panel-status-warning .notification-panel-icon {
+ color: #F59E0B;
+}
+
+.notification-panel-status-error .notification-panel-icon {
+ color: #EF4444;
+}
+
+@keyframes notification-buzz {
+ 0% { transform: translateX(0) rotate(0); }
+ 15% { transform: translateX(-5px) rotate(-5deg); }
+ 30% { transform: translateX(5px) rotate(5deg); }
+ 45% { transform: translateX(-4px) rotate(-4deg); }
+ 60% { transform: translateX(4px) rotate(4deg); }
+ 75% { transform: translateX(-2px) rotate(-2deg); }
+ 90% { transform: translateX(2px) rotate(2deg); }
+ 100% { transform: translateX(0) rotate(0); }
+}
+
+.notification-bell-icon.buzz {
+ animation: notification-buzz 0.8s ease-out;
+}
\ No newline at end of file
diff --git a/web/static/custom/custom.js b/web/static/custom/custom.js
index 570d72332..617fb4adb 100644
--- a/web/static/custom/custom.js
+++ b/web/static/custom/custom.js
@@ -1,3 +1,26 @@
+function loadScript(src) {
+ // helper function to load a script asynchronously
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = src;
+ script.onload = resolve;
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+}
+
+function loadCSS(href) {
+ // helper function to load a css asynchronously
+ return new Promise((resolve, reject) => {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = href;
+ link.onload = resolve;
+ link.onerror = reject;
+ document.head.appendChild(link);
+ });
+}
+
function getCurrentProjectSlug(){
return document.querySelector('input[name="current_project"]').value;
}
@@ -3377,3 +3400,5 @@ function show_scan_configuration(starting_path, out_of_scope_subdomains, exclude
$('#modal-content').append(content);
$('#modal_dialog').modal('show');
}
+
+
diff --git a/web/static/custom/notification.js b/web/static/custom/notification.js
new file mode 100644
index 000000000..1c6e484bc
--- /dev/null
+++ b/web/static/custom/notification.js
@@ -0,0 +1,229 @@
+// all the functions and event listeners for the notification panel
+
+// this is to check and compare the last notification id with the current notification id
+let lastNotificationId = null;
+let isInitialLoad = true;
+
+function updateNotifications() {
+ let api_url = "/api/notifications/";
+ const currentProjectSlug = getCurrentProjectSlug();
+ if (currentProjectSlug) {
+ api_url += `?project_slug=${currentProjectSlug}`;
+ }
+ fetch(api_url, {
+ method: "GET",
+ credentials: "same-origin",
+ headers: {
+ "X-CSRFToken": getCookie("csrftoken"),
+ },
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ const notificationPanel = document.querySelector(
+ ".notification-panel-body"
+ );
+ notificationPanel.innerHTML = "";
+
+ if (data.length === 0) {
+ const noNotificationsMessage = document.createElement("div");
+ noNotificationsMessage.className =
+ "notification-panel-item d-flex align-items-center justify-content-center p-3";
+ noNotificationsMessage.innerHTML = `
+
Ping? Pong! No notifications, moving along
+ `; + notificationPanel.appendChild(noNotificationsMessage); + } else { + // decisive part to show the Snackbar + if (!isInitialLoad && data[0].id !== lastNotificationId) { + showNotificationSnackbar(data[0]); + } + lastNotificationId = data[0].id; + + data.forEach((notification) => { + const notificationItem = document.createElement("div"); + notificationItem.className = `notification-panel-item d-flex align-items-start p-3 ${ + notification.is_read ? "" : "notification-panel-unread" + } notification-panel-status-${notification.status}`; + notificationItem.innerHTML = ` +${ + notification.description + }
+ ${timeago.format( + new Date(notification.created_at) + )} +