update bans page

*add remain and remain period
*create table with bans data
*add ip ban number + top reason
*add keyword, reason and period filter
This commit is contained in:
Jordan Blasenhauer 2024-01-19 14:08:30 +01:00
parent 7707aab9a8
commit 3edff50047
5 changed files with 546 additions and 51 deletions

View file

@ -48,7 +48,7 @@ from src.Config import Config
from src.ReverseProxied import ReverseProxied
from src.User import AnonymousUser, User
from utils import check_settings, get_b64encoded_qr_image, path_to_dict
from utils import check_settings, get_b64encoded_qr_image, path_to_dict, get_remain, get_period_from_remain
from Database import Database # type: ignore
from logging import getLogger
@ -1605,26 +1605,22 @@ def block_requests():
{"ip": "124.0.0.3", "url": "/test", "date": "12/51/9851", "reason": "antibot", "method": "GET", "status": 403, "data": "{fesfmk fesfsf sfesfes}"},
]
# Get top reasons
# Prepare data
reasons = {}
codes = {}
for request in requests:
# Get top reasons
if not request["reason"] in reasons:
reasons[request["reason"]] = 0
reasons[request["reason"]] = reasons[request["reason"]] + 1
top_reason = [k for k, v in reasons.items() if v == max(reasons.values())][0]
# Get top status code
codes = {}
for request in requests:
# Get top status code
if not request["status"] in codes:
codes[request["status"]] = 0
codes[request["status"]] = codes[request["status"]] + 1
top_reason = [k for k, v in reasons.items() if v == max(reasons.values())][0]
top_code = [k for k, v in codes.items() if v == max(codes.values())][0]
# Get top reason and status
return render_template(
"block_requests.html",
block_requests=[
@ -1643,9 +1639,34 @@ def block_requests():
@login_required
def bans():
# TODO : Get bans list from database and send it
bans = [
{"ip": "124.0.0.1", "ban_start": 1705663430, "ban_end": 1705675421, "reason": "antibot"},
{"ip": "124.0.0.2", "ban_start": 1705663430, "ban_end": 1705685421, "reason": "test"},
{"ip": "124.0.0.3", "ban_start": 1705663430, "ban_end": 1705664748, "reason": "unknown"},
]
# Prepare data
reasons = {}
now_stamp = int(time())
for ban in bans:
# Add remain
ban["remain"] = "unknown" if ban["ban_end"] - now_stamp < 0 else get_remain(ban["ban_end"] - now_stamp)
ban["period"] = get_period_from_remain(ban["remain"])
# Convert stamp to date
ban["ban_start"] = datetime.fromtimestamp(ban["ban_start"])
ban["ban_end"] = datetime.fromtimestamp(ban["ban_end"])
# Get top reason
if not ban["reason"] in reasons:
reasons[ban["reason"]] = 0
reasons[ban["reason"]] = reasons[ban["reason"]] + 1
top_reason = [k for k, v in reasons.items() if v == max(reasons.values())][0]
return render_template(
"bans.html",
bans=[],
bans=bans,
top_reason=top_reason,
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)

File diff suppressed because one or more lines are too long

View file

@ -1,16 +1,302 @@
function addReasonOption(endpoint, reasons) {
let content = "";
class Filter {
constructor(prefix = "bans") {
this.prefix = prefix;
this.container = document.querySelector(`[data-${this.prefix}-filter]`);
this.keyInp = document.querySelector("input#keyword");
this.periodValue = "all";
this.reasonValue = "all";
this.initHandler();
}
reasons.forEach((reason, id) => {
content += `<button
role="option"
data-${endpoint}-setting-select-dropdown-btn="reason"
value="${reason}"
class="${
id === reasons.length - 1 ? "rounded-b" : ""
} border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 bg-white text-gray-700 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
${reason}
</button>`;
});
initHandler() {
// REASON HANDLER
+this.container.addEventListener("click", (e) => {
try {
if (
e.target
.closest("button")
.getAttribute(`data-${this.prefix}-setting-select-dropdown-btn`) ===
"reason"
) {
setTimeout(() => {
const value = document
.querySelector(
`[data-${this.prefix}-setting-select-text="reason"]`
)
.textContent.trim();
this.reasonValue = value;
//run filter
this.filter();
}, 10);
}
} catch (err) {}
});
// PERIOD HANDLER
+this.container.addEventListener("click", (e) => {
try {
if (
e.target
.closest("button")
.getAttribute(`data-${this.prefix}-setting-select-dropdown-btn`) ===
"period"
) {
setTimeout(() => {
const value = document
.querySelector(
`[data-${this.prefix}-setting-select-text="period"]`
)
.textContent.trim();
this.periodValue = value;
//run filter
this.filter();
}, 10);
}
} catch (err) {}
});
//KEYWORD HANDLER
this.keyInp.addEventListener("input", (e) => {
this.filter();
});
}
filter() {
const bans = document.querySelector(`[data-${this.prefix}-list]`).children;
if (bans.length === 0) return;
//reset
for (let i = 0; i < bans.length; i++) {
const el = bans[i];
el.classList.remove("hidden");
}
//filter type
this.setFilterKeyword(bans);
this.setFilterReason(bans);
this.setFilterPeriod(bans);
}
setFilterKeyword(bans) {
const keyword = this.keyInp.value.trim().toLowerCase();
if (!keyword) return;
for (let i = 0; i < bans.length; i++) {
const el = bans[i];
const ip = this.getElAttribut(el, "ip");
const banStart = this.getElAttribut(el, "ban_sart");
const banEnd = this.getElAttribut(el, "ban_end");
const remain = this.getElAttribut(el, "remain");
if (
!ip.includes(keyword) &&
!banStart.includes(keyword) &&
!banEnd.includes(keyword) &&
!remain.includes(keyword)
)
el.classList.add("hidden");
}
}
setFilterPeriod(bans) {
if (this.periodValue === "all") return;
for (let i = 0; i < bans.length; i++) {
const el = bans[i];
const type = this.getElAttribut(el, "period");
if (type !== this.periodValue) el.classList.add("hidden");
}
}
setFilterReason(bans) {
if (this.reasonValue === "all") return;
for (let i = 0; i < bans.length; i++) {
const el = bans[i];
const type = this.getElAttribut(el, "reason");
if (type !== this.reasonValue) el.classList.add("hidden");
}
}
getElAttribut(el, attr) {
return el
.querySelector(`[data-${this.prefix}-${attr}]`)
.getAttribute(`data-${this.prefix}-${attr}`)
.trim();
}
}
class Dropdown {
constructor(prefix = "bans") {
this.prefix = prefix;
this.container = document.querySelector("main");
this.lastDrop = "";
this.initDropdown();
}
initDropdown() {
this.container.addEventListener("click", (e) => {
//SELECT BTN LOGIC
try {
if (
e.target
.closest("button")
.hasAttribute(`data-${this.prefix}-setting-select`) &&
!e.target.closest("button").hasAttribute(`disabled`)
) {
const btnName = e.target
.closest("button")
.getAttribute(`data-${this.prefix}-setting-select`);
if (this.lastDrop !== btnName) {
this.lastDrop = btnName;
this.closeAllDrop();
}
this.toggleSelectBtn(e);
}
} catch (err) {}
//SELECT DROPDOWN BTN LOGIC
try {
if (
e.target
.closest("button")
.hasAttribute(`data-${this.prefix}-setting-select-dropdown-btn`)
) {
const btn = e.target.closest("button");
const btnValue = btn.getAttribute("value");
const btnSetting = btn.getAttribute(
`data-${this.prefix}-setting-select-dropdown-btn`
);
//stop if same value to avoid new fetching
const isSameVal = this.isSameValue(btnSetting, btnValue);
if (isSameVal) return this.hideDropdown(btnSetting);
//else, add new value to custom
this.setSelectNewValue(btnSetting, btnValue);
//close dropdown and change style
this.hideDropdown(btnSetting);
if (
!e.target.closest("button").hasAttribute(`data-${this.prefix}-file`)
) {
this.changeDropBtnStyle(btnSetting, btn);
}
//show / hide filter
if (btnSetting === "instances") {
this.hideFilterOnLocal(btn.getAttribute("data-_type"));
}
}
} catch (err) {}
});
}
closeAllDrop() {
const drops = document.querySelectorAll(
`[data-${this.prefix}-setting-select-dropdown]`
);
drops.forEach((drop) => {
drop.classList.add("hidden");
drop.classList.remove("flex");
document
.querySelector(
`svg[data-${this.prefix}-setting-select="${drop.getAttribute(
`data-${this.prefix}-setting-select-dropdown`
)}"]`
)
.classList.remove("rotate-180");
});
}
isSameValue(btnSetting, value) {
const selectCustom = document.querySelector(
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`
);
const currVal = selectCustom.textContent;
return currVal === value ? true : false;
}
setSelectNewValue(btnSetting, value) {
const selectCustom = document.querySelector(
`[data-${this.prefix}-setting-select="${btnSetting}"]`
);
selectCustom.querySelector(
`[data-${this.prefix}-setting-select-text]`
).textContent = value;
}
hideDropdown(btnSetting) {
//hide dropdown
const dropdownEl = document.querySelector(
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
);
dropdownEl.classList.add("hidden");
dropdownEl.classList.remove("flex");
//svg effect
const dropdownChevron = document.querySelector(
`svg[data-${this.prefix}-setting-select="${btnSetting}"]`
);
dropdownChevron.classList.remove("rotate-180");
}
changeDropBtnStyle(btnSetting, selectedBtn) {
const dropdownEl = document.querySelector(
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
);
//reset dropdown btns
const btnEls = dropdownEl.querySelectorAll("button");
btnEls.forEach((btn) => {
btn.classList.remove(
"bg-primary",
"dark:bg-primary",
"text-gray-300",
"text-gray-300"
);
btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
});
//highlight clicked btn
selectedBtn.classList.remove(
"bg-white",
"dark:bg-slate-700",
"text-gray-700"
);
selectedBtn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
}
toggleSelectBtn(e) {
const attribute = e.target
.closest("button")
.getAttribute(`data-${this.prefix}-setting-select`);
//toggle dropdown
const dropdownEl = document.querySelector(
`[data-${this.prefix}-setting-select-dropdown="${attribute}"]`
);
const dropdownChevron = document.querySelector(
`svg[data-${this.prefix}-setting-select="${attribute}"]`
);
dropdownEl.classList.toggle("hidden");
dropdownEl.classList.toggle("flex");
dropdownChevron.classList.toggle("rotate-180");
}
//hide date filter on local
hideFilterOnLocal(type) {
if (type === "local") {
this.hideInp(`input#from-date`);
this.hideInp(`input#to-date`);
}
if (type !== "local") {
this.showInp(`input#from-date`);
this.showInp(`input#to-date`);
}
}
showInp(selector) {
document.querySelector(selector).closest("div").classList.add("flex");
document.querySelector(selector).closest("div").classList.remove("hidden");
}
hideInp(selector) {
document.querySelector(selector).closest("div").classList.add("hidden");
document.querySelector(selector).closest("div").classList.remove("flex");
}
}
const setDropdown = new Dropdown();
const setFilter = new Filter();

View file

@ -1,5 +1,18 @@
{% extends "base.html" %} {% block content %} {% set current_endpoint =
url_for(request.endpoint)[1:].split("/")[-1].strip() %}
{% set reasons = [] %}
{% set periods = [] %}
{% for ban in bans %}
{% if ban["reason"] not in reasons %}
{% set reasons = reasons.append(ban["reason"]) %}
{% endif %}
{% if ban["period"] not in periods %}
{% set periods = periods.append(ban["period"]) %}
{% endif %}
{% endfor %}
<!-- info-->
<div
class="col-span-12 md:col-span-4 3xl:col-span-3 p-4 relative min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
@ -17,6 +30,18 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
{{bans|length}}
</p>
</div>
<div class="mx-1 flex items-center my-4">
<p
class="transition duration-300 ease-in-out font-bold mb-0 font-sans text-sm leading-normal uppercase dark:text-gray-500 dark:opacity-80"
>
TOP REASON
</p>
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
{{top_reason}}
</p>
</div>
</div>
<!-- end info -->
@ -27,8 +52,8 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
>
<h5 class="mb-2 font-bold dark:text-white/90">FILTER</h5>
<div class="mx-2 grid grid-cols-12 gap-x-4 gap-y-2">
<!-- search inpt-->
<div class="flex flex-col relative col-span-12 md:col-span-6">
<!-- search inpt-->
<div class="flex flex-col relative col-span-12 md:col-span-6">
<h5
class="my-1 transition duration-300 ease-in-out dark:opacity-90 text-sm sm:text-md font-bold m-0 dark:text-gray-300"
>
@ -40,7 +65,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
id="keyword"
name="keyword"
class="dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-3 py-1 font-normal text-gray-700 transition-all placeholder:text-gray-500"
placeholder="keyword"
placeholder="ip, ban start, ban end"
pattern="(.*?)"
required
/>
@ -94,10 +119,82 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
>
all
</button>
{% for reason in reasons %}
<button
role="option"
data-{{current_endpoint}}-setting-select-dropdown-btn="reason"
value="{{reason}}"
class="{% if loop.last %}rounded-b{%endif%} border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 bg-white text-gray-700 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
{{reason}}
</button>
{% endfor %}
</div>
<!-- end dropdown-->
</div>
<!-- end select success -->
<!-- end select reason -->
<!-- select period -->
<div class="flex flex-col relative col-span-12 md:col-span-6">
<h5
class="my-1 transition duration-300 ease-in-out dark:opacity-90 text-sm sm:text-md font-bold m-0 dark:text-gray-300"
>
Period
</h5>
<button
aria-controls="filter-state"
data-{{current_endpoint}}-setting-select="period"
class="disabled:opacity-75 dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 focus:border-green-500 flex justify-between align-middle items-center text-left text-sm leading-5.6 ease w-full rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 font-normal text-gray-700 transition-all placeholder:text-gray-500"
>
<span
aria-description="current filter state value"
id="{{current_endpoint}}-period"
data-name="{{current_endpoint}}-period"
data-{{current_endpoint}}-setting-select-text="period"
>all</span
>
<!-- chevron -->
<svg
data-{{current_endpoint}}-setting-select="period"
class="transition-transform h-4 w-4 fill-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
</button>
<!-- end chevron -->
<!-- dropdown-->
<div
id="filter-state"
role="listbox"
data-{{current_endpoint}}-setting-select-dropdown="period"
class="hidden z-100 absolute h-full flex-col w-full translate-y-16"
>
<button
role="option"
data-{{current_endpoint}}-setting-select-dropdown-btn="period"
value="all"
class="border-t rounded-t border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:text-gray-300 dark:bg-primary bg-primary text-gray-300"
>
all
</button>
{% for period in periods %}
<button
role="option"
data-{{current_endpoint}}-setting-select-dropdown-btn="period"
value="{{period}}"
class="{% if loop.last %}rounded-b{%endif%} border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 bg-white text-gray-700 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
{{period}}
</button>
{% endfor %}
</div>
<!-- end dropdown-->
</div>
<!-- end select period -->
</div>
</div>
<!-- end filter -->
@ -108,15 +205,15 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
<div data-{{current_endpoint}}-bans-list>
<h5 class="mb-4 mt-2 font-bold dark:text-white/90 mx-2">BANS LIST</h5>
<!-- list container-->
<div class="min-w-[900px] w-full grid grid-cols-12 rounded p-2">
<div class="min-w-[1150px] w-full grid grid-cols-12 rounded p-2">
<!-- header-->
<p
class="dark:text-gray-300 h-8 text-sm font-bold col-span-3 m-0 pb-2 border-b border-gray-400"
class="dark:text-gray-300 h-8 text-sm font-bold col-span-1 m-0 pb-2 border-b border-gray-400"
>
Select
</p>
<p
class="dark:text-gray-300 h-8 text-sm font-bold col-span-3 m-0 pb-2 border-b border-gray-400"
class="dark:text-gray-300 h-8 text-sm font-bold col-span-2 m-0 pb-2 border-b border-gray-400"
>
IP
</p>
@ -126,58 +223,79 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
Reason
</p>
<p
class="dark:text-gray-300 h-8 text-sm font-bold col-span-1 m-0 pb-2 border-b border-gray-400"
class="dark:text-gray-300 h-8 text-sm font-bold col-span-2 m-0 pb-2 border-b border-gray-400"
>
Ban start
</p>
<p
class="dark:text-gray-300 h-8 text-sm font-bold col-span-1 m-0 pb-2 border-b border-gray-400"
class="dark:text-gray-300 h-8 text-sm font-bold col-span-2 m-0 pb-2 border-b border-gray-400"
>
Ban end
</p>
<p
class="dark:text-gray-300 h-8 text-sm font-bold col-span-3 m-0 pb-2 border-b border-gray-400"
>
Remain
</p>
<p
class="dark:text-gray-300 h-8 text-sm font-bold col-span-1 m-0 pb-2 border-b border-gray-400"
>
Period
</p>
<!-- end header-->
<!-- list -->
<ul class="col-span-12 w-full" data-{{current_endpoint}}-list>
{% for request, value in bans.items() %}
{% for ban in bans %}
<li
data-{{current_endpoint}}-list-item="{{request}}"
data-{{current_endpoint}}-list-item-value="{{value}}"
data-{{current_endpoint}}-list-item="{{ban}}"
class="items-center grid grid-cols-12 border-b border-gray-300 py-2.5"
>
<p
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-3 m-0 my-1"
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1 pl-2"
data-{{current_endpoint}}-select
>
<input type="checkbox" name="{{request}}" value="{{request}}" class="" />
<input class="checkbox" type="checkbox" name="{{ban.__name__}}" value="{{ban.__name__}}" class="" />
</p>
<p
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-3 m-0 my-1"
data-{{current_endpoint}}-ip
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-2 m-0 my-1"
data-{{current_endpoint}}-ip="{{ban["ip"]}}"
>
{{value['ip']}}
{{ban['ip']}}
</p>
<p
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1"
data-{{current_endpoint}}-reason
data-{{current_endpoint}}-reason="{{ban["reason"]}}"
>
{{value["reason"]}}
{{ban["reason"]}}
</p>
<p
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1"
data-{{current_endpoint}}-ban_start
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-2 m-0 my-1"
data-{{current_endpoint}}-ban_start="{{ban["ban_start"]}}"
>
{{value["ban_start"]}}
{{ban["ban_start"]}}
</p>
<p
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1"
data-{{current_endpoint}}-ban_end
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-2 m-0 my-1"
data-{{current_endpoint}}-ban_end="{{ban["ban_end"]}}"
>
{{value["ban_end"]}}
{{ban["ban_end"]}}
</p>
<p
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-3 m-0 my-1"
data-{{current_endpoint}}-remain="{{ban["remain"]}}"
>
{{ban["remain"]}}
</p>
<p
class="dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1"
data-{{current_endpoint}}-period="{{ban["period"]}}"
>
{{ban["period"]}}
</p>
</li>
{% endfor %}

View file

@ -7,6 +7,76 @@ from typing import List, Optional
from qrcode.main import QRCode
import math
def get_remain(stamp):
# Convert to milliseconds if not
time = str(stamp)
length = len(time)
if length < 13:
missing = 13 - length
print(missing)
for i in range(missing):
time = time + "0"
# Get remain
ms = int(time)
seconds = math.floor(ms / 1000)
minutes = math.floor(seconds / 60)
hours = math.floor(minutes / 60)
days = math.floor(hours / 24)
months = math.floor(days / 30)
years = math.floor(days / 365)
seconds %= 60
minutes %= 60
hours %= 24
days %= 30
months %= 12
return f"{years}y {months}m {days}d {hours}h {minutes}min {seconds}s"
def get_period_from_remain(remain):
# Data, need format <n>y <n>m <n>d <n>h <n>min <n>s
periods = remain.split(" ")
period = "unknown"
formats = ["years", "months", "days", "hours", "minutes", "seconds"]
chars = ["y", "min", "m", "d", "h", "s"]
# Case not right format
if len(periods) != 6:
return period
# start from seconds to years, stop when first 0 occurence
# The remain period is first 0 occurence - 1
for i in range(len(periods)):
# remove letter
num = periods[len(periods) - 1 - i]
for char in chars:
num = num.replace(char, "")
num = "0" if not num else num
num = int(num)
# Case seconds or less
if not num and i == 0:
period = formats[len(formats) - 1]
break
# Case years period
if num and i == (len(periods) - 1):
period = formats[0]
break
# Case between seconds and years
if not num:
period = formats[len(formats) - i]
break
return period
def path_to_dict(
path: str,