Preparing for actual products...
IP Rate Limit "firewall"
This commit is contained in:
parent
320e031786
commit
d3348cc4de
20 changed files with 387 additions and 48 deletions
|
@ -1,23 +1,31 @@
|
|||
class BaseProductHandler:
|
||||
def __init__(self, product_id):
|
||||
self.product_id = product_id
|
||||
class BaseProductType:
|
||||
def __init__(self, product_type_id):
|
||||
self.id = product_type_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "Base Product Type"
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return ""
|
||||
|
||||
def is_creatable(self):
|
||||
'''Returns True if this product can be created right now, or False if it cannot. Should return True if the module is correctly configured, the product is in stock (if physical), etc.'''
|
||||
'''Returns True if a product can be created right now using this type, or False if it cannot. Should return True if the module is correctly configured, etc.'''
|
||||
return False
|
||||
|
||||
@property
|
||||
def setup_view(self):
|
||||
'''Returns a view in which the product can be configured, or None if no configuration is possible/necessary.'''
|
||||
return None
|
||||
|
||||
class FallbackProductHandler(BaseProductHandler):
|
||||
pass
|
||||
|
||||
class CoreProductHandler(BaseProductHandler):
|
||||
@property
|
||||
def is_creatable(self):
|
||||
def is_orderable(self, product):
|
||||
'''Returns True if this product can be ordered right now. Should return True if the product is in stock, etc.'''
|
||||
return True
|
||||
|
||||
ProductHandler = CoreProductHandler
|
||||
def orderable_by_user(self, product):
|
||||
'''Returns a tuple of boolean values (user is allowed to order this, override by administrator allowed). First boolean is True if the user can order this product. If the first boolean is False, the second boolean indicates if an administrator can provide the product manually nonetheless.'''
|
||||
return self.is_orderable()
|
||||
|
||||
def product_setup_view(self):
|
||||
'''Returns a view in which a new product can be configured, or None if no configuration is possible/necessary.'''
|
||||
return None
|
||||
|
||||
def service_setup_view(self, product):
|
||||
'''Returns a view in which a new service can be configured, or None if no configuration is possible/necessary.'''
|
||||
return True
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from core.helpers.mail import get_template, simple_send_mail
|
||||
from core.helpers.urls import relative_to_absolute as reltoabs
|
||||
from core.models.auth import LoginLog, PWResetToken
|
||||
from core.models.auth import LoginLog, PWResetToken, IPLimit
|
||||
from core.helpers.request import get_client_ip
|
||||
|
||||
from django.urls import reverse
|
||||
|
@ -28,7 +28,7 @@ def clear_login_log(maxage=int(getValue("core.auth.ratelimit.period", 600))):
|
|||
|
||||
def clear_ratelimits(maxage=int(getValue("core.auth.ratelimit.block", 3600))):
|
||||
timestamp = timezone.now() - timezone.timedelta(seconds=maxage)
|
||||
LoginLog.objects.filter(timestamp__lt=timestamp).delete()
|
||||
IPLimit.objects.filter(end__lt=timestamp).delete()
|
||||
|
||||
def request_password(user):
|
||||
token = PWResetToken.objects.create(user=user)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.contrib.messages import error
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from core.models.profiles import AdminProfile
|
||||
|
||||
class AdminMixin(AccessMixin):
|
||||
@never_cache
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
self.permission_denied_message = "You must be logged in to access this area."
|
||||
|
|
|
@ -3,6 +3,8 @@ from django.contrib.auth import get_user_model
|
|||
|
||||
from dbsettings.functions import getValue
|
||||
|
||||
from core.fields.base import LongCharField
|
||||
|
||||
from uuid import uuid4
|
||||
from datetime import timedelta
|
||||
|
||||
|
@ -24,9 +26,5 @@ class LoginLog(Model):
|
|||
|
||||
class IPLimit(Model):
|
||||
ip = GenericIPAddressField()
|
||||
start = DateTimeField(auto_now_add=True)
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
delta = timedelta(seconds=int(getValue("core.auth.ratelimit.block", 3600)))
|
||||
return self.start + delta
|
||||
end = DateTimeField()
|
||||
reason = LongCharField(null=True, blank=True)
|
||||
|
|
|
@ -15,17 +15,17 @@ class ProductGroup(Model):
|
|||
class Product(Model):
|
||||
name = LongCharField()
|
||||
description = TextField(null=True, blank=True)
|
||||
handler_module = LongCharField(null=True, blank=True)
|
||||
product_type = LongCharField(null=True, blank=True)
|
||||
product_groups = ManyToManyField(ProductGroup)
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
if self.handler_module:
|
||||
if self.product_type:
|
||||
try:
|
||||
handler_module = import_module(self.handler_module)
|
||||
return handler_module.ProductRouter(self.id)
|
||||
product_type = import_module(self.product_type)
|
||||
return product_type.ProductRouter(self.id)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not load product handler {self.handler_module} for product {self.id}: {e}")
|
||||
logger.error(f"Could not load product type {self.product_type} for product {self.id}: {e}")
|
||||
return None
|
||||
|
||||
class ProductPlan(Model):
|
||||
|
|
|
@ -2,7 +2,7 @@ import importlib
|
|||
|
||||
from django.conf import settings
|
||||
|
||||
from core.classes.navigation import NavItem, NavSection, Navigation
|
||||
from core.classes.navigation import Navigation
|
||||
|
||||
navigations = {
|
||||
"backend_main": Navigation(),
|
||||
|
|
15
core/modules/products.py
Normal file
15
core/modules/products.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import importlib
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
producttypes = {}
|
||||
|
||||
for module in ["core"] + settings.EXPEPHALON_MODULES:
|
||||
try:
|
||||
mop = importlib.import_module(f"{module}.products")
|
||||
for name, ptype in mop.PRODUCTTYPES:
|
||||
if name in producttypes.keys:
|
||||
raise ValueError(f"Error in {module}: Product type of name {name} already exists!")
|
||||
producttypes[name] = ptype
|
||||
except (AttributeError, ModuleNotFoundError):
|
||||
continue
|
|
@ -25,6 +25,10 @@ from core.views import (
|
|||
BrandDeleteView,
|
||||
BrandEditView,
|
||||
BrandListView,
|
||||
RateLimitCreateView,
|
||||
RateLimitDeleteView,
|
||||
RateLimitEditView,
|
||||
RateLimitListView,
|
||||
)
|
||||
|
||||
URLPATTERNS = []
|
||||
|
@ -65,6 +69,13 @@ URLPATTERNS.append(path("admin/brands/<pk>/delete/", BrandDeleteView.as_view(),
|
|||
URLPATTERNS.append(path("admin/brands/<pk>/edit/", BrandEditView.as_view(), name="brands_edit"))
|
||||
URLPATTERNS.append(path("admin/brands/create/", BrandCreateView.as_view(), name="brands_create"))
|
||||
|
||||
# Rate Limit Administration URLs
|
||||
|
||||
URLPATTERNS.append(path('admin/firewall/', RateLimitListView.as_view(), name="ratelimits"))
|
||||
URLPATTERNS.append(path("admin/firewall/<pk>/delete/", RateLimitDeleteView.as_view(), name="ratelimits_delete"))
|
||||
URLPATTERNS.append(path("admin/firewall/<pk>/edit/", RateLimitEditView.as_view(), name="ratelimits_edit"))
|
||||
URLPATTERNS.append(path("admin/firewall/create/", RateLimitCreateView.as_view(), name="ratelimits_create"))
|
||||
|
||||
# External Module URLs
|
||||
|
||||
for module in settings.EXPEPHALON_MODULES:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from core.modules.navigation import navigations, NavSection, NavItem
|
||||
from core.modules.navigation import navigations
|
||||
from core.classes.navigation import NavSection, NavItem
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -90,6 +91,7 @@ administration_section = NavSection("Administration", "")
|
|||
|
||||
user_administration_item = NavItem("Administrator Users", "fa-users-cog", "admins")
|
||||
brand_administration_item = NavItem("Brands", "fa-code-branch", "brands")
|
||||
ratelimit_administration_item = NavItem("Firewall", "fa-shield-alt", "ratelimits")
|
||||
sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni")
|
||||
otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "backendni")
|
||||
backup_administration_item = NavItem("Backups", "fa-shield-alt", "backendni")
|
||||
|
@ -100,6 +102,7 @@ dbsettings_item = NavItem("Database Settings", "fa-database", "dbsettings")
|
|||
|
||||
administration_section.add_item(user_administration_item)
|
||||
administration_section.add_item(brand_administration_item)
|
||||
administration_section.add_item(ratelimit_administration_item)
|
||||
administration_section.add_item(sms_administration_item)
|
||||
administration_section.add_item(otp_administration_item)
|
||||
administration_section.add_item(backup_administration_item)
|
||||
|
|
17
core/products.py
Normal file
17
core/products.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from core.classes.products import BaseProductType
|
||||
|
||||
PRODUCTTYPES = {}
|
||||
|
||||
class CoreProductType(BaseProductType):
|
||||
@property
|
||||
def name(self):
|
||||
return "Basic Product"
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return "core.products.core"
|
||||
|
||||
def is_creatable(self):
|
||||
return True
|
||||
|
||||
PRODUCTTYPES[CoreProductType().id()] = CoreProductType
|
|
@ -7,6 +7,7 @@ from core.views.auth import *
|
|||
from core.views.profiles import *
|
||||
from core.views.generic import *
|
||||
from core.views.brands import *
|
||||
from core.views.firewall import *
|
||||
from core.mixins.auth import AdminMixin
|
||||
|
||||
# Create your views here.
|
||||
|
|
|
@ -5,6 +5,8 @@ from django.shortcuts import redirect
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib import messages
|
||||
from django.utils import timezone
|
||||
from django.db.models import Max
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm
|
||||
from core.models.auth import LoginSession, PWResetToken, IPLimit, LoginLog
|
||||
|
@ -18,25 +20,25 @@ from dbsettings.functions import getValue
|
|||
class RateLimitedView(TemplateView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/ratelimit.html"
|
||||
|
||||
@never_cache
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
for iplimit in list(IPLimit.objects.filter(ip=get_client_ip(request))):
|
||||
if iplimit.end >= timezone.now():
|
||||
messages.error(request, f"Sorry, there have been to many failed login attempts from your IP. Please try again after {str(iplimit.end)}, or contact support if you need help getting into your account.")
|
||||
try:
|
||||
limit = IPLimit.objects.filter(ip=get_client_ip(request), end__gte=timezone.now()).first()
|
||||
messages.error(request, f"Sorry, your IP has been blocked, so you cannot login at the moment.{f' Reason: {limit.reason}' if limit.reason else ''} Please try again after {str(iplimit.end)}, or contact support if you need help getting into your account.")
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
except:
|
||||
return redirect("login")
|
||||
|
||||
class AuthView(FormView):
|
||||
@never_cache
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
limits = list(IPLimit.objects.filter(ip=get_client_ip(request)))
|
||||
|
||||
for limit in limits:
|
||||
if limit.end > timezone.now():
|
||||
if IPLimit.objects.filter(ip=get_client_ip(request), end__gte=timezone.now()):
|
||||
return redirect("ratelimited")
|
||||
|
||||
period = timezone.now() - timezone.timedelta(seconds=int(getValue("core.auth.ratelimit.period", 600)))
|
||||
failures = LoginLog.objects.filter(ip=get_client_ip(request), success=False, timestamp__gte=period)
|
||||
if len(failures) >= int(getValue("core.auth.ratelimit.attempts", 5)):
|
||||
IPLimit.objects.create(ip=get_client_ip(request))
|
||||
IPLimit.objects.create(ip=get_client_ip(request), end=timezone.now() + timezone.timedelta(seconds=getValue("core.auth.ratelimit.block", 3600)), reason="Too many failed login attempts.")
|
||||
return redirect("ratelimited")
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
|
|
@ -16,8 +16,8 @@ class BrandListView(BackendListView):
|
|||
class BrandEditView(BackendUpdateView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/brands/update.html"
|
||||
model = Brand
|
||||
success_url = reverse_lazy("dbsettings")
|
||||
fields = ["key", "value"]
|
||||
success_url = reverse_lazy("brands")
|
||||
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
@ -27,7 +27,7 @@ class BrandEditView(BackendUpdateView):
|
|||
class BrandDeleteView(BackendDeleteView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/brands/delete.html"
|
||||
model = Brand
|
||||
success_url = reverse_lazy("dbsettings")
|
||||
success_url = reverse_lazy("brands")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
@ -35,10 +35,10 @@ class BrandDeleteView(BackendDeleteView):
|
|||
return context
|
||||
|
||||
class BrandCreateView(BackendCreateView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/dbsettings/create.html"
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/brands/create.html"
|
||||
model = Brand
|
||||
success_url = reverse_lazy("dbsettings")
|
||||
fields = ["key", "value"]
|
||||
success_url = reverse_lazy("brands")
|
||||
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
|
46
core/views/firewall.py
Normal file
46
core/views/firewall.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from core.models.auth import IPLimit
|
||||
from core.views.generic import BackendListView, BackendUpdateView, BackendDeleteView, BackendCreateView
|
||||
|
||||
class RateLimitListView(BackendListView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/index.html"
|
||||
model = IPLimit
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = "Firewall Settings"
|
||||
return context
|
||||
|
||||
class RateLimitEditView(BackendUpdateView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/update.html"
|
||||
model = IPLimit
|
||||
success_url = reverse_lazy("ratelimits")
|
||||
fields = ["ip", "end", "reason"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = "Edit Rule"
|
||||
return context
|
||||
|
||||
class RateLimitDeleteView(BackendDeleteView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/delete.html"
|
||||
model = IPLimit
|
||||
success_url = reverse_lazy("ratelimits")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = "Delete Rule"
|
||||
return context
|
||||
|
||||
class RateLimitCreateView(BackendCreateView):
|
||||
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/create.html"
|
||||
model = IPLimit
|
||||
success_url = reverse_lazy("ratelimits")
|
||||
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = "Create Rule"
|
||||
return context
|
1
start_celery.sh
Normal file
1
start_celery.sh
Normal file
|
@ -0,0 +1 @@
|
|||
celery -A expephalon worker --loglevel=info
|
1
start_celery_beat.sh
Normal file
1
start_celery_beat.sh
Normal file
|
@ -0,0 +1 @@
|
|||
celery -A expephalon beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
57
templates/backend/firewall/create.html
Normal file
57
templates/backend/firewall/create.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "backend/base.html" %}
|
||||
{% load bootstrap4 %}
|
||||
{% block content %}
|
||||
<div class="app-page-title">
|
||||
<div class="page-title-wrapper">
|
||||
<div class="page-title-heading">
|
||||
<div class="page-title-icon">
|
||||
<i class="fa fa-users-cog">
|
||||
</i>
|
||||
</div>
|
||||
<div>Firewall - Create Rule
|
||||
<div class="page-title-subheading">Create a new firewall rule
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-title-actions">
|
||||
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
|
||||
<i class="fa fa-plus"></i> New Rule
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-lg-0">
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header-tab card-header-tab-animation card-header">
|
||||
<div class="card-header-title">
|
||||
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
|
||||
Create Rule
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tabs-eg-77">
|
||||
<form method="POST" enctype="multipart/form-data" >
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn-shadow mr-3 btn btn-success">
|
||||
<i class="fa fa-check"></i> Save
|
||||
</button>
|
||||
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
|
||||
<i class="fa fa-times"></i> Cancel
|
||||
</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
57
templates/backend/firewall/delete.html
Normal file
57
templates/backend/firewall/delete.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "backend/base.html" %}
|
||||
{% load bootstrap4 %}
|
||||
{% block content %}
|
||||
<div class="app-page-title">
|
||||
<div class="page-title-wrapper">
|
||||
<div class="page-title-heading">
|
||||
<div class="page-title-icon">
|
||||
<i class="fa fa-database">
|
||||
</i>
|
||||
</div>
|
||||
<div>Firewall Rules - Delete Rule
|
||||
<div class="page-title-subheading">Delete a rule from the system
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-title-actions">
|
||||
<button type="button" data-toggle="tooltip" title="New Rule" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
|
||||
<i class="fa fa-plus"></i> New Rule
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-lg-0">
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header-tab card-header-tab-animation card-header">
|
||||
<div class="card-header-title">
|
||||
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
|
||||
Deleting rule for {{ object.ip }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tabs-eg-77">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
Are you sure you wish to delete the rule for {{ object.ip }}?
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn-shadow mr-3 btn btn-success">
|
||||
<i class="fa fa-check"></i> Save
|
||||
</button>
|
||||
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
|
||||
<i class="fa fa-times"></i> Cancel
|
||||
</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
63
templates/backend/firewall/index.html
Normal file
63
templates/backend/firewall/index.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
{% extends "backend/base.html" %}
|
||||
{% block content %}
|
||||
<div class="app-page-title">
|
||||
<div class="page-title-wrapper">
|
||||
<div class="page-title-heading">
|
||||
<div class="page-title-icon">
|
||||
<i class="fa fa-shield-alt">
|
||||
</i>
|
||||
</div>
|
||||
<div>Firewall Rules
|
||||
<div class="page-title-subheading">Create, edit and delete rules
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-title-actions">
|
||||
<a href="{% url "ratelimits_create" %}" type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
|
||||
<i class="fa fa-plus"></i> New Rule
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-lg-0">
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header-tab card-header-tab-animation card-header">
|
||||
<div class="card-header-title">
|
||||
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
|
||||
Active Rules
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tabs-eg-77">
|
||||
<div class="card mb-3 widget-chart widget-chart2 text-left w-100">
|
||||
<table class="mb-0 table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>End of block</th>
|
||||
<th>Reason</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rule in object_list %}
|
||||
<tr>
|
||||
<td>{{ rule.ip }}</td>
|
||||
<td>{{ rule.end }}</td>
|
||||
<td>{{ rule.reason }}</td>
|
||||
<td><a href="{% url "ratelimits_edit" rule.id %}"><i class="fas fa-edit" title="Edit Rule"></i></a> <a href="{% url "ratelimits_delete" rule.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Rule"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
57
templates/backend/firewall/update.html
Normal file
57
templates/backend/firewall/update.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "backend/base.html" %}
|
||||
{% load bootstrap4 %}
|
||||
{% block content %}
|
||||
<div class="app-page-title">
|
||||
<div class="page-title-wrapper">
|
||||
<div class="page-title-heading">
|
||||
<div class="page-title-icon">
|
||||
<i class="fa fa-users-cog">
|
||||
</i>
|
||||
</div>
|
||||
<div>Brands - Edit Brand
|
||||
<div class="page-title-subheading">Edit brand properties
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-title-actions">
|
||||
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
|
||||
<i class="fa fa-plus"></i> New Brand
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-lg-0">
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header-tab card-header-tab-animation card-header">
|
||||
<div class="card-header-title">
|
||||
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
|
||||
Editing {{ object.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tabs-eg-77">
|
||||
<form method="POST" enctype="multipart/form-data" >
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn-shadow mr-3 btn btn-success">
|
||||
<i class="fa fa-check"></i> Save
|
||||
</button>
|
||||
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
|
||||
<i class="fa fa-times"></i> Cancel
|
||||
</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in a new issue