mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Merge pull request #889 from bunkerity/dev
Merge banch "dev" into branch "ui"
This commit is contained in:
commit
07dce6a265
23 changed files with 525 additions and 7351 deletions
|
|
@ -170,6 +170,10 @@ Another core component of BunkerWeb is the ModSecurity Web Application Firewall
|
|||
|
||||
## Database
|
||||
|
||||
<p align="center">
|
||||
<img alt="Database model" src="https://github.com/bunkerity/bunkerweb/raw/v1.5.5/docs/assets/img/bunkerweb_db.svg" />
|
||||
</p>
|
||||
|
||||
State of the current configuration of BunkerWeb is stored in a backend database which contains the following data :
|
||||
|
||||
- Settings defined for all the services
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 903 KiB |
|
|
@ -1,10 +1,12 @@
|
|||
# Quickstart guide
|
||||
|
||||
!!! info "Prerequisites"
|
||||
|
||||
We assume that you're already familiar with the [core concepts](concepts.md) and you have followed the [integrations instructions](integrations.md) for your environment.
|
||||
|
||||
!!! tip "Going further"
|
||||
To demonstrate the use of BunkerWeb, we will deploy a dummy "Hello World" web application as an example. See the [examples folder](https://github.com/bunkerity/bunkerweb/tree/v1.5.5/examples) of the repository to get real-world examples.
|
||||
|
||||
To demonstrate the use of BunkerWeb, we will deploy a dummy "Hello World" web application as an example. See the [examples folder](https://github.com/bunkerity/bunkerweb/tree/v1.5.5/examples) of the repository to get real-world examples.
|
||||
|
||||
## Protect HTTP applications
|
||||
|
||||
|
|
@ -1117,7 +1119,8 @@ REAL_IP_HEADER=proxy_protocol
|
|||
## Protect UDP/TCP applications
|
||||
|
||||
!!! warning "Feature is in beta"
|
||||
This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository.
|
||||
|
||||
This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository.
|
||||
|
||||
BunkerWeb offers the capability to function as a **generic UDP/TCP reverse proxy**, allowing you to protect any network-based applications operating at least on layer 4 of the OSI model. Instead of utilizing the "classical" HTTP module, BunkerWeb leverages the [stream module](https://nginx.org/en/docs/stream/ngx_stream_core_module.html) of NGINX.
|
||||
|
||||
|
|
@ -2329,7 +2332,8 @@ BunkerWeb supports PHP using external or remote [PHP-FPM](https://www.php.net/ma
|
|||
## IPv6
|
||||
|
||||
!!! warning "Feature is in beta"
|
||||
This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository.
|
||||
|
||||
This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository.
|
||||
|
||||
By default, BunkerWeb will only listen on IPv4 addresses and won't use IPv6 for network communications. If you want to enable IPv6 support, you need to set `USE_IPV6=yes`. Please note that IPv6 configuration of your network and environment is out-of-the-scope of this documentation.
|
||||
|
||||
|
|
|
|||
|
|
@ -636,6 +636,7 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th
|
|||
labels:
|
||||
app: bunkerweb-ui
|
||||
spec:
|
||||
serviceAccountName: sa-bunkerweb
|
||||
containers:
|
||||
- name: bunkerweb-ui
|
||||
image: bunkerity/bunkerweb-ui:1.5.5
|
||||
|
|
@ -1475,6 +1476,7 @@ After a successful login/password combination, you will be prompted to enter you
|
|||
labels:
|
||||
app: bunkerweb-ui
|
||||
spec:
|
||||
serviceAccountName: sa-bunkerweb
|
||||
containers:
|
||||
- name: bunkerweb-ui
|
||||
image: bunkerity/bunkerweb-ui:1.5.5
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ utils.get_reason = function(ctx)
|
|||
end
|
||||
local banned, _ = datastore:get("bans_ip_" .. ip)
|
||||
if banned then
|
||||
return banned, {}
|
||||
return decode(banned)["reason"], {}
|
||||
end
|
||||
-- unknown
|
||||
if ngx.status == utils.get_deny_status() then
|
||||
|
|
@ -645,9 +645,9 @@ utils.is_banned = function(ip)
|
|||
local ok, ttl = datastore:ttl("bans_ip_" .. ip)
|
||||
local ban_data = decode(result)
|
||||
if not ok then
|
||||
return true, ban_data, -1
|
||||
return true, ban_data["reason"], -1
|
||||
end
|
||||
return true, ban_data, ttl
|
||||
return true, ban_data["reason"], ttl
|
||||
end
|
||||
-- Redis case
|
||||
local use_redis, err = utils.get_variable("USE_REDIS", false)
|
||||
|
|
@ -694,7 +694,7 @@ utils.is_banned = function(ip)
|
|||
if not ok then
|
||||
return nil, "datastore:set() error : " .. err
|
||||
end
|
||||
return true, data[1], data[2]
|
||||
return true, decode(data[1])["reason"], data[2]
|
||||
end
|
||||
clusterstore:close()
|
||||
return false, "not banned"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,16 @@
|
|||
"label": "The database URI",
|
||||
"regex": "^(postgresql|mysql|mariadb|sqlite|oracle)(\\+[\\w\\-]+)?:.+$",
|
||||
"type": "text"
|
||||
},
|
||||
"DATABASE_LOG_LEVEL": {
|
||||
"context": "global",
|
||||
"default": "warning",
|
||||
"help": "The level to use for database logs.",
|
||||
"id": "database-log-level",
|
||||
"label": "Database log level",
|
||||
"regex": "^(debug|info|warn|warning|error)$",
|
||||
"type": "select",
|
||||
"select": ["debug", "info", "warn", "warning", "error"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
local cjson = require "cjson"
|
||||
local class = require "middleclass"
|
||||
local datastore = require "datastore"
|
||||
local datastore = require "bunkerweb.datastore"
|
||||
local plugin = require "bunkerweb.plugin"
|
||||
local utils = require "bunkerweb.utils"
|
||||
|
||||
|
|
@ -50,12 +50,12 @@ function metrics:log()
|
|||
end
|
||||
end
|
||||
local request = {
|
||||
date = self.ctx.bw.local_time,
|
||||
date = os.time(),
|
||||
ip = self.ctx.bw.remote_addr,
|
||||
country = country,
|
||||
method = self.ctx.bw.request_method,
|
||||
url = self.ctx.bw.request_uri,
|
||||
code = ngx.status,
|
||||
status = ngx.status,
|
||||
["user-agent"] = self.ctx.bw.http_user_agent or "",
|
||||
reason = reason,
|
||||
data = data,
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ basicConfig(
|
|||
level=default_level,
|
||||
)
|
||||
|
||||
getLogger("sqlalchemy.orm.mapper.Mapper").setLevel(default_level if default_level != INFO else WARNING)
|
||||
getLogger("sqlalchemy.orm.relationships.RelationshipProperty").setLevel(default_level if default_level != INFO else WARNING)
|
||||
getLogger("sqlalchemy.orm.strategies.LazyLoader").setLevel(default_level if default_level != INFO else WARNING)
|
||||
getLogger("sqlalchemy.pool.impl.QueuePool").setLevel(default_level if default_level != INFO else WARNING)
|
||||
getLogger("sqlalchemy.pool.impl.SingletonThreadPool").setLevel(default_level if default_level != INFO else WARNING)
|
||||
getLogger("sqlalchemy.engine.Engine").setLevel(default_level if default_level != INFO else WARNING)
|
||||
database_default_level = _nameToLevel.get(getenv("DATABASE_LOG_LEVEL", "WARNING").upper(), WARNING)
|
||||
|
||||
getLogger("sqlalchemy.orm.mapper.Mapper").setLevel(database_default_level)
|
||||
getLogger("sqlalchemy.orm.relationships.RelationshipProperty").setLevel(database_default_level)
|
||||
getLogger("sqlalchemy.orm.strategies.LazyLoader").setLevel(database_default_level)
|
||||
getLogger("sqlalchemy.pool.impl.QueuePool").setLevel(database_default_level)
|
||||
getLogger("sqlalchemy.pool.impl.SingletonThreadPool").setLevel(database_default_level)
|
||||
getLogger("sqlalchemy.engine.Engine").setLevel(database_default_level)
|
||||
|
||||
# Edit the default levels of the logging module
|
||||
addLevelName(CRITICAL, "🚨")
|
||||
|
|
|
|||
174
src/ui/main.py
174
src/ui/main.py
|
|
@ -31,6 +31,7 @@ from jinja2 import Template
|
|||
from kubernetes import client as kube_client
|
||||
from kubernetes import config as kube_config
|
||||
from kubernetes.client.exceptions import ApiException as kube_ApiException
|
||||
from redis import Redis, Sentinel
|
||||
from regex import compile as re_compile, match as regex_match
|
||||
from requests import get
|
||||
from shutil import move, rmtree
|
||||
|
|
@ -1599,47 +1600,15 @@ def logs_container(container_id):
|
|||
@app.route("/reports", methods=["GET"])
|
||||
@login_required
|
||||
def reports():
|
||||
# TODO : Get block requests from database to send it
|
||||
reports = [
|
||||
{
|
||||
"user_agent": "Version 0.6.1 - Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.5a) Gecko/20030728 Mozilla Firebird/0.6.1",
|
||||
"ip": "124.0.0.1",
|
||||
"country": "FR",
|
||||
"url": "/test",
|
||||
"date": "12/51/9851",
|
||||
"reason": "antibot",
|
||||
"method": "GET",
|
||||
"status": 403,
|
||||
"data": "{fesfmk fesfsf sfesfes}",
|
||||
},
|
||||
{
|
||||
"user_agent": "Version 0.6.1 - Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.5a) Gecko/20030728 Mozilla Firebird/0.6.1",
|
||||
"ip": "124.0.0.2",
|
||||
"country": "EN",
|
||||
"url": "/test",
|
||||
"date": "12/51/9851",
|
||||
"reason": "test",
|
||||
"method": "GET",
|
||||
"status": 403,
|
||||
"data": "{fesfmk fesfsf sfesfes}",
|
||||
},
|
||||
{
|
||||
"user_agent": "Version 0.6.1 - Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.5a) Gecko/20030728 Mozilla Firebird/0.6.1",
|
||||
"ip": "124.0.0.3",
|
||||
"country": "ES",
|
||||
"url": "/test",
|
||||
"date": "12/51/9851",
|
||||
"reason": "antibot",
|
||||
"method": "GET",
|
||||
"status": 403,
|
||||
"data": "{fesfmk fesfsf sfesfes}",
|
||||
},
|
||||
]
|
||||
reports = app.config["INSTANCES"].get_reports()
|
||||
total_reports = len(reports)
|
||||
reports = reports[:100]
|
||||
|
||||
# Prepare data
|
||||
reasons = {}
|
||||
codes = {}
|
||||
for report in reports:
|
||||
for i, report in enumerate(deepcopy(reports)):
|
||||
reports[i]["date"] = datetime.fromtimestamp(floor(reports[i]["date"])).strftime("%d/%m/%Y %H:%M:%S")
|
||||
# Get top reasons
|
||||
if not report["reason"] in reasons:
|
||||
reasons[report["reason"]] = 0
|
||||
|
|
@ -1649,12 +1618,13 @@ def reports():
|
|||
codes[report["status"]] = 0
|
||||
codes[report["status"]] = codes[report["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]
|
||||
top_reason = ([k for k, v in reasons.items() if v == max(reasons.values())] or [""])[0]
|
||||
top_code = ([k for k, v in codes.items() if v == max(codes.values())] or [""])[0]
|
||||
|
||||
return render_template(
|
||||
"reports.html",
|
||||
reports=reports,
|
||||
total_reports=total_reports,
|
||||
top_code=top_code,
|
||||
top_reason=top_reason,
|
||||
username=current_user.get_id(),
|
||||
|
|
@ -1665,6 +1635,76 @@ def reports():
|
|||
@app.route("/bans", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def bans():
|
||||
redis_client = None
|
||||
db_config = app.config["CONFIG"].get_config(methods=False)
|
||||
use_redis = db_config.get("USE_REDIS", "no") == "yes"
|
||||
redis_host = db_config.get("REDIS_HOST")
|
||||
if use_redis and redis_host:
|
||||
redis_port = db_config.get("REDIS_PORT", "6379")
|
||||
if not redis_port.isdigit():
|
||||
redis_port = "6379"
|
||||
redis_port = int(redis_port)
|
||||
|
||||
redis_db = db_config.get("REDIS_DB", "0")
|
||||
if not redis_db.isdigit():
|
||||
redis_db = "0"
|
||||
redis_db = int(redis_db)
|
||||
|
||||
redis_timeout = db_config.get("REDIS_TIMEOUT", "1000.0")
|
||||
try:
|
||||
redis_timeout = float(redis_timeout)
|
||||
except ValueError:
|
||||
redis_timeout = 1000.0
|
||||
|
||||
redis_keepalive_pool = db_config.get("REDIS_KEEPALIVE_POOL", "10")
|
||||
if not redis_keepalive_pool.isdigit():
|
||||
redis_keepalive_pool = "10"
|
||||
redis_keepalive_pool = int(redis_keepalive_pool)
|
||||
|
||||
redis_ssl = db_config.get("REDIS_SSL", "no") == "yes"
|
||||
username = db_config.get("REDIS_USERNAME", None) or None
|
||||
password = db_config.get("REDIS_PASSWORD", None) or None
|
||||
sentinel_hosts = db_config.get("REDIS_SENTINEL_HOSTS", [])
|
||||
|
||||
if isinstance(sentinel_hosts, str):
|
||||
sentinel_hosts = [host.split(":") if ":" in host else host for host in sentinel_hosts.split(" ") if host]
|
||||
|
||||
if sentinel_hosts:
|
||||
sentinel_username = db_config.get("REDIS_SENTINEL_USERNAME", None) or None
|
||||
sentinel_password = db_config.get("REDIS_SENTINEL_PASSWORD", None) or None
|
||||
sentinel_master = db_config.get("REDIS_SENTINEL_MASTER", "")
|
||||
|
||||
sentinel = Sentinel(
|
||||
sentinel_hosts,
|
||||
username=sentinel_username,
|
||||
password=sentinel_password,
|
||||
ssl=redis_ssl,
|
||||
socket_timeout=redis_timeout,
|
||||
socket_connect_timeout=redis_timeout,
|
||||
socket_keepalive=True,
|
||||
max_connections=redis_keepalive_pool,
|
||||
)
|
||||
redis_client = sentinel.slave_for(sentinel_master, db=redis_db, username=username, password=password)
|
||||
else:
|
||||
redis_client = Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
db=redis_db,
|
||||
username=username,
|
||||
password=password,
|
||||
socket_timeout=redis_timeout,
|
||||
socket_connect_timeout=redis_timeout,
|
||||
socket_keepalive=True,
|
||||
max_connections=redis_keepalive_pool,
|
||||
ssl=redis_ssl,
|
||||
)
|
||||
|
||||
try:
|
||||
redis_client.ping()
|
||||
except BaseException:
|
||||
redis_client = None
|
||||
flash("Couldn't connect to redis, ban list might be incomplete", "error")
|
||||
|
||||
if request.method == "POST":
|
||||
# Check variables
|
||||
if not request.form:
|
||||
|
|
@ -1683,6 +1723,7 @@ def bans():
|
|||
data = json_loads(request.form["data"])
|
||||
assert isinstance(data, list)
|
||||
except BaseException:
|
||||
app.logger.exception(f"Couldn't load data: {request.form['data']}")
|
||||
flash("Data must be a list of dict", "error")
|
||||
return redirect(url_for("bans"))
|
||||
|
||||
|
|
@ -1691,9 +1732,18 @@ def bans():
|
|||
try:
|
||||
unban = json_loads(unban.replace('"', '"').replace("'", '"'))
|
||||
except BaseException:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
app.logger.exception(f"Couldn't unban {unban['ip']}")
|
||||
continue
|
||||
|
||||
if "ip" not in unban:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
if redis_client:
|
||||
if not redis_client.delete(f"bans_ip_{unban['ip']}"):
|
||||
flash(f"Couldn't unban {unban['ip']} on redis", "error")
|
||||
|
||||
resp = app.config["INSTANCES"].unban(unban["ip"])
|
||||
if resp:
|
||||
flash(f"Couldn't unban {unban['ip']} on the following instances: {', '.join(resp)}", "error")
|
||||
|
|
@ -1701,17 +1751,26 @@ def bans():
|
|||
flash(f"Successfully unbanned {unban['ip']}")
|
||||
elif request.form["operation"] == "ban":
|
||||
for ban in data:
|
||||
try:
|
||||
ban = json_loads(ban.replace('"', '"').replace("'", '"'))
|
||||
except BaseException:
|
||||
if not isinstance(ban, dict) or "ip" not in ban:
|
||||
flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
continue
|
||||
if "ip" not in ban:
|
||||
continue
|
||||
try:
|
||||
ban_end = float(ban.get("ban_end", 86400))
|
||||
except BaseException:
|
||||
continue
|
||||
resp = app.config["INSTANCES"].ban(ban["ip"], ban_end, ban.get("reason", "manual"))
|
||||
|
||||
reason = ban.get("reason", "ui")
|
||||
ban_end = 86400.0
|
||||
if "ban_end" in ban:
|
||||
try:
|
||||
ban_end = float(ban["ban_end"])
|
||||
except ValueError:
|
||||
continue
|
||||
ban_end = (datetime.fromtimestamp(ban_end) - datetime.now()).total_seconds()
|
||||
|
||||
if redis_client:
|
||||
ok = redis_client.set(f"bans_ip_{ban['ip']}", dumps({"reason": reason, "date": time()}))
|
||||
if not ok:
|
||||
flash(f"Couldn't ban {ban['ip']} on redis", "error")
|
||||
redis_client.expire(f"bans_ip_{ban['ip']}", int(ban_end))
|
||||
|
||||
resp = app.config["INSTANCES"].ban(ban["ip"], ban_end, reason)
|
||||
if resp:
|
||||
flash(f"Couldn't ban {ban['ip']} on the following instances: {', '.join(resp)}", "error")
|
||||
else:
|
||||
|
|
@ -1722,12 +1781,27 @@ def bans():
|
|||
|
||||
return redirect(url_for("loading", next=url_for("bans"), message="Update bans"))
|
||||
|
||||
bans = app.config["INSTANCES"].get_bans()[:100]
|
||||
bans = []
|
||||
if redis_client:
|
||||
for key in redis_client.scan_iter("bans_ip_*"):
|
||||
ip = key.decode("utf-8").replace("bans_ip_", "")
|
||||
data = redis_client.get(key)
|
||||
if not data:
|
||||
continue
|
||||
exp = redis_client.ttl(key)
|
||||
bans.append({"ip": ip, "exp": exp} | json_loads(data)) # type: ignore
|
||||
instance_bans = app.config["INSTANCES"].get_bans()
|
||||
|
||||
# Prepare data
|
||||
reasons = {}
|
||||
timestamp_now = time()
|
||||
|
||||
for ban in instance_bans:
|
||||
if not any(b["ip"] == ban["ip"] for b in bans):
|
||||
bans.append(ban)
|
||||
|
||||
bans = bans[:100]
|
||||
|
||||
for ban in bans:
|
||||
exp = ban.pop("exp")
|
||||
# Add remain
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ class Instance:
|
|||
def unban(self, ip: str) -> bool:
|
||||
return self.apiCaller.send_to_apis("POST", "/unban", data={"ip": ip})
|
||||
|
||||
def reports(self) -> Tuple[bool, dict[str, Any]]:
|
||||
return self.apiCaller.send_to_apis("GET", "/metrics/requests", response=True)
|
||||
|
||||
|
||||
class Instances:
|
||||
def __init__(self, docker_client, kubernetes_client, integration: str):
|
||||
|
|
@ -342,3 +345,22 @@ class Instances:
|
|||
return f"Can't unban {ip} on {instance.name}"
|
||||
|
||||
return [instance.name for instance in self.get_instances() if not instance.unban(ip)]
|
||||
|
||||
def get_reports(self, _id: Optional[int] = None) -> List[dict[str, Any]]:
|
||||
if _id:
|
||||
instance = self.__instance_from_id(_id)
|
||||
resp, instance_reports = instance.reports()
|
||||
if not resp:
|
||||
return []
|
||||
return instance_reports[instance.name].get("msg", [])
|
||||
|
||||
reports: List[dict[str, Any]] = []
|
||||
for instance in self.get_instances():
|
||||
resp, instance_reports = instance.reports()
|
||||
if not resp:
|
||||
continue
|
||||
reports.extend(instance_reports[instance.name].get("msg", []))
|
||||
|
||||
reports.sort(key=lambda x: x["date"], reverse=True)
|
||||
|
||||
return reports
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="stylesheet" href="/css/flag-icons.min.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BunkerWeb | Account</title>
|
||||
<script
|
||||
type="module"
|
||||
crossorigin
|
||||
src="/assets/account-ba9110db.js"
|
||||
></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/lang-f5f8ee65.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/State-99778106.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Base-fc8c7eae.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Input-294600dd.js" />
|
||||
<link rel="stylesheet" href="/assets/State-26c5bb69.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="stylesheet" href="/css/flag-icons.min.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BunkerWeb | Actions</title>
|
||||
<script
|
||||
type="module"
|
||||
crossorigin
|
||||
src="/assets/actions-43d7100c.js"
|
||||
></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/lang-f5f8ee65.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/State-99778106.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/List-82cc034b.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Input-294600dd.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Select-55aa1b49.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Item-e1d34516.js" />
|
||||
<link
|
||||
rel="modulepreload"
|
||||
crossorigin
|
||||
href="/assets/Datepicker-349b9bba.js"
|
||||
/>
|
||||
<link rel="stylesheet" href="/assets/State-26c5bb69.css" />
|
||||
<link rel="stylesheet" href="/assets/Datepicker-b3d9355d.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="stylesheet" href="/css/flag-icons.min.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BunkerWeb | Bans</title>
|
||||
<script type="module" crossorigin src="/assets/bans-04a6a92e.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/lang-f5f8ee65.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/State-99778106.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Input-294600dd.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Select-55aa1b49.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/List-82cc034b.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Item-e1d34516.js" />
|
||||
<link
|
||||
rel="modulepreload"
|
||||
crossorigin
|
||||
href="/assets/Datepicker-349b9bba.js"
|
||||
/>
|
||||
<link rel="modulepreload" crossorigin href="/assets/Warning-42bddb5b.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Base-fc8c7eae.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Checkbox-a81c9afa.js" />
|
||||
<link rel="stylesheet" href="/assets/State-26c5bb69.css" />
|
||||
<link rel="stylesheet" href="/assets/Datepicker-b3d9355d.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="stylesheet" href="/css/flag-icons.min.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BunkerWeb | Configs</title>
|
||||
<script
|
||||
type="module"
|
||||
crossorigin
|
||||
src="/assets/configs-86035b54.js"
|
||||
></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/lang-f5f8ee65.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/State-99778106.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/List-82cc034b.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Input-294600dd.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Checkbox-a81c9afa.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Base-27eb0461.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/Base-fc8c7eae.js" />
|
||||
<link rel="modulepreload" crossorigin href="/assets/v4-4a60fe23.js" />
|
||||
<link rel="stylesheet" href="/assets/State-26c5bb69.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -21,7 +21,7 @@ class Filter {
|
|||
setTimeout(() => {
|
||||
const value = document
|
||||
.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text="reason"]`,
|
||||
`[data-${this.prefix}-setting-select-text="reason"]`
|
||||
)
|
||||
.textContent.trim();
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ class Dropdown {
|
|||
const btn = e.target.closest("button");
|
||||
const btnValue = btn.getAttribute("value");
|
||||
const btnSetting = btn.getAttribute(
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`,
|
||||
`data-${this.prefix}-setting-select-dropdown-btn`
|
||||
);
|
||||
//stop if same value to avoid new fetching
|
||||
const isSameVal = this.isSameValue(btnSetting, btnValue);
|
||||
|
|
@ -185,7 +185,7 @@ class Dropdown {
|
|||
|
||||
closeAllDrop() {
|
||||
const drops = document.querySelectorAll(
|
||||
`[data-${this.prefix}-setting-select-dropdown]`,
|
||||
`[data-${this.prefix}-setting-select-dropdown]`
|
||||
);
|
||||
drops.forEach((drop) => {
|
||||
drop.classList.add("hidden");
|
||||
|
|
@ -193,8 +193,8 @@ class Dropdown {
|
|||
document
|
||||
.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${drop.getAttribute(
|
||||
`data-${this.prefix}-setting-select-dropdown`,
|
||||
)}"]`,
|
||||
`data-${this.prefix}-setting-select-dropdown`
|
||||
)}"]`
|
||||
)
|
||||
.classList.remove("rotate-180");
|
||||
});
|
||||
|
|
@ -202,7 +202,7 @@ class Dropdown {
|
|||
|
||||
isSameValue(btnSetting, value) {
|
||||
const selectCustom = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`,
|
||||
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`
|
||||
);
|
||||
const currVal = selectCustom.textContent;
|
||||
return currVal === value ? true : false;
|
||||
|
|
@ -210,30 +210,30 @@ class Dropdown {
|
|||
|
||||
setSelectNewValue(btnSetting, value) {
|
||||
const selectCustom = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select="${btnSetting}"]`,
|
||||
`[data-${this.prefix}-setting-select="${btnSetting}"]`
|
||||
);
|
||||
selectCustom.querySelector(
|
||||
`[data-${this.prefix}-setting-select-text]`,
|
||||
`[data-${this.prefix}-setting-select-text]`
|
||||
).textContent = value;
|
||||
}
|
||||
|
||||
hideDropdown(btnSetting) {
|
||||
//hide dropdown
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`,
|
||||
`[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}"]`,
|
||||
`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}"]`,
|
||||
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
|
||||
);
|
||||
//reset dropdown btns
|
||||
const btnEls = dropdownEl.querySelectorAll("button");
|
||||
|
|
@ -243,7 +243,7 @@ class Dropdown {
|
|||
"bg-primary",
|
||||
"dark:bg-primary",
|
||||
"text-gray-300",
|
||||
"text-gray-300",
|
||||
"text-gray-300"
|
||||
);
|
||||
btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
|
||||
});
|
||||
|
|
@ -251,7 +251,7 @@ class Dropdown {
|
|||
selectedBtn.classList.remove(
|
||||
"bg-white",
|
||||
"dark:bg-slate-700",
|
||||
"text-gray-700",
|
||||
"text-gray-700"
|
||||
);
|
||||
selectedBtn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
|
||||
}
|
||||
|
|
@ -262,10 +262,10 @@ class Dropdown {
|
|||
.getAttribute(`data-${this.prefix}-setting-select`);
|
||||
//toggle dropdown
|
||||
const dropdownEl = document.querySelector(
|
||||
`[data-${this.prefix}-setting-select-dropdown="${attribute}"]`,
|
||||
`[data-${this.prefix}-setting-select-dropdown="${attribute}"]`
|
||||
);
|
||||
const dropdownChevron = document.querySelector(
|
||||
`svg[data-${this.prefix}-setting-select="${attribute}"]`,
|
||||
`svg[data-${this.prefix}-setting-select="${attribute}"]`
|
||||
);
|
||||
dropdownEl.classList.toggle("hidden");
|
||||
dropdownEl.classList.toggle("flex");
|
||||
|
|
@ -318,7 +318,7 @@ class Unban {
|
|||
setTimeout(() => {
|
||||
// Check if at least one item is selected
|
||||
const selected = this.listEl.querySelectorAll(
|
||||
`input[data-checked="true"]`,
|
||||
`input[data-checked="true"]`
|
||||
);
|
||||
|
||||
// Case true, enable unban button
|
||||
|
|
@ -340,7 +340,7 @@ class Unban {
|
|||
if (this.unbanBtn.hasAttribute("disabled")) return;
|
||||
// Get all selected items
|
||||
const selected = this.listEl.querySelectorAll(
|
||||
`input[data-checked="true"]`,
|
||||
`input[data-checked="true"]`
|
||||
);
|
||||
const getDatas = [];
|
||||
selected.forEach((el) => {
|
||||
|
|
@ -366,10 +366,10 @@ class AddBanModal {
|
|||
this.listEl = document.querySelector(`[data-bans-add-ban-list]`);
|
||||
this.submitBtn = document.querySelector(`button[data-bans-modal-submit]`);
|
||||
this.removeAllFieldBtn = document.querySelector(
|
||||
"button[data-add-ban-delete-all-item]",
|
||||
"button[data-add-ban-delete-all-item]"
|
||||
);
|
||||
this.formEl = document.querySelector("form[data-ban-add-form]");
|
||||
this.itemCount = 1;
|
||||
this.itemCount = 0;
|
||||
this.setDatepicker("0"); // for default field
|
||||
this.init();
|
||||
}
|
||||
|
|
@ -437,8 +437,8 @@ class AddBanModal {
|
|||
reason: "ui",
|
||||
});
|
||||
});
|
||||
console.log(data);
|
||||
this.addBanInp.setAttribute("value", data);
|
||||
this.addBanInp.setAttribute("value", JSON.stringify(data));
|
||||
this.addBanInp.value = JSON.stringify(data);
|
||||
this.formEl.submit();
|
||||
});
|
||||
}
|
||||
|
|
@ -502,17 +502,33 @@ class AddBanModal {
|
|||
/>
|
||||
</div>
|
||||
<div class="mx-1.5 col-span-5">
|
||||
<label for="ban-end-${this.itemCount}" class="sr-only">Ban end</label>
|
||||
<input
|
||||
data-bans-add-ban-end
|
||||
type="text"
|
||||
id="ban-end-${this.itemCount}"
|
||||
name="ban-end-${this.itemCount}"
|
||||
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="01/01/2021 00:00:00"
|
||||
pattern="(.*?)"
|
||||
required
|
||||
/>
|
||||
<label for="ban-end-ban-end-${this.itemCount}" class="sr-only">Ban end</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
data-bans-add-ban-end
|
||||
type="text"
|
||||
id="ban-end-${this.itemCount}"
|
||||
name="ban-end-${this.itemCount}"
|
||||
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="01/01/2021 00:00:00"
|
||||
pattern="(.*?)"
|
||||
required
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="pointer-events-none absolute top-1 right-2 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-1.5 col-span-2 flex justify-center items-center">
|
||||
<button
|
||||
|
|
@ -543,26 +559,30 @@ class AddBanModal {
|
|||
}
|
||||
|
||||
setDatepicker(id) {
|
||||
const defaultDate = +(Date.now() + 3600000 * 24)
|
||||
.toString()
|
||||
.substring(0, 10);
|
||||
const inpEl = document.querySelector(`input#ban-end-${id}`);
|
||||
inpEl.setAttribute("data-timestamp", defaultDate);
|
||||
|
||||
// instantiate datepicker
|
||||
const dateOptions = {
|
||||
locale: "en",
|
||||
dateFormat: "m/d/Y H:i:S",
|
||||
defaultDate: false,
|
||||
defaultDate: defaultDate,
|
||||
enableTime: true,
|
||||
enableSeconds: true,
|
||||
time_24hr: true,
|
||||
minuteIncrement: 1,
|
||||
onChange(selectedDates, dateStr, instance) {
|
||||
const inpEl = document.querySelector(`input#ban-end-${id}`);
|
||||
|
||||
// Get date to timestamp
|
||||
const pickStamp = +Date.parse(new Date(dateStr).toString());
|
||||
const nowStamp = +Date.now();
|
||||
|
||||
// Case pick is before current date
|
||||
if (pickStamp < nowStamp) {
|
||||
inpEl.setAttribute("data-timestamp", Date.now());
|
||||
return instance.setDate(nowStamp);
|
||||
inpEl.setAttribute("data-timestamp", defaultDate);
|
||||
return instance.setDate(defaultDate);
|
||||
}
|
||||
|
||||
// Case right value
|
||||
|
|
|
|||
18
src/ui/templates/bans.html
vendored
18
src/ui/templates/bans.html
vendored
|
|
@ -32,9 +32,17 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
</div>
|
||||
<!-- end actions -->
|
||||
|
||||
<div class=" {% if bans|length == 0 %}w-full overflow-hidden grid grid-cols-12 max-h-100 sm:max-h-125 col-span-12 p-4 relative break-words{%else%}hidden{% endif%} "
|
||||
> <div class="col-span-12 flex flex-col justify-center items-center h-fit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mb-2 w-8 h-8 stroke-white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607ZM10.5 7.5v6m3-3h-6" />
|
||||
</svg>
|
||||
<h5 class="font-bold dark:text-white/90 mx-2 text-white">No bans found</h5>
|
||||
</div></div>
|
||||
|
||||
<!-- info-->
|
||||
<div
|
||||
class="h-fit 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"
|
||||
class="{% if bans|length == 0 %}hidden{% endif%} h-fit 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"
|
||||
>
|
||||
<h5 class="mb-2 font-bold dark:text-white/90">INFO</h5>
|
||||
<div class="mx-1 flex items-center my-4">
|
||||
|
|
@ -49,7 +57,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
{{bans|length}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-1 flex items-center my-4">
|
||||
<div class="{% if bans|length == 0 %}hidden{% endif%} 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"
|
||||
>
|
||||
|
|
@ -67,7 +75,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
<!-- filter -->
|
||||
<div
|
||||
data-{{current_endpoint}}-filter
|
||||
class="h-fit col-span-12 md:col-span-8 2xl:col-span-6 3xl:col-span-5 p-4 relative flex flex-col min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
class="{% if bans|length == 0 %}hidden{% endif%} h-fit col-span-12 md:col-span-8 2xl:col-span-6 3xl:col-span-5 p-4 relative flex flex-col min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
>
|
||||
<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">
|
||||
|
|
@ -218,7 +226,9 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
</div>
|
||||
<!-- end filter -->
|
||||
|
||||
<div class="w-full overflow-hidden grid grid-cols-12 max-h-100 sm:max-h-125 col-span-12 p-4 relative break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
|
||||
|
||||
<div class=" {% if bans|length == 0 %}hidden{% endif%} w-full overflow-hidden grid grid-cols-12 max-h-100 sm:max-h-125 col-span-12 p-4 relative break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
>
|
||||
<div class="col-span-12">
|
||||
<h5 class="mb-4 mt-2 font-bold dark:text-white/90 mx-2">BANS LIST</h5>
|
||||
|
|
|
|||
21
src/ui/templates/bans_modal.html
vendored
21
src/ui/templates/bans_modal.html
vendored
|
|
@ -1,11 +1,11 @@
|
|||
<!-- modal -->
|
||||
<div
|
||||
data-bans-modal
|
||||
class="dark:brightness-110 hidden w-screen h-screen fixed bg-gray-600/50 z-[1001] top-0 left-0 justify-center items-center"
|
||||
class="dark:brightness-110 hidden w-screen h-screen fixed bg-gray-600/50 z-[1001] top-0 left-0 justify-center items-start"
|
||||
>
|
||||
<div
|
||||
data-bans-modal-card
|
||||
class="overflow-y-auto mx-0 sm:mx-6 lg:mx-8 my-3 px-4 pt-4 pb-8 w-full sm:min-w-[500px] max-w-[650px] flex flex-col justify-between break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
class="overflow-y-auto mx-0 sm:mx-6 lg:mx-8 my-3 px-4 pt-4 pb-8 w-full sm:min-w-[500px] max-w-[650px] mt-[15vh] max-h-[70vh] flex flex-col justify-between break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
>
|
||||
<div>
|
||||
<div class="w-full flex justify-between mb-2">
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-span-12 overflow-y-auto overflow-x-auto">
|
||||
<div class="col-span-12 overflow-y-auto overflow-x-auto max-h-[250px]">
|
||||
<div data-{{current_endpoint}}-bans-list>
|
||||
<!-- list container-->
|
||||
<div
|
||||
|
|
@ -135,8 +135,19 @@
|
|||
pattern="(.*?)"
|
||||
required
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pointer-events-none absolute top-1 right-2 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="pointer-events-none absolute top-1 right-2 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
5
src/ui/templates/loading.html
vendored
5
src/ui/templates/loading.html
vendored
File diff suppressed because one or more lines are too long
8
src/ui/templates/plugins_modal.html
vendored
8
src/ui/templates/plugins_modal.html
vendored
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div
|
||||
data-plugins-modal-card
|
||||
class="overflow-y-auto mx-0 sm:mx-6 lg:mx-8 my-3 px-4 pt-4 pb-8 w-full sm:min-w-[500px] h-[90vh] flex flex-col break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
class="overflow-y-auto mx-0 sm:mx-6 lg:mx-8 my-3 px-4 pt-4 pb-8 w-[50vw] max-w-[700px] sm:min-w-[500px] max-h-[90vh] flex flex-col break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
>
|
||||
<div class="w-full flex justify-between mb-2">
|
||||
<p
|
||||
|
|
@ -50,15 +50,15 @@
|
|||
</div>
|
||||
<!-- action button -->
|
||||
<div class="w-full justify-center flex mt-10">
|
||||
<button
|
||||
<button
|
||||
data-plugins-modal-close
|
||||
class="dark:brightness-90 mr-3 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-red-500 hover:bg-red-500/80 focus:bg-red-500/80 leading-normal text-md ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
|
||||
class="close-btn mb-4 mr-3 text-base"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-sky-500 hover:bg-sky-500/80 focus:bg-sky-500/80 leading-normal text-md ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
|
||||
class="delete-btn mb-4 mr-3 text-base"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
|
|
|||
14
src/ui/templates/reports.html
vendored
14
src/ui/templates/reports.html
vendored
|
|
@ -22,9 +22,18 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class=" {% if reports|length == 0 %}w-full overflow-hidden grid grid-cols-12 max-h-100 sm:max-h-125 col-span-12 p-4 relative break-words{%else%}hidden{% endif%} "
|
||||
> <div class="col-span-12 flex flex-col justify-center items-center h-fit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mb-2 w-8 h-8 stroke-white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607ZM10.5 7.5v6m3-3h-6" />
|
||||
</svg>
|
||||
<h5 class="font-bold dark:text-white/90 mx-2 text-white">No reports found</h5>
|
||||
</div></div>
|
||||
|
||||
<!-- info-->
|
||||
{% if reports|length != 0 %}
|
||||
<div
|
||||
class="h-fit 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"
|
||||
class=" h-fit 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"
|
||||
>
|
||||
<h5 class="mb-2 font-bold dark:text-white/90">INFO</h5>
|
||||
<div class="mx-1 flex items-center my-4">
|
||||
|
|
@ -36,7 +45,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
<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"
|
||||
>
|
||||
{{reports|length}}
|
||||
{{ total_reports }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-1 flex items-center my-4">
|
||||
|
|
@ -479,5 +488,6 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
</div>
|
||||
<!-- end list container--></div>
|
||||
</div>
|
||||
{%endif%}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
326
tests/ui/main.py
326
tests/ui/main.py
|
|
@ -1186,131 +1186,127 @@ location /hello {
|
|||
|
||||
sleep(3)
|
||||
|
||||
print("The cache file content is correct, trying logs page ...", flush=True)
|
||||
print("The cache file content is correct, trying reporting page ...", flush=True)
|
||||
|
||||
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[8]/a", "logs")
|
||||
get(f"http://www.example.com{ui_url}/home?id=/etc/passwd")
|
||||
|
||||
### LOGS PAGE
|
||||
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[8]/a", "reports")
|
||||
|
||||
print("Selecting correct instance ...", flush=True)
|
||||
### REPORTS PAGE
|
||||
|
||||
assert_button_click(driver, "//button[@data-logs-setting-select='instances']")
|
||||
print("Trying to filter the reports ...", flush=True)
|
||||
|
||||
instances = safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//div[@data-logs-setting-select-dropdown='instances']/button",
|
||||
multiple=True,
|
||||
)
|
||||
reports_list = safe_get_element(driver, By.XPATH, "//ul[@data-reports-list='']/li", multiple=True)
|
||||
|
||||
first_instance = instances[0].text
|
||||
|
||||
if len(instances) == 0:
|
||||
print("No instances found, exiting ...", flush=True)
|
||||
if not reports_list:
|
||||
print("No reports found, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
assert_button_click(driver, instances[0])
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "submit-data"))
|
||||
|
||||
sleep(3)
|
||||
|
||||
logs_list = safe_get_element(driver, By.XPATH, "//ul[@data-logs-list='']/li", multiple=True)
|
||||
|
||||
if len(logs_list) == 0:
|
||||
print("No logs found, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("Logs found, trying auto refresh ...", flush=True)
|
||||
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "live-update"))
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "submit-live"))
|
||||
|
||||
sleep(3)
|
||||
|
||||
if len(logs_list) == len(
|
||||
safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//ul[@data-logs-list='']/li[not(contains(@class, 'hidden'))]",
|
||||
multiple=True,
|
||||
)
|
||||
):
|
||||
print("Auto refresh is not working, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("Auto refresh is working, deactivating it ...", flush=True)
|
||||
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "live-update"))
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "submit-data"))
|
||||
|
||||
sleep(3)
|
||||
|
||||
logs_list = safe_get_element(driver, By.XPATH, "//ul[@data-logs-list='']/li", multiple=True)
|
||||
|
||||
print("Trying filters ...", flush=True)
|
||||
|
||||
filter_input = safe_get_element(driver, By.ID, "keyword")
|
||||
filter_input.send_keys("abcde")
|
||||
|
||||
filter_input.send_keys("gen")
|
||||
with suppress(TimeoutException):
|
||||
WebDriverWait(driver, 2).until(
|
||||
EC.presence_of_element_located(
|
||||
(
|
||||
By.XPATH,
|
||||
"//ul[@data-reports-list='']/li[not(contains(@class, 'hidden'))]",
|
||||
)
|
||||
)
|
||||
)
|
||||
print("The keyword filter is not working, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("The reports have been filtered, trying bans page ...", flush=True)
|
||||
|
||||
### BANS PAGE
|
||||
|
||||
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[9]/a", "bans")
|
||||
|
||||
try:
|
||||
safe_get_element(driver, By.XPATH, "/html/body/main/div/div[2]/div/h5", error=True)
|
||||
except TimeoutException:
|
||||
print("Bans present even though they shouldn't be, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("No bans found, as expected, trying to add a ban ...", flush=True)
|
||||
|
||||
assert_button_click(driver, "//button[@data-add-ban='']")
|
||||
|
||||
try:
|
||||
WebDriverWait(driver, 2).until(
|
||||
EC.presence_of_element_located(
|
||||
(
|
||||
By.XPATH,
|
||||
"//ul[@data-bans-add-ban-list='']/li",
|
||||
)
|
||||
)
|
||||
)
|
||||
except TimeoutException:
|
||||
print("No bans found, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
assert_button_click(driver, "//button[@data-add-ban-delete-all-item='']")
|
||||
|
||||
with suppress(TimeoutException):
|
||||
WebDriverWait(driver, 2).until(
|
||||
EC.presence_of_element_located(
|
||||
(
|
||||
By.XPATH,
|
||||
"//ul[@data-bans-add-ban-list='']/li",
|
||||
)
|
||||
)
|
||||
)
|
||||
print("Bans present even though they shouldn't be, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("No bans found, as expected, trying to add multiple bans ...", flush=True)
|
||||
|
||||
add_entry_button = safe_get_element(driver, By.XPATH, "//button[@data-ban-add-new='']")
|
||||
|
||||
assert_button_click(driver, add_entry_button)
|
||||
|
||||
ip_input = safe_get_element(driver, By.ID, "ip-1")
|
||||
ip_input.send_keys("127.0.0.1")
|
||||
|
||||
sleep(3)
|
||||
|
||||
if len(logs_list) == len(
|
||||
safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//ul[@data-logs-list='']/li[not(contains(@class, 'hidden'))]",
|
||||
multiple=True,
|
||||
)
|
||||
):
|
||||
print("The keyword filter is not working, exiting ...", flush=True)
|
||||
assert_button_click(driver, add_entry_button)
|
||||
|
||||
ip_input = safe_get_element(driver, By.ID, "ip-2")
|
||||
ip_input.send_keys("8.8.8.8")
|
||||
|
||||
access_page(driver, driver_wait, "//button[@data-bans-modal-submit='']", "bans")
|
||||
|
||||
try:
|
||||
entries = safe_get_element(driver, By.XPATH, "//ul[@data-bans-list='']/li", multiple=True, error=True)
|
||||
except TimeoutException:
|
||||
print("No ban found, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
filter_input.clear()
|
||||
|
||||
print("Keyword filter is working, trying type filter ...", flush=True)
|
||||
|
||||
assert_button_click(driver, "//button[@data-logs-setting-select='types']")
|
||||
|
||||
assert_button_click(
|
||||
driver,
|
||||
"//div[@data-logs-setting-select-dropdown='types']/button[@value='warn']",
|
||||
)
|
||||
|
||||
if len(logs_list) == len(
|
||||
safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//ul[@data-logs-list='']/li[not(contains(@class, 'hidden'))]",
|
||||
multiple=True,
|
||||
)
|
||||
):
|
||||
print("The keyword filter is not working, exiting ...", flush=True)
|
||||
if len(entries) != 2:
|
||||
print("The bans are present but there should be 2, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
assert_button_click(driver, "//button[@data-logs-setting-select='types']")
|
||||
print("Bans found, trying to delete them ...", flush=True)
|
||||
|
||||
assert_button_click(
|
||||
driver,
|
||||
"//div[@data-logs-setting-select-dropdown='types']/button[@value='all']",
|
||||
)
|
||||
assert_button_click(driver, "//input[@id='ban-item-2']")
|
||||
|
||||
print("Type filter is working, trying to filter by date ...", flush=True)
|
||||
access_page(driver, driver_wait, "//button[@data-unban-btn='']", "bans")
|
||||
|
||||
current_date = datetime.now()
|
||||
resp = get(
|
||||
f"http://www.example.com{ui_url}/logs/{first_instance}?from_date={int(current_date.timestamp() - 86400000)}&to_date={int((current_date - timedelta(days=1)).timestamp())}",
|
||||
headers={"Host": "www.example.com", "User-Agent": driver.execute_script("return navigator.userAgent;")},
|
||||
cookies={"session": driver.get_cookies()[0]["value"]},
|
||||
)
|
||||
|
||||
if len(resp.json()["logs"]) != 0:
|
||||
print("The date filter is not working, exiting ...", flush=True)
|
||||
try:
|
||||
entries = safe_get_element(driver, By.XPATH, "//ul[@data-bans-list='']/li", multiple=True, error=True)
|
||||
except TimeoutException:
|
||||
print("No bans found, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("Date filter is working, trying jobs page ...", flush=True)
|
||||
if len(entries) != 1:
|
||||
print("The bans are present but there should be 1, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[9]/a", "jobs")
|
||||
print("Ban deleted successfully, trying jobs page ...", flush=True)
|
||||
|
||||
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[10]/a", "jobs")
|
||||
|
||||
### JOBS PAGE
|
||||
|
||||
|
|
@ -1441,7 +1437,129 @@ location /hello {
|
|||
print("The cache download is not working, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("Cache download is working, trying account page ...", flush=True)
|
||||
print("Jobs cache download is working, trying logs page ...", flush=True)
|
||||
|
||||
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[11]/a", "logs")
|
||||
|
||||
### LOGS PAGE
|
||||
|
||||
print("Selecting correct instance ...", flush=True)
|
||||
|
||||
assert_button_click(driver, "//button[@data-logs-setting-select='instances']")
|
||||
|
||||
instances = safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//div[@data-logs-setting-select-dropdown='instances']/button",
|
||||
multiple=True,
|
||||
)
|
||||
|
||||
first_instance = instances[0].text
|
||||
|
||||
if len(instances) == 0:
|
||||
print("No instances found, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
assert_button_click(driver, instances[0])
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "submit-data"))
|
||||
|
||||
sleep(3)
|
||||
|
||||
logs_list = safe_get_element(driver, By.XPATH, "//ul[@data-logs-list='']/li", multiple=True)
|
||||
|
||||
if len(logs_list) == 0:
|
||||
print("No logs found, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("Logs found, trying auto refresh ...", flush=True)
|
||||
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "live-update"))
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "submit-live"))
|
||||
|
||||
sleep(3)
|
||||
|
||||
if len(logs_list) == len(
|
||||
safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//ul[@data-logs-list='']/li[not(contains(@class, 'hidden'))]",
|
||||
multiple=True,
|
||||
)
|
||||
):
|
||||
print("Auto refresh is not working, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("Auto refresh is working, deactivating it ...", flush=True)
|
||||
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "live-update"))
|
||||
assert_button_click(driver, safe_get_element(driver, By.ID, "submit-data"))
|
||||
|
||||
sleep(3)
|
||||
|
||||
logs_list = safe_get_element(driver, By.XPATH, "//ul[@data-logs-list='']/li", multiple=True)
|
||||
|
||||
print("Trying filters ...", flush=True)
|
||||
|
||||
filter_input = safe_get_element(driver, By.ID, "keyword")
|
||||
|
||||
filter_input.send_keys("gen")
|
||||
|
||||
sleep(3)
|
||||
|
||||
if len(logs_list) == len(
|
||||
safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//ul[@data-logs-list='']/li[not(contains(@class, 'hidden'))]",
|
||||
multiple=True,
|
||||
)
|
||||
):
|
||||
print("The keyword filter is not working, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
filter_input.clear()
|
||||
|
||||
print("Keyword filter is working, trying type filter ...", flush=True)
|
||||
|
||||
assert_button_click(driver, "//button[@data-logs-setting-select='types']")
|
||||
|
||||
assert_button_click(
|
||||
driver,
|
||||
"//div[@data-logs-setting-select-dropdown='types']/button[@value='warn']",
|
||||
)
|
||||
|
||||
if len(logs_list) == len(
|
||||
safe_get_element(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//ul[@data-logs-list='']/li[not(contains(@class, 'hidden'))]",
|
||||
multiple=True,
|
||||
)
|
||||
):
|
||||
print("The keyword filter is not working, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
assert_button_click(driver, "//button[@data-logs-setting-select='types']")
|
||||
|
||||
assert_button_click(
|
||||
driver,
|
||||
"//div[@data-logs-setting-select-dropdown='types']/button[@value='all']",
|
||||
)
|
||||
|
||||
print("Type filter is working, trying to filter by date ...", flush=True)
|
||||
|
||||
current_date = datetime.now()
|
||||
resp = get(
|
||||
f"http://www.example.com{ui_url}/logs/{first_instance}?from_date={int((current_date - timedelta(weeks=1)).timestamp())}&to_date={int((current_date - timedelta(days=1)).timestamp())}",
|
||||
headers={"Host": "www.example.com", "User-Agent": driver.execute_script("return navigator.userAgent;")},
|
||||
cookies={"session": driver.get_cookies()[0]["value"]},
|
||||
)
|
||||
|
||||
if len(resp.json()["logs"]) != 0:
|
||||
print("The date filter is not working, exiting ...", flush=True)
|
||||
exit(1)
|
||||
|
||||
print("Date filter is working, trying jobs page ...", flush=True)
|
||||
|
||||
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/a", "account")
|
||||
|
||||
|
|
@ -1700,6 +1818,8 @@ location /hello {
|
|||
)
|
||||
|
||||
print("Successfully logged in without 2FA, tests are done, exiting ...", flush=True)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SystemExit:
|
||||
exit(1)
|
||||
except:
|
||||
|
|
|
|||
Loading…
Reference in a new issue