diff --git a/backend/account/admin.py b/backend/account/admin.py index d4e4460..22f080d 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -10,6 +10,12 @@ from django.contrib.auth.models import Group from unfold.forms import AdminPasswordChangeForm from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm from utils.admin import ModelAdmin +from django.template.loader import render_to_string +from folium import Map, Marker +from unfold.decorators import action, display +from django.utils.html import format_html + + class UserAddressInLine(TabularInline): model = UserAddressModel extra = 0 @@ -95,4 +101,37 @@ class PushSubscription(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm compressed_fields = True - warn_unsaved_form = True \ No newline at end of file + warn_unsaved_form = True + + + +@admin.register(SecurityBreachAttemptModel) +class SecurityBreachAttemptAdmin(ModelAdmin, ImportExportModelAdmin): + import_form_class = ImportForm + export_form_class = ExportForm + compressed_fields = True + warn_unsaved_form = True + change_form_template = 'loction_chagne_form.html' + list_display = ['ip', 'country', 'region_name', 'city', 'zip_code', 'isp', 'created_at', 'trys', 'display_viewd'] + + def change_view(self, request, object_id, form_url='', extra_context=None): + extra_context = extra_context or {} + obj = self.get_object(request, object_id) + + if obj and obj.lat and obj.lon: + m = Map(location=[obj.lat, obj.lon], zoom_start=10) + Marker([obj.lat, obj.lon], popup=f"Location: {obj.ip}").add_to(m) + map_html = m._repr_html_() + extra_context['map_html'] = map_html + return super().change_view(request, object_id, form_url, extra_context) + + @display(description='دیده شده') + def display_viewd(self, instance): + if instance.viewd: + svg = f'' + else: + svg = f'' + + return format_html( + svg + ) \ No newline at end of file diff --git a/backend/account/migrations/0015_securitybreachattemptmodel.py b/backend/account/migrations/0015_securitybreachattemptmodel.py new file mode 100644 index 0000000..15e68d1 --- /dev/null +++ b/backend/account/migrations/0015_securitybreachattemptmodel.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.2 on 2025-02-20 20:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0014_alter_pushsubscription_created_at_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='SecurityBreachAttemptModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip', models.CharField(max_length=40, unique=True, verbose_name='آدرس آی\u200cپی')), + ('country', models.CharField(blank=True, max_length=40, null=True, verbose_name='کشور')), + ('region_name', models.CharField(blank=True, max_length=40, null=True, verbose_name='منطقه')), + ('city', models.CharField(blank=True, max_length=40, null=True, verbose_name='شهر')), + ('zip_code', models.CharField(blank=True, max_length=40, null=True, verbose_name='کد پستی')), + ('lon', models.CharField(blank=True, max_length=40, null=True, verbose_name='طول جغرافیایی')), + ('lat', models.CharField(blank=True, max_length=40, null=True, verbose_name='عرض جغرافیایی')), + ('isp', models.CharField(blank=True, max_length=40, null=True, verbose_name='ارائه\u200cدهنده اینترنت (ISP)')), + ], + options={ + 'verbose_name': 'تلاش نفوذ', + 'verbose_name_plural': 'تلاش\u200cهای نفوذ', + }, + ), + ] diff --git a/backend/account/migrations/0016_securitybreachattemptmodel_viewd.py b/backend/account/migrations/0016_securitybreachattemptmodel_viewd.py new file mode 100644 index 0000000..6f16be0 --- /dev/null +++ b/backend/account/migrations/0016_securitybreachattemptmodel_viewd.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-20 21:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0015_securitybreachattemptmodel'), + ] + + operations = [ + migrations.AddField( + model_name='securitybreachattemptmodel', + name='viewd', + field=models.BooleanField(default=False, verbose_name='تماشا شده'), + ), + ] diff --git a/backend/account/migrations/0017_securitybreachattemptmodel_created_at_and_more.py b/backend/account/migrations/0017_securitybreachattemptmodel_created_at_and_more.py new file mode 100644 index 0000000..ff75286 --- /dev/null +++ b/backend/account/migrations/0017_securitybreachattemptmodel_created_at_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.2 on 2025-02-20 21:35 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0016_securitybreachattemptmodel_viewd'), + ] + + operations = [ + migrations.AddField( + model_name='securitybreachattemptmodel', + name='created_at', + field=models.DateField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='securitybreachattemptmodel', + name='trys', + field=models.IntegerField(default=0), + ), + ] diff --git a/backend/account/migrations/0018_alter_securitybreachattemptmodel_created_at_and_more.py b/backend/account/migrations/0018_alter_securitybreachattemptmodel_created_at_and_more.py new file mode 100644 index 0000000..79be7eb --- /dev/null +++ b/backend/account/migrations/0018_alter_securitybreachattemptmodel_created_at_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2025-02-20 21:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0017_securitybreachattemptmodel_created_at_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='securitybreachattemptmodel', + name='created_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='شروع حمله'), + ), + migrations.AlterField( + model_name='securitybreachattemptmodel', + name='trys', + field=models.IntegerField(default=0, verbose_name='تعداد تلاش ها'), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 6512684..5e41384 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -198,4 +198,46 @@ class PushSubscription(models.Model): } ) except WebPushException as ex: - print(f"Failed to send notification to {sub.user}:", ex) \ No newline at end of file + print(f"Failed to send notification to {sub.user}:", ex) + + + +def get_location_from_ip(ip_address): + try: + response = requests.get(f"http://ip-api.com/json/{ip_address}") + data = response.json() + if data["status"] == "success": + return data['country'], data['regionName'], data['city'], data.get('zip', 'ناموجود'), data['lat'], data['lon'], data['isp'] + else: + print("Error fetching data: ", data["message"]) + return None + except Exception as e: + print(f"An error occurred: {e}") + return None + +class SecurityBreachAttemptModel(models.Model): + ip = models.CharField(max_length=40, unique=True, verbose_name="آدرس آی‌پی") + country = models.CharField(max_length=40, verbose_name="کشور", blank=True, null=True) + region_name = models.CharField(max_length=40, verbose_name="منطقه", blank=True, null=True) + city = models.CharField(max_length=40, verbose_name="شهر", blank=True, null=True) + zip_code = models.CharField(max_length=40, verbose_name="کد پستی", blank=True, null=True) + lon = models.CharField(max_length=40, verbose_name="طول جغرافیایی", blank=True, null=True) + lat = models.CharField(max_length=40, verbose_name="عرض جغرافیایی", blank=True, null=True) + isp = models.CharField(max_length=40, verbose_name="ارائه‌دهنده اینترنت (ISP)", blank=True, null=True) + viewd = models.BooleanField(default=False, verbose_name='تماشا شده') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='شروع حمله') + trys = models.IntegerField(default=0, verbose_name='تعداد تلاش ها') + + def save(self, *args, **kwargs): + if not self.id: + location_data = get_location_from_ip(self.ip) + if location_data: + self.country, self.region_name, self.city, self.zip_code, self.lat, self.lon, self.isp = location_data + + super().save(*args, **kwargs) + + def __str__(self): + return f'تلاش نفوذ از {self.ip} در {self.city}, {self.country}' + class Meta: + verbose_name = "تلاش نفوذ" + verbose_name_plural = "تلاش‌های نفوذ" \ No newline at end of file diff --git a/backend/account/urls.py b/backend/account/urls.py index c5ebbf4..efdc99e 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -12,5 +12,6 @@ urlpatterns = [ path('address/delete/', views.DeleteAddressView.as_view(), name='delete-address'), path('address/list', views.GetUserAddressesView.as_view(), name='list-addresses'), path('address/', views.GetIDUserAddressView.as_view(), name='get-ID-address'), - path('subscribe', views.SubscribeView.as_view(), name='subscibe') + path('subscribe', views.SubscribeView.as_view(), name='subscibe'), + path('attack/view/', views.ChangeViewAttack.as_view(), name='attack-view'), ] \ No newline at end of file diff --git a/backend/account/views.py b/backend/account/views.py index 69e81dd..0e15ef2 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -3,13 +3,14 @@ from rest_framework.views import APIView from rest_framework import generics, permissions, status from rest_framework.response import Response from .serializers import * -from .models import UserAddressModel, User +from .models import UserAddressModel, User, SecurityBreachAttemptModel from rest_framework.permissions import IsAuthenticated, AllowAny from drf_spectacular.utils import extend_schema, OpenApiParameter from rest_framework_simplejwt.views import TokenObtainPairView -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from rest_framework_simplejwt.tokens import RefreshToken import ghasedak_sms +from django.views import View # this works only need to be used # class APIView(APIView): # def __init__(self, *args, **kwargs): @@ -185,4 +186,12 @@ class SubscribeView(APIView): defaults=(push_ser.validated_data) ) return Response(status=status.HTTP_201_CREATED) - return Response(status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(status=status.HTTP_400_BAD_REQUEST) + + +class ChangeViewAttack(View): + def get(self, request, pk): + attack = get_object_or_404(SecurityBreachAttemptModel, pk=pk) + attack.viewd = not attack.viewd + attack.save() + return redirect('admin:account_securitybreachattemptmodel_changelist') \ No newline at end of file diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index 934d808..0577431 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -209,6 +209,12 @@ UNFOLD = { "icon": "contact_mail", "link": reverse_lazy("admin:account_useraddressmodel_changelist"), }, + { + "title": _("تلاش‌های نفوذ"), + "icon": "gpp_maybe", + "link": reverse_lazy("admin:account_securitybreachattemptmodel_changelist"), + "badge": 'utils.admin.new_attck_count' + }, ], }, diff --git a/backend/core/views.py b/backend/core/views.py index d673079..ec0b347 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -9,7 +9,7 @@ from django.views.generic import RedirectView, TemplateView from unfold.views import UnfoldModelAdminViewMixin from order.models import OrderModel from ticket.models import Ticket - +from account.models import SecurityBreachAttemptModel import json @@ -267,20 +267,15 @@ class FakeAdminLoginView(View): return context def get(self, request): - # Log empty attempt (optional) ip = request.META.get("REMOTE_ADDR") - print(f"Honeypot page accessed from IP: {ip}") - + hacker, created = SecurityBreachAttemptModel.objects.get_or_create(ip=ip) return render(request, 'admin/fake_login.html', self.get_context(request)) def post(self, request): - username = request.POST.get("username") - password = request.POST.get("password") # Never actually used ip = request.META.get("REMOTE_ADDR") - - print(f"Honeypot triggered! IP: {ip}, Username: {username}") + hacker, created = SecurityBreachAttemptModel.objects.get_or_create(ip=ip) + hacker.trys += 1 + hacker.save() messages.error(request, "Please correct the error below.") messages.error(request, "Please enter the correct شماره تماس and password for a staff account. Note that both fields may be case-sensitive.") - - # Redirect back to fake login page with context return render(request, 'admin/fake_login.html', self.get_context(request)) \ No newline at end of file diff --git a/backend/templates/loction_chagne_form.html b/backend/templates/loction_chagne_form.html new file mode 100644 index 0000000..e03f446 --- /dev/null +++ b/backend/templates/loction_chagne_form.html @@ -0,0 +1,187 @@ +{% extends "admin/base_site.html" %} +{% load unfold %} +{% load i18n admin_urls static admin_modify %} + +{% block extrahead %}{{ block.super }} + + {{ media }} + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %} + +{% if not is_popup %} + {% block breadcrumbs %} +
+
+
    + {% url 'admin:index' as link %} + {% trans 'Home' as name %} + {% include 'unfold/helpers/breadcrumb_item.html' with link=link name=name %} + + {% url 'admin:app_list' app_label=opts.app_label as link %} + {% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.app_config.verbose_name %} + + {% if has_view_permission %} + {% url opts|admin_urlname:'changelist' as link %} + {% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.verbose_name_plural|capfirst %} + {% else %} + {% include 'unfold/helpers/breadcrumb_item.html' with link='' name=opts.verbose_name_plural|capfirst %} + {% endif %} + + {% if add %} + {% blocktranslate trimmed with name=opts.verbose_name asvar breadcrumb_name %} + Add {{ name }} + {% endblocktranslate %} + {% include 'unfold/helpers/breadcrumb_item.html' with link='' name=breadcrumb_name %} + {% else %} + {% include 'unfold/helpers/breadcrumb_item.html' with link='' name=original|truncatewords:'18' %} + {% endif %} +
