diff --git a/backend/api_app/monitoring/migrations/0006_alerts_active.py b/backend/api_app/monitoring/migrations/0006_alerts_active.py new file mode 100644 index 0000000..06fd8bc --- /dev/null +++ b/backend/api_app/monitoring/migrations/0006_alerts_active.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.19 on 2023-11-08 06:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('monitoring', '0005_alter_notification_notification_body'), + ] + + operations = [ + migrations.AddField( + model_name='alerts', + name='active', + field=models.BooleanField(default=True), + ), + ] diff --git a/backend/api_app/monitoring/models.py b/backend/api_app/monitoring/models.py index f9cc282..8a3eb7e 100644 --- a/backend/api_app/monitoring/models.py +++ b/backend/api_app/monitoring/models.py @@ -31,6 +31,7 @@ class Alerts(BaseMixin): alert_yaml = YAMLField() name = models.CharField(max_length=255, blank=False, null=False) description = models.TextField(blank=True, null=True) + active = models.BooleanField(default=True) def __str__(self): return f"{self.smart_contract.address} - {self.name}" diff --git a/backend/api_app/monitoring/permissions.py b/backend/api_app/monitoring/permissions.py index 335caa4..f9229fb 100644 --- a/backend/api_app/monitoring/permissions.py +++ b/backend/api_app/monitoring/permissions.py @@ -20,9 +20,11 @@ def has_permission(self, request, view): if not request.user.is_authenticated: return False - alert = Alerts.objects.filter(id=view.kwargs["pk"]).first() + id = view.kwargs.get("pk") + + alert = Alerts.objects.filter(id=id).first() if alert is None: - self.message = f"Alert with id {view.kwargs['pk']} does not exist." + self.message = f"Alert with id {id} does not exist." return False return Membership.is_member( diff --git a/backend/api_app/monitoring/serializers.py b/backend/api_app/monitoring/serializers.py index 0c3bc2a..32c118e 100644 --- a/backend/api_app/monitoring/serializers.py +++ b/backend/api_app/monitoring/serializers.py @@ -108,6 +108,23 @@ def validate_configuration(yaml_data): serializer.is_valid(raise_exception=True) return serializer.validated_data +class AlertUpdateSerializer(rfs.ModelSerializer): + class Meta: + model = Alerts + fields = ('name', 'smart_contract', 'alert_yaml', 'active') # Include the fields you want to allow for update + + def validate(self, data): + if "alert_yaml" in data: + try: + yaml_to_json = yaml.safe_load(data["alert_yaml"]) + data["alert_yaml"] = validate_configuration(yaml_to_json) + except Exception as e: + logger.error(e) + raise rfs.ValidationError(e) + if "smart_contract" in data: + if data["smart_contract"].owner_organization != self.instance.smart_contract.owner_organization: + raise rfs.ValidationError("Cannot change smart contract to a different organization") + return data class AlertsAPISerializer(rfs.ModelSerializer): class CustomYAMLField(rfs.Field): diff --git a/backend/api_app/monitoring/tasks.py b/backend/api_app/monitoring/tasks.py index 939d5af..19327c2 100644 --- a/backend/api_app/monitoring/tasks.py +++ b/backend/api_app/monitoring/tasks.py @@ -38,8 +38,8 @@ def call_smart_contract_function(self, monitoring_task_id): def send_webhook(self, notification_id): notification = Notification.objects.filter(id=notification_id).first() - if not notification: - error_msg = f"Notification with id {notification_id} not found" + if (not notification) and (notification.alert.active is False): + error_msg = f"Active notification with id {notification_id} not found" logger.error(error_msg) raise Exception(error_msg) @@ -51,8 +51,10 @@ def send_webhook(self, notification_id): webhook_url = notification.notification_target webhook_body = notification.notification_body + headers = {"Content-Length": str(len(webhook_body))} + try: - response = requests.post(webhook_url, json=webhook_body) + response = requests.post(webhook_url, json=webhook_body, headers=headers) notification.meta_logs = { "timestamp": datetime.now().isoformat(), "response": response.text, @@ -220,9 +222,14 @@ def trace_transaction(transaction_hash): transaction["fn_name"] = fn_name alerts = Alerts.objects.filter( + active=True, smart_contract=monitoring_task.SmartContract ) + if len(alerts) == 0: + logger.info("No alerts found for this contract") + break + for alert in alerts: alert_runner = BlockchainAlertRunner(alert, transaction) alert_runner.run() diff --git a/backend/api_app/monitoring/urls.py b/backend/api_app/monitoring/urls.py index 4bbbd7b..88a4a22 100644 --- a/backend/api_app/monitoring/urls.py +++ b/backend/api_app/monitoring/urls.py @@ -3,6 +3,8 @@ from .views import ( AlertCreateAPIView, AlertRetrieveAPIView, + AlertUpdateAPIView, + AlertDeleteAPIView, NotificationListViewSet, OrganizationAlertListViewSet, OverviewDataAPIView, @@ -17,6 +19,8 @@ path("organization/alerts", OrganizationAlertListViewSet.as_view()), path("alerts/", AlertRetrieveAPIView.as_view(), name="alert-retrieve"), path("alerts", AlertCreateAPIView.as_view(), name="alert-create"), + path("alerts/update/", AlertUpdateAPIView.as_view(), name="alert-update"), + path("alerts/delete/", AlertDeleteAPIView.as_view(), name="alert-delete"), path("notifications", NotificationListViewSet.as_view(), name="notification-list"), path("pre-written-alerts", get_pre_written_alerts, name="pre-written-alerts"), path( diff --git a/backend/api_app/monitoring/views.py b/backend/api_app/monitoring/views.py index cce0dd0..195e003 100644 --- a/backend/api_app/monitoring/views.py +++ b/backend/api_app/monitoring/views.py @@ -3,7 +3,7 @@ from rest_framework import status from rest_framework.decorators import api_view, permission_classes -from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveAPIView +from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -19,7 +19,7 @@ SmartContractAlertPermissions, SmartContractNotificationAndAlertsPermissions, ) -from .serializers import AlertsAPISerializer, NotificationAPISerializer +from .serializers import AlertUpdateSerializer, AlertsAPISerializer, NotificationAPISerializer logger = logging.getLogger(__name__) @@ -293,7 +293,30 @@ def post(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) + +class AlertDeleteAPIView(DestroyAPIView): + queryset = Alerts.objects.all() + permission_classes = [AlertCanBeAccessedPermissions] + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + +class AlertUpdateAPIView(UpdateAPIView): + serializer_class = AlertUpdateSerializer + permission_classes = [AlertCanBeAccessedPermissions] + + def get_queryset(self): + queryset = Alerts.objects.all() + return queryset + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) class NotificationListViewSet(ListAPIView): serializer_class = NotificationAPISerializer