+
+
+ {% endblock %} +{% endif %} + + + +{% block nav-global-side %} + {% if has_add_permission %} + {% include "unfold/helpers/add_link.html" %} + {% endif %} +{% endblock %} + +{% block content %} +
+ {% block form_before %}{% endblock %} + {% if adminform.model_admin.change_form_outer_before_template %} + {% include adminform.model_admin.change_form_outer_before_template %} + {% endif %} + +
+ {% if original.lon and original.lat %} +
+
+ {% if original.country %} + + 🇺🇳 {{ original.country }} + + {% endif %} + + {% if original.region_name %} + + + {{ original.region_name }} + + {% endif %} + + {% if original.city %} + + + {{ original.city }} + + {% endif %} + + {% if original.zip_code %} + + + {{ original.zip_code }} + + {% endif %} + + {% if original.isp %} + + + {{ original.isp }} + + {% endif %} + {% if original.ip %} + + + {{ original.ip }} + + {% endif %} +
+
+ + {% if map_html %} + +
+
+ {{ map_html|safe }} +
+
+ {% else %} +

نقشه در دسترس نیست

+ {% endif %} + {% else %} +

موقعیت جغرافیایی موجود نیست

+ {% endif %} +
+ + {% if adminform.model_admin.change_form_outer_after_template %} + {% include adminform.model_admin.change_form_outer_after_template %} + {% endif %} + + {% block form_after %}{% endblock %} +
+{% endblock %} + +{% block extrajs %} + + +{% endblock %} \ No newline at end of file diff --git a/backend/utils/admin.py b/backend/utils/admin.py index a86239c..1107fab 100644 --- a/backend/utils/admin.py +++ b/backend/utils/admin.py @@ -2,6 +2,7 @@ from order.models import OrderModel from product.models import DollorModel, CommentModel from ticket.models import Ticket from home.models import LearnVideoModel +from account.models import SecurityBreachAttemptModel def admin_pending_count(request): pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count() @@ -20,6 +21,8 @@ def new_ticket_count(request): def new_learn_video_count(request): return LearnVideoModel.objects.filter(viewd=False).count() +def new_attck_count(request): + return SecurityBreachAttemptModel.objects.filter(viewd=False).count() from django.contrib import admin, messages from unfold.admin import ModelAdmin