Run pre-commit-config and apply it + update it

This commit is contained in:
Théophile Diot 2024-10-07 15:02:49 +02:00
parent c73e9bf161
commit b2f9fab7ad
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
36 changed files with 147 additions and 2516 deletions

View file

@ -2,249 +2,6 @@ name: Automatic tests (1.5)
permissions: read-all
on:
push:
branches: [1.5]
jobs:
# Containers
build-containers:
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
image: [bunkerweb, scheduler, autoconf, ui]
include:
- image: bunkerweb
dockerfile: src/bw/Dockerfile
- image: scheduler
dockerfile: src/scheduler/Dockerfile
- image: autoconf
dockerfile: src/autoconf/Dockerfile
- image: ui
dockerfile: src/ui/Dockerfile
uses: ./.github/workflows/container-build.yml
with:
RELEASE: 1.5
ARCH: linux/amd64
CACHE: true
IMAGE: ${{ matrix.image }}
DOCKERFILE: ${{ matrix.dockerfile }}
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
# Build Linux packages
build-packages:
permissions:
contents: read
packages: write
strategy:
matrix:
linux: [ubuntu, debian, fedora, rhel, rhel9, ubuntu-jammy]
include:
- linux: ubuntu
package: deb
- linux: ubuntu-jammy
package: deb
- linux: debian
package: deb
- linux: fedora
package: rpm
- linux: rhel
package: rpm
- linux: rhel9
package: rpm
uses: ./.github/workflows/linux-build.yml
with:
RELEASE: 1.5
LINUX: ${{ matrix.linux }}
PACKAGE: ${{ matrix.package }}
TEST: true
PLATFORMS: linux/amd64
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
codeql:
uses: ./.github/workflows/codeql.yml
permissions:
actions: read
contents: read
security-events: write
# UI tests
prepare-tests-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- id: set-matrix
run: |
tests=$(find ./tests/ui/ -name "*_page.py" -type f -printf "%f\n" | jq -c --raw-input --slurp 'split("\n")| .[0:-1]')
echo "tests=$tests" >> $GITHUB_OUTPUT
outputs:
tests: ${{ steps.set-matrix.outputs.tests }}
tests-ui:
needs: [prepare-tests-ui, build-containers]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-ui.outputs.tests) }}
uses: ./.github/workflows/tests-ui.yml
with:
TEST: ${{ matrix.test }}
RELEASE: 1.5
tests-ui-linux:
needs: [prepare-tests-ui, build-packages]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-ui.outputs.tests) }}
uses: ./.github/workflows/tests-ui-linux.yml
with:
TEST: ${{ matrix.test }}
RELEASE: 1.5
# Core tests
prepare-tests-core:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- id: set-matrix
run: |
tests=$(find ./tests/core/ -maxdepth 1 -mindepth 1 -type d -printf "%f\n" | jq -c --raw-input --slurp 'split("\n")| .[0:-1]')
echo "tests=$tests" >> $GITHUB_OUTPUT
outputs:
tests: ${{ steps.set-matrix.outputs.tests }}
tests-core:
needs: [build-containers, prepare-tests-core]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
uses: ./.github/workflows/test-core.yml
with:
TEST: ${{ matrix.test }}
RELEASE: 1.5
tests-core-linux:
needs: [build-packages, prepare-tests-core]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
uses: ./.github/workflows/test-core-linux.yml
with:
TEST: ${{ matrix.test }}
RELEASE: 1.5
secrets: inherit
# Push with 1.5 tag
push-1_5:
needs: [tests-ui, tests-core]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to ghcr
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push BW image
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
env:
FROM: "bunkerweb"
TO: "bunkerweb"
- name: Push scheduler image
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
env:
FROM: "scheduler"
TO: "bunkerweb-scheduler"
- name: Push UI image
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
env:
FROM: "ui"
TO: "bunkerweb-ui"
- name: Push autoconf image
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
env:
FROM: "autoconf"
TO: "bunkerweb-autoconf"
# Push Linux packages
push-packages:
needs: [tests-ui-linux, tests-core-linux]
strategy:
matrix:
linux: [ubuntu, debian, fedora, el, el9, ubuntu-jammy]
arch: [amd64]
include:
- release: 1.5
repo: bunkerweb
- linux: ubuntu
package_arch: amd64
separator: _
suffix: ""
version: noble
package: deb
- linux: debian
package_arch: amd64
separator: _
suffix: ""
version: bookworm
package: deb
- linux: fedora
package_arch: x86_64
separator: "-"
suffix: "1."
version: 40
package: rpm
- linux: el
package_arch: x86_64
separator: "-"
suffix: "1."
version: 8
package: rpm
- linux: el9
package_arch: x86_64
separator: "-"
suffix: "1."
version: 9
package: rpm
- linux: ubuntu-jammy
package_arch: amd64
separator: _
suffix: ""
version: jammy
package: deb
uses: ./.github/workflows/push-packagecloud.yml
with:
SEPARATOR: ${{ matrix.separator }}
SUFFIX: ${{ matrix.suffix }}
REPO: ${{ matrix.repo }}
LINUX: ${{ matrix.linux }}
VERSION: ${{ matrix.version }}
PACKAGE: ${{ matrix.package }}
BW_VERSION: ${{ matrix.release }}
PACKAGE_ARCH: ${{ matrix.package_arch }}
ARCH: ${{ matrix.arch }}
secrets:
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
name: Automatic tests (1.5)
permissions: read-all
on:
push:
branches: [1.5]

View file

@ -1,9 +1,9 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
exclude: (^src/ui/client|^LICENSE.md$|^src/VERSION$|^env/|^src/(bw/misc/root-ca.pem$|deps/src/|common/core/modsecurity/files|ui/static/(js/(editor/|utils/purify/|tsparticles\.bundle\.min\.js)|css/dashboard\.css))|\.(svg|drawio|patch\d?|ascii|tf|tftpl|key)$)
exclude: (^src/ui/client|^LICENSE.md$|^src/VERSION$|^env/|^examples/community/|^src/(bw/misc/root-ca.pem$|deps/src/|common/core/modsecurity/files|ui/static/(js/(editor/|utils/purify/|tsparticles\.bundle\.min\.js)|css/dashboard\.css))|\.(svg|drawio|patch\d?|ascii|tf|tftpl|key)$)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0
hooks:
- id: requirements-txt-fixer
name: Fix requirements.txt and requirements.in files
@ -17,7 +17,7 @@ repos:
- id: check-case-conflict
- repo: https://github.com/psf/black
rev: 3702ba224ecffbcec30af640c149f231d90aebdb # frozen: 24.4.2
rev: b965c2a5026f8ba399283ba3e01898b012853c79 # frozen: 24.8.0
hooks:
- id: black
name: Black Python Formatter
@ -43,7 +43,7 @@ repos:
args: ["--std", "min", "--codes", "--ranges", "--no-cache"]
- repo: https://github.com/pycqa/flake8
rev: 7d37d9032d0d161634be4554273c30efd4dea0b3 # frozen: 7.0.0
rev: e43806be3607110919eff72939fda031776e885a # frozen: 7.1.1
hooks:
- id: flake8
name: Flake8 Python Linter
@ -62,12 +62,12 @@ repos:
- id: codespell
name: Codespell Spell Checker
exclude: (^src/(ui/templates|common/core/.+/files|bw/loading)/.+.html|modsecurity-rules.conf.*|src/ui/app/static/(fonts|libs)/.+)$
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py,src/ui/app/static/json/countries.geojson,src/ui/app/static/js/pages/reports.js
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py,src/ui/app/static/json/countries.geojson,src/ui/app/static/js/pages/reports.js,src/ui/app/static/json/periscop.min.json,src/ui/app/static/json/blockhaus.min.json
language: python
types: [text]
- repo: https://github.com/gitleaks/gitleaks
rev: 145400593c178304246371bc45290588bc72f43e # frozen: v8.18.2
rev: ce2702a4889da86abb07e0fb0482e9a6e12a9f24 # frozen: v8.20.0
hooks:
- id: gitleaks

File diff suppressed because it is too large Load diff

View file

@ -1,97 +0,0 @@
const fs = require("fs");
const path = require("path");
// Merge all components md on this file name
const finalFile = "ui-components.md";
// Format merge file
function formatMd() {
// Create a md file to merge
const merge = path.join(finalFile);
// Get data from merge
let data = fs.readFileSync(merge, "utf8");
let isLevel = true;
let currAttemps = 0;
const maxAttemps = 6;
while (isLevel && currAttemps < maxAttemps) {
currAttemps++;
const titles = [];
let tag = "#";
for (let i = 0; i < currAttemps; i++) {
tag += "#";
}
tag += " ";
// Each time, get the first level title and add it to the titles array
data.split("\n").forEach((line) => {
if (line.startsWith(tag) && line.includes("/")) {
const firstLevel = line.split("/")[0];
if (!titles.includes(firstLevel.replace(tag, "").trim()))
titles.push(firstLevel.replace(tag, ""));
}
});
// Create a top title at the first occurrence
// And remove from component the first level string
titles.forEach((title) => {
let isTitleSet = false;
data.split("\n").forEach((line) => {
// For title
if (line.startsWith(tag) && line.includes("/")) {
// Add a top title before the current line
if (!isTitleSet && line.includes(`${title}/`)) {
data = data.replace(
line,
`${tag} ${title}\n\n${line
.replace(tag, "#" + tag)
.replace(`${title}/`, "")}`,
);
isTitleSet = true;
return;
}
if (line.includes(`${title}/`)) {
data = data.replace(
line,
line.replace(tag, "#" + tag).replace(`${title}/`, ""),
);
}
return;
}
});
});
}
// Update the child of .vue component title to keep title levels consistency
let componentTag = "";
let dataSplit = data.split("\n");
data.split("\n").forEach((line, id) => {
if (line.startsWith("#") && line.includes(".vue")) {
componentTag = line.split(" ")[0];
return;
}
if (
(line.startsWith("#") && line.includes("Parameters")) ||
(line.startsWith("#") && line.includes("Examples"))
) {
const elTag = line.split(" ")[0];
// get line per id
const updateLine = line.replace(elTag, `${componentTag}#`);
dataSplit[id] = updateLine;
}
});
// Update the data with split
data = dataSplit.join("\n");
// Add title and description
const title = "# UI Components";
const description =
"This page contains all the UI components used in the application.";
data = `${title}\n\n${description}\n\n${data}`;
fs.writeFileSync(merge, data, "utf8");
}
formatMd();

View file

@ -1,209 +0,0 @@
from os.path import abspath
from pathlib import Path
from subprocess import Popen, PIPE
from typing import List
from shutil import rmtree
from re import search
from traceback import format_exc
outputFilename = "ui-components.md"
# We want to get path of the folder where our components are
# The path is "../src/client/dashboard/src/components" from here
inputFolder = abspath("../src/ui/client/dashboard/components")
outputFolder = abspath("../docs/components")
outputFile = abspath("../docs")
components_path_to_exclude = ("components/Icons", "components/Forms/Error", "components/Dashboard", "components/Builder")
def run_command(command: List[str]) -> int:
"""Utils to run a subprocess command. This is useful to run npm commands to build vite project"""
print(f"Running command: {command}", flush=True)
try:
process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, text=True)
while process.poll() is None:
if process.stdout is not None:
for line in process.stdout:
print(line.strip(), flush=True)
if process.returncode != 0:
print("Error while running command", flush=True)
print(process.stdout.read(), flush=True)
print(process.stderr.read(), flush=True)
return 1
except BaseException as e:
print(f"Error while running command: {e}", flush=True)
return 1
print("Command executed successfully", flush=True)
return 0
def install_npm_packages():
"""Install all packages needed to run the script"""
# Install documentation package
run_command("npm install -g documentation")
def reset():
"""Reset the docs folder"""
# delete the output folder even if not empty
rmtree(outputFolder, ignore_errors=True)
# remove outputfilename
output_file_path = Path(outputFile) / outputFilename
output_file_path.unlink(missing_ok=True)
def vue2js():
"""Get the script part of a Vue file and create a JS file"""
# Create outputFolder if not exists
Path(outputFolder).mkdir(parents=True, exist_ok=True)
# Get every subfolders from the input folder
for folder in Path(inputFolder).rglob("*"):
# Get only vue file
if not folder.is_file() or folder.suffix != ".vue":
continue
# Exclude some files
if any(folder_path in folder.as_posix() for folder_path in components_path_to_exclude):
continue
# Read the file content
data = folder.read_text()
# Get only the content between <script setup> and </script> tag
script = data.split("<script setup>")[1].split("</script>")[0]
# Get index of jsdoc comments
first_doc_index_start = script.find("/**")
first_doc_index_end = script.find("*/")
if first_doc_index_start != -1 and first_doc_index_end != -1:
# get content before first_doc_index_end
script = script[first_doc_index_start : first_doc_index_end + 2]
# Create a file on the output folder with the same name but with .js extension
fileName = folder.name.replace(".vue", ".js")
dest = Path(outputFolder) / fileName
dest.write_text(script)
def js2md():
"""Run a command to render markdown from JS files"""
# Get all files from the output folder
files = list(Path(outputFolder).rglob("*"))
process_list = []
# Create a markdown file for each JS file
for file in files:
# Run a process `documentation build <filename> -f md > <filename>.md
command = f"documentation build {file} -f md > {file.with_suffix('.md')}"
# Run the command
# I want to run this command async
process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, text=True)
process_list.append(process)
# Wait that all processes are done
for process in process_list:
process.wait()
# Remove js files after
for file in files:
file.unlink()
def formatMd():
"""Format each markdown file to remove useless data and format some data like params"""
# Get all files from the output folder
files = list(Path(outputFolder).rglob("*"))
for file in files:
try:
# Get the title from first line
data = file.read_text()
# Remove everything after a [1]: tag
data = data.split("[1]:")[0]
# Remove ### Table of contents
data = data.replace("### Table of Contents", "")
# Remove everything before the first ## tag
if "## " in data:
index = data.index("## ")
data = data[index:]
# I want to loop on each line
lines = data.split("\n")
line_result = []
for line in lines:
# remove space (so &#x20 or &#32)
line = line.replace("&#x20", "").replace("&#32", "")
if line.startswith("#") and ".vue" in line and "\\.vue" in line:
line = line.replace("\\.vue", ".vue")
# Case not a param, keep the line as is
if not line.startswith("*"):
line_result.append(line)
continue
# get line without first char
line = "-" + line[1:]
# remove each **[string][num]** pattern in a param by **string**
reg = r"\[\w+\]\[\d+\]"
while search(reg, line):
# get data of the pattern
pattern = search(reg, line).group()
# get content of first bracket
content = pattern.split("][")[0].replace("[", "")
line = line.replace(pattern, f"{content}")
line_result.append(line)
# I can merge the lines
data = "\n".join(line_result)
# update the file with the new content
file.write_text(data)
except BaseException:
print(format_exc(), flush=True)
print("Error while parsing file", str(file.name), flush=True)
exit(1)
def mergeMd():
"""Merge all markdown files into one"""
# Get all files from the output folder
files = list(Path(outputFolder).rglob("*"))
# Create order using the tag title path of each file
order = []
for file in files:
# Get the title from first line
data = file.read_text()
filePath = data.split("\n")[0].replace("## ", "")
order.append({"path": filePath, "fileName": str(file.name)})
# Sort by path
order.sort(key=lambda x: x["path"])
# Create the md file to merge
merge = Path(outputFile) / outputFilename
merge.write_text("")
# Append each file in order and keep indentation
for info in order:
file_path = Path(outputFolder) / info["fileName"]
data = file_path.read_text()
merge.write_text(merge.read_text() + data)
# Remove all files
rmtree(outputFolder, ignore_errors=True)
def formatMergeMd():
"""ATM didn't convert the js function to python.
So I will run a command to format the file"""
command = "node ./vue2md.js"
run_command(command)
install_npm_packages()
reset()
vue2js()
js2md()
formatMd()
mergeMd()
formatMergeMd()

View file

@ -2229,311 +2229,3 @@ After a successful login/password combination, you will be prompted to enter you
```shell
systemctl reload bunkerweb
```
## UI development
The web UI is moving from Flask to Vue.js using a builder approach. I will detail some steps and parts of the UI development process.
### Create a dashboard page
A dashboard page is a page that will have at build time a separate html and js file. Furthermore, the main.py file will need to be updated to include the new page.
**1. Create page folder**
For the example, we will create a page named `example`.
First, you need to go to the `src/ui/client/dashboard/pages` folder and copy an existing page folder like `home` folder and rename it to `example`.
Don't forget to rename the `Home.vue` file to `Example.vue`, and `home.js` to `example.js`.
**2. Update page folder files**
Open `example.js` file, you get something like this :
```js
import { createApp } from "vue"; // Utils
import { createPinia } from "pinia"; // Global Vue.js store
import { getI18n } from "@utils/lang.js"; // Get i18n
import Home from "./Home.vue"; // Vue page app
const pinia = createPinia();
// We set the page as app, add pinia plugin, and we add the i18n module with wanted prefix key and mount the app
// The i18n keys are in the src/ui/client/dashboard/lang folder
createApp(Home)
.use(pinia)
.use(getI18n(["dashboard", "action", "inp", "icons", "home"]))
.mount("#app");
```
You need to update it like this :
```js
import { createApp } from "vue";
import { createPinia } from "pinia";
import { getI18n } from "@utils/lang.js";
import Example from "./Example.vue";
const pinia = createPinia();
createApp(Example)
.use(pinia)
.use(getI18n(["dashboard", "action", "inp", "icons", "example"])) // get example prefix keys from lang folder
.mount("#app");
```
Open `Home.vue` file, you get something like this :
```vue
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue"; // built-in vue functions
import DashboardLayout from "@components/Dashboard/Layout.vue"; // Dashboard base layout
// BuilderHome is where we define the component to use for the current page
// We will update it later
import BuilderHome from "@components/Builder/Home.vue";
// Global is utils for buttons or link actions
import { useGlobal } from "@utils/global";
// This is JSDOC
/**
* @name Page/Home.vue
* @description This component is the home page.
This page displays an overview of multiple stats related to BunkerWeb.
*/
// Get data for builder logic
const home = reactive({
builder: "",
});
onBeforeMount(() => {
// Get builder data
const dataAtt = "data-server-builder";
const dataEl = document.querySelector(`[${dataAtt}]`);
const data =
dataEl && !dataEl.getAttribute(dataAtt).includes(dataAtt)
? JSON.parse(atob(dataEl.getAttribute(dataAtt)))
: {};
home.builder = data;
});
// Use utils when app is built
onMounted(() => {
useGlobal();
});
</script>
<template>
<!-- Template part -->
<DashboardLayout>
<BuilderHome v-if="home.builder" :builder="home.builder" />
</DashboardLayout>
</template>
```
You need to update it like this :
```vue
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
// We will create it, or we can use
// import BuilderCollection from "@components/Builder/Collection.vue";
// to get all components but performance will be impacted
import BuilderExample from "@components/Builder/Example.vue";
import { useGlobal } from "@utils/global";
/**
* @name Page/Example.vue
* @description This component is the Example page.
*/
// Get data for builder logic
const example = reactive({
builder: "",
});
onBeforeMount(() => {
// Get builder data
const dataAtt = "data-server-builder";
const dataEl = document.querySelector(`[${dataAtt}]`);
const data =
dataEl && !dataEl.getAttribute(dataAtt).includes(dataAtt)
? JSON.parse(atob(dataEl.getAttribute(dataAtt)))
: {};
example.builder = data;
});
// Use utils when app is built
onMounted(() => {
useGlobal();
});
</script>
<template>
<!-- Template part -->
<DashboardLayout>
<BuilderExample v-if="example.builder" :builder="example.builder" />
<!-- alternative -->
<!-- <BuilderCollection v-if="example.builder" :builder="example.builder" /> -->
</DashboardLayout>
</template>
```
Open `index.html` file, you get something like this :
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/img/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 | Home</title>
</head>
<body>
<div
class="hidden"
data-server-global='{"username" : "admin", "plugins_page": [{"id" : "antibot", "name": "Antibot"}, {"id": "backup", "name" : "backup"}]}'
></div>
<div
class="hidden"
data-server-flash='[{"type" : "success", "title" : "success", "message" : "Success feedback"}, {"type" : "error", "title" : "error", "message" : "Error feedback"}, {"type" : "warning", "title" : "warning", "message" : "Warning feedback"}, {"type" : "info", "title" : "info", "message" : "Info feedback"}]'
></div>
<div
class="hidden"
data-server-builder=""
></div>
<div id="app"></div>
<script type="module" src="home.js"></script>
</body>
</html>
```
You need to change title and the script part like this :
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/img/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 | Example</title>
</head>
<body>
<div
class="hidden"
data-server-global='{"username" : "admin", "plugins_page": [{"id" : "antibot", "name": "Antibot"}, {"id": "backup", "name" : "backup"}]}'
></div>
<div
class="hidden"
data-server-flash='[{"type" : "success", "title" : "success", "message" : "Success feedback"}, {"type" : "error", "title" : "error", "message" : "Error feedback"}, {"type" : "warning", "title" : "warning", "message" : "Warning feedback"}, {"type" : "info", "title" : "info", "message" : "Info feedback"}]'
></div>
<div
class="hidden"
data-server-builder=""
></div>
<div id="app"></div>
<script type="module" src="example.js"></script>
</body>
</html>
```
**2.1 Create custom builder (optional)**
In case you don't want to use the `BuilderCollection` component but a custom and optimized one for your page, you need to create a new component in the `src/ui/client/dashboard/components/Builder` folder.
You can copy an existing component like `Collection.vue` and rename it to `Example.vue`, and inside it, you can remove useless components (and add new ones if needed).
**3. Access page on dev**
Now you can start dev your page, go to `src/ui/client` and run the following command :
```shell
npm install &&
npm run dev-dashboard
```
This will prepare vite and run a dev server, you can access your page on `http://localhost:3000/dashboard/pages/example/index.html`
**4. Create page builder**
The UI is using the JSdoc from components to generate a `widgets.py` that allow to create a component using python in a builder approach.
In order to create a builder for the new page, you need to go to `src/ui/client/builder/pages` and you can start by copying an existing page folder like `home.py` and rename it to `example.py`.
**4.1 Create test_builder (optional)**
In case you want to test your page and the result of the builder, you can create a test file in the `src/ui/client/builder` folder.
For example, you can copy the `test_bans.py` file and rename it to `test_example.py`.
You can import your builder, and run the builder with raw data to see the result.
You can also directly update the data on your dev page using the `save_builder` utils.
**5. Add page to build**
By default, the new pages will not be added to built app. We need to update the `vite.config.dashboard.js` file and inclue the new page on build :
```js
import { resolve } from "path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
import { comment } from "postcss";
// https://vitejs.dev/config/
export default defineConfig({
// ...
build: {
// ...
rollupOptions: {
input: {
// ...
// Add new page
example: resolve(__dirname, "./dashboard/pages/example/index.html"),
},
},
},
});
```
**6. Update flask part**
You need to know that pages and utils from `src/ui/client/builder/` will be accessible on built from the `main.py` file.
In order to add the page, you can use the existing `src/ui/pages` folder and follow how they are import in the `main.py` file.
After that, you will get the new page on the built app.
### Create a standalone page
Standalone page is an all-in-one html file with js, css and any resources directly in the file. This is useful for pages that don't need to be part of the dashboard, or for pages that can't access to external resources like the setup page.
A standalone is similar to a dashboard page, but the differences is that we need a specifig vite config file like `vite.config.standalone.js` that is using `viteSingleFile` plugin to enable a single file build.
If you want to create your own standalone page, you can copy the standalone folder in `src/ui/client/standalone` and rename it to your page name.
You need to create a vite config file based on the `vite.config.standalone.js` updating the build settings.
In case you want to add the standalone page in built app, you need to update the `build.py` file, and add the output file on the `template` folder **AFTER** the other logic.
Notice that standalone page is useful for custom plugins pages.
### Utils
Because the UI is using a builder approach, we need to create utils that will allow to interact with components or to allow actions without components interaction.
The utils are located in the `src/ui/client/dashboard/utils` folder, you have the following utils :
- **`global.js` :** You can find attributes based utils, this is utils that will listen to component attributes. It works well with some components and the `attrs` props. You can attach a link to a button to be redirect, or create a static value form to submit on click...
- **`filter.js` :** Filter component utils only.
- **`lang.js` :** All logic that is related to i18n, like getting the i18n instance, getting only needed keys, or getting the current language.
- **`etc`**
You have specific logic in `src/ui/client/dashboard/store` too :
- **`form.js` :** Allow to share and execute specific logic for advanced, raw or easy mode.
- **`global.js` :** Others share data and state. For example the `displayStore` is useful because it allows to show/hide an element after a button click.

View file

@ -82,9 +82,9 @@ try:
for chunk in resp.iter_content(chunk_size=4 * 1024):
if chunk:
file_content.write(chunk)
assert file_content
# Decompress it
LOGGER.info("Decompressing mmdb file ...")
file_content.seek(0)

View file

@ -82,9 +82,9 @@ try:
for chunk in resp.iter_content(chunk_size=4 * 1024):
if chunk:
file_content.write(chunk)
assert file_content
# Decompress it
LOGGER.info("Decompressing mmdb file ...")
file_content.seek(0)

View file

@ -147,10 +147,12 @@ try:
LOGGER.warning(f"Domains for {first_server} are not the same as in the certificate, asking new certificate...")
domains_to_ask[first_server] = True
continue
elif ("TEST_CERT" in current_domains.groupdict()['expiry_date'] and getenv(f"{first_server}_")):
elif "TEST_CERT" in current_domains.groupdict()["expiry_date"] and getenv(f"{first_server}_"):
LOGGER.warning(f"Certificate environment (staging/production) changed for {first_server}, asking new certificate...")
use_letsencrypt_staging = getenv(f"{first_server}_USE_LETS_ENCRYPT_STAGING", getenv("USE_LETS_ENCRYPT_STAGING", "no")) == "yes"
if ("TEST_CERT" in current_domains.groupdict()['expiry_date'] and not use_letsencrypt_staging) or ("TEST_CERT" not in current_domains.groupdict()['expiry_date'] and use_letsencrypt_staging):
if ("TEST_CERT" in current_domains.groupdict()["expiry_date"] and not use_letsencrypt_staging) or (
"TEST_CERT" not in current_domains.groupdict()["expiry_date"] and use_letsencrypt_staging
):
LOGGER.warning(f"Certificate environment (staging/production) changed for {first_server}, asking new certificate...")
domains_to_ask[first_server] = True
LOGGER.info(f"Certificates already exists for domain(s) {domains}")

View file

@ -1,4 +1,4 @@
from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, Logger, _nameToLevel, addLevelName, basicConfig, getLogger, setLoggerClass
from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, FileHandler, Formatter, Logger, _nameToLevel, addLevelName, basicConfig, getLogger, setLoggerClass
from os import getenv
from typing import Optional, Union

View file

@ -6,7 +6,6 @@ from datetime import datetime
from io import BytesIO
from itertools import chain
from json import load as json_load
from logging import FileHandler, Formatter
from os import _exit, environ, getenv, getpid, sep
from os.path import join
from pathlib import Path

View file

@ -1,13 +1,12 @@
from datetime import datetime
from threading import Thread
from time import time
from typing import Dict
from flask import Blueprint, redirect, render_template, request, url_for
from flask_login import login_required
from app.dependencies import BW_CONFIG, DATA, DB
from app.routes.utils import get_remain, handle_error, manage_bunkerweb, verify_data_in_form, wait_applying
from app.utils import LOGGER, flash
from app.utils import flash
pro = Blueprint("pro", __name__)

View file

@ -179,7 +179,9 @@ button.list-group-item-secondary.active {
background-color: var(--bs-bw-green); /* Initial background color */
color: #fff;
animation: colorPhase 3s infinite; /* Apply the color phasing animation */
transition: background-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; /* Smooth transitions */
transition:
background-color 0.3s ease-in-out,
box-shadow 0.3s ease-in-out; /* Smooth transitions */
}
.buy-now .btn-buy-now:hover {
@ -520,7 +522,9 @@ a.badge:hover {
.setting-highlight {
background-color: rgba(var(--bs-bw-green-rgb), 0.5);
transition: background-color 2s ease, opacity 2s ease;
transition:
background-color 2s ease,
opacity 2s ease;
opacity: 1;
}

View file

@ -95,14 +95,14 @@ $(document).ready(function () {
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
<div class="fw-bold">Time left</div>
</li>
</ul>`
</ul>`,
);
$("#selected-ips-unban").append(list);
bans.forEach((ban) => {
// Create the list item using template literals
const list = $(
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
);
const listItem =
@ -130,8 +130,8 @@ $(document).ready(function () {
.find(".alert")
.text(
`Are you sure you want to unban the selected IP address${"es".repeat(
bans.length > 1
)}?`
bans.length > 1,
)}?`,
);
modal.show();
@ -453,7 +453,7 @@ $(document).ready(function () {
$("#bans_wrapper .dt-buttons")
.attr(
"data-bs-original-title",
"The database is in readonly, therefore you cannot add bans."
"The database is in readonly, therefore you cannot add bans.",
)
.attr("data-bs-placement", "right")
.tooltip();
@ -576,7 +576,7 @@ $(document).ready(function () {
});
const ipRegex = new RegExp(
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?!$)|$)){4}$|^((?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|:(?::[A-Fa-f0-9]{1,4}){1,7}|::)$/i
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?!$)|$)){4}$|^((?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|:(?::[A-Fa-f0-9]{1,4}){1,7}|::)$/i,
);
const validateBan = (ban, ipSet) => {
@ -679,14 +679,14 @@ $(document).ready(function () {
type: "hidden",
name: "csrf_token",
value: $("#csrf_token").val(),
})
}),
);
form.append(
$("<input>", {
type: "hidden",
name: "bans",
value: JSON.stringify(bans),
})
}),
);
// Append the form to the body and submit it

View file

@ -207,15 +207,15 @@ $(document).ready(function () {
});
$(`#DataTables_Table_0 span[title='${cacheJobNameSelection}']`).trigger(
"click"
"click",
);
$(`#DataTables_Table_1 span[title='${cachePluginSelection}']`).trigger(
"click"
"click",
);
$(`#DataTables_Table_2 span[title='${cacheServiceSelection}']`).trigger(
"click"
"click",
);
$("#cache").removeClass("d-none");

View file

@ -59,7 +59,8 @@ $(document).ready(function () {
$typeDropdownItems.each(function () {
const item = $(this);
item.toggle(
selectedService === "no service" || item.data("context") === "multisite"
selectedService === "no service" ||
item.data("context") === "multisite",
);
});
};
@ -86,13 +87,13 @@ $(document).ready(function () {
if (visibleItems === 0) {
if ($serviceDropdownMenu.find(".no-service-items").length === 0) {
$serviceDropdownMenu.append(
'<li class="no-service-items dropdown-item text-muted">No Item</li>'
'<li class="no-service-items dropdown-item text-muted">No Item</li>',
);
}
} else {
$serviceDropdownMenu.find(".no-service-items").remove();
}
}, 50)
}, 50),
);
$(document).on("hidden.bs.dropdown", "#select-service", function () {
@ -107,7 +108,7 @@ $(document).ready(function () {
$(`#config-type-${selectedType}`).data("context") !== "multisite"
) {
const firstMultisiteType = $(
`#types-dropdown-menu li.nav-item[data-context="multisite"]`
`#types-dropdown-menu li.nav-item[data-context="multisite"]`,
).first();
$("#select-type")
.parent()
@ -115,7 +116,7 @@ $(document).ready(function () {
"data-bs-original-title",
`Switched to ${firstMultisiteType
.text()
.trim()} as ${selectedType} is not a valid multisite type.`
.trim()} as ${selectedType} is not a valid multisite type.`,
)
.tooltip("show");
@ -167,13 +168,14 @@ $(document).ready(function () {
if (!configName) {
errorMessage = "A custom configuration name is required.";
isValid = false;
} else if (pattern && !new RegExp(pattern).test(configName)) isValid = false;
} else if (pattern && !new RegExp(pattern).test(configName))
isValid = false;
if (!isValid) {
$configInput
.attr(
"data-bs-original-title",
errorMessage || "Please enter a valid configuration name."
errorMessage || "Please enter a valid configuration name.",
)
.tooltip("show");
@ -195,35 +197,35 @@ $(document).ready(function () {
type: "hidden",
name: "service",
value: $("<div>").text(selectedService).html(),
})
}),
);
form.append(
$("<input>", {
type: "hidden",
name: "type",
value: $("<div>").text(selectedType).html(),
})
}),
);
form.append(
$("<input>", {
type: "hidden",
name: "name",
value: $("<div>").text(configName).html(),
})
}),
);
form.append(
$("<input>", {
type: "hidden",
name: "value",
value: $("<div>").text(value).html(),
})
}),
);
form.append(
$("<input>", {
type: "hidden",
name: "csrf_token",
value: $("<div>").text($("#csrf_token").val()).html(), // Sanitize the value
})
}),
);
$(window).off("beforeunload");

View file

@ -59,13 +59,13 @@ $(document).ready(function () {
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 1 0;">
<div class="fw-bold">Service</div>
</li>
</ul>`
</ul>`,
);
$("#selected-configs-delete").append(list);
configs.forEach((config) => {
const list = $(
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
);
// Create the list item using template literals
@ -79,13 +79,13 @@ $(document).ready(function () {
const id = `${config.type.toLowerCase()}-${config.service.replaceAll(
".",
"_"
"_",
)}-${config.name}`;
// Clone the type element and append it to the list item
const typeClone = $(`#type-${id}`).clone();
const typeListItem = $(
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
);
typeListItem.append(typeClone.removeClass("highlight"));
list.append(typeListItem);
@ -93,7 +93,7 @@ $(document).ready(function () {
// Clone the service element and append it to the list item
const serviceClone = $(`#service-${id}`).clone();
const serviceListItem = $(
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
);
serviceListItem.append(serviceClone.removeClass("highlight"));
list.append(serviceListItem);
@ -107,8 +107,8 @@ $(document).ready(function () {
.find(".alert")
.text(
`Are you sure you want to delete the selected custom configuration${"s".repeat(
configs.length > 1
)}?`
configs.length > 1,
)}?`,
);
modal.show();
@ -431,7 +431,7 @@ $(document).ready(function () {
$("#configs_wrapper .dt-buttons")
.attr(
"data-bs-original-title",
"The database is in readonly, therefore you cannot create new custom configurations."
"The database is in readonly, therefore you cannot create new custom configurations.",
)
.attr("data-bs-placement", "right")
.tooltip();
@ -439,11 +439,11 @@ $(document).ready(function () {
});
$(`#DataTables_Table_0 span[title='${configTypeSelection}']`).trigger(
"click"
"click",
);
$(`#DataTables_Table_2 span[title='${configServiceSelection}']`).trigger(
"click"
"click",
);
$("#configs").removeClass("d-none");

View file

@ -193,7 +193,7 @@ $(function () {
getColor(from + 1) +
'"></i> ' +
from +
(to ? "&ndash;" + to : "+")
(to ? "&ndash;" + to : "+"),
);
}
@ -210,7 +210,7 @@ $(function () {
// Ensure each value is properly converted to a number
const totalRequests = Object.values(requestsData).reduce(
(acc, curr) => acc + parseInt(curr, 10), // Parse as integer
0 // Initial value for the accumulator
0, // Initial value for the accumulator
);
const blockedRequests = Object.keys(requestsData).reduce((acc, key) => {
@ -305,7 +305,7 @@ $(function () {
const requestsChart = new ApexCharts(
document.querySelector("#requests-stats"),
requestsOptions
requestsOptions,
);
requestsChart.render();
@ -396,7 +396,7 @@ $(function () {
const ipsChart = new ApexCharts(
document.querySelector("#requests-ips"),
ipsOptions
ipsOptions,
);
ipsChart.render();
}
@ -406,10 +406,10 @@ $(function () {
const blockingData = JSON.parse($("#requests-blocking-data").text());
const dataValues = Object.values(blockingData).map((value) =>
parseInt(value, 10)
parseInt(value, 10),
);
const categories = Object.keys(blockingData).map((key) =>
new Date(key).toLocaleTimeString()
new Date(key).toLocaleTimeString(),
);
const minValue = Math.min(...dataValues);
@ -498,7 +498,7 @@ $(function () {
const blockingChart = new ApexCharts(
document.querySelector("#requests-blocking"),
blockingOptions
blockingOptions,
);
blockingChart.render();
});

View file

@ -75,14 +75,14 @@ $(document).ready(function () {
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
<div class="fw-bold">Health</div>
</li>
</ul>`
</ul>`,
);
$("#selected-instances").append(list);
const delete_modal = $("#modal-delete-instances");
instances.forEach((instance) => {
const list = $(
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
);
// Create the list item using template literals
@ -97,7 +97,7 @@ $(document).ready(function () {
// Clone the status element and append it to the list item
const statusClone = $("#status-" + instance).clone();
const statusListItem = $(
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`,
);
statusListItem.append(statusClone.removeClass("highlight"));
list.append(statusListItem);
@ -124,14 +124,14 @@ $(document).ready(function () {
type: "hidden",
name: "csrf_token",
value: $("#csrf_token").val(),
})
}),
);
form.append(
$("<input>", {
type: "hidden",
name: "instances",
value: instances.join(","),
})
}),
);
// Append the form to the body and submit it
@ -564,7 +564,7 @@ $(document).ready(function () {
$("#instances_wrapper .dt-buttons")
.attr(
"data-bs-original-title",
"The database is in readonly, therefore you cannot create new instances."
"The database is in readonly, therefore you cannot create new instances.",
)
.attr("data-bs-placement", "right")
.tooltip();

View file

@ -121,14 +121,14 @@ $(document).ready(function () {
type: "hidden",
name: "csrf_token",
value: $("#csrf_token").val(),
})
}),
);
form.append(
$("<input>", {
type: "hidden",
name: "jobs",
value: JSON.stringify(jobs),
})
}),
);
// Append the form to the body and submit it
@ -369,7 +369,7 @@ $(document).ready(function () {
.html(
`Last${historyCount > 1 ? " " + historyCount : ""} execution${
historyCount > 1 ? "s" : ""
} of Job <span class="fw-bold fst-italic">${job}</span> from plugin <span class="fw-bold fst-italic">${plugin}</span>`
} of Job <span class="fw-bold fst-italic">${job}</span> from plugin <span class="fw-bold fst-italic">${plugin}</span>`,
);
history.removeClass("visually-hidden");
historyModal.find(".modal-body").html(history);

View file

@ -21,13 +21,13 @@ $(document).ready(function () {
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 1 0;">
<div class="fw-bold">Type</div>
</li>
</ul>`
</ul>`,
);
$("#selected-plugins-delete").append(list);
plugins.forEach((plugin) => {
const list = $(
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
);
// Create the list item using template literals
@ -42,7 +42,7 @@ $(document).ready(function () {
// Clone the version element and append it to the list item
const versionClone = $(`#version-${plugin}`).clone();
const versionListItem = $(
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
);
versionListItem.append(versionClone.removeClass("highlight"));
list.append(versionListItem);
@ -50,7 +50,7 @@ $(document).ready(function () {
// Clone the type element and append it to the list item
const typeClone = $(`#type-${plugin}`).clone();
const typeListItem = $(
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
);
typeListItem.append(typeClone.removeClass("highlight"));
list.append(typeListItem);
@ -64,8 +64,8 @@ $(document).ready(function () {
.find(".alert")
.text(
`Are you sure you want to delete the selected plugin${"s".repeat(
plugins.length > 1
)}?`
plugins.length > 1,
)}?`,
);
modal.show();
@ -113,7 +113,7 @@ $(document).ready(function () {
// Create a progress bar element
const progressBar = $(
'<div class="progress-bar" role="progressbar" style="width: 0%;"></div>'
'<div class="progress-bar" role="progressbar" style="width: 0%;"></div>',
);
const progress = $('<div class="progress mt-2"></div>').append(progressBar);
fileList.append(progress);
@ -447,7 +447,7 @@ $(document).ready(function () {
$("#plugins_wrapper .dt-buttons")
.attr(
"data-bs-original-title",
"The database is in readonly, therefore you cannot create add plugins."
"The database is in readonly, therefore you cannot create add plugins.",
)
.attr("data-bs-placement", "right")
.tooltip();

View file

@ -283,7 +283,7 @@ $(function () {
if (country) {
$(`[data-bs-original-title="${countryCode}"]`).attr(
"data-bs-original-title",
country
country,
);
}
});

View file

@ -22,7 +22,7 @@ $(function () {
services.forEach((service) => {
const sanitizedService = service.replace(/\./g, "-");
const serviceList = $(
'<ul class="list-group list-group-horizontal d-flex w-100"></ul>'
'<ul class="list-group list-group-horizontal d-flex w-100"></ul>',
);
const listItem = $(`
@ -57,7 +57,7 @@ $(function () {
.text(
`Are you sure you want to convert the selected service${
services.length > 1 ? "s" : ""
} to ${conversionType}?`
} to ${conversionType}?`,
);
convertModal
.find("button[type=submit]")
@ -78,7 +78,7 @@ $(function () {
.text(
`Are you sure you want to delete the selected service${
services.length > 1 ? "s" : ""
}?`
}?`,
);
const modalInstance = new bootstrap.Modal(deleteModal);
modalInstance.show();
@ -184,10 +184,10 @@ $(function () {
.empty();
$(this)
.find(
"#selected-services-input-convert, #selected-services-input-delete"
"#selected-services-input-convert, #selected-services-input-delete",
)
.val("");
}
},
);
const getSelectedServices = () =>
@ -230,7 +230,7 @@ $(function () {
const filteredServices = services.filter((service) => {
const serviceType = $(`#type-${service.replace(/\./g, "-")}`).data(
"value"
"value",
);
return serviceType !== conversionType;
});
@ -408,7 +408,7 @@ $(function () {
.find(".dt-buttons")
.attr(
"data-bs-original-title",
"The database is in read-only mode; you cannot create new services."
"The database is in read-only mode; you cannot create new services.",
)
.attr("data-bs-placement", "right")
.tooltip();

View file

@ -33,22 +33,22 @@ $(document).ready(() => {
isValid = validateCondition(
password.length >= 8,
"#length-check i",
isValid
isValid,
);
isValid = validateCondition(
/[A-Z]/.test(password),
"#uppercase-check i",
isValid
isValid,
);
isValid = validateCondition(
/\d/.test(password),
"#number-check i",
isValid
isValid,
);
isValid = validateCondition(
/[ -~]/.test(password),
"#special-check i",
isValid
isValid,
); // Check for special characters
return isValid;
@ -165,7 +165,7 @@ $(document).ready(() => {
if (!$feedback.length) {
const $textSpan = $input.parent().find("span.input-group-text");
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
$textSpan.length ? $textSpan : $input
$textSpan.length ? $textSpan : $input,
);
}
@ -184,7 +184,7 @@ $(document).ready(() => {
.find("i")
.toggleClass(
"bx-question-mark text-warning bx-check text-success",
false
false,
)
.toggleClass("bx-x text-danger", true);
} else {
@ -255,7 +255,7 @@ $(document).ready(() => {
if (!$feedback.length) {
const $textSpan = $input.parent().find("span.input-group-text");
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
$textSpan.length ? $textSpan : $input
$textSpan.length ? $textSpan : $input,
);
}
@ -374,7 +374,7 @@ $(document).ready(() => {
}
if (!uiReverseProxy) {
$("#overview_service_url").val(
`https://${getServerName()}${$("#REVERSE_PROXY_URL").val()}`
`https://${getServerName()}${$("#REVERSE_PROXY_URL").val()}`,
);
}
};
@ -403,7 +403,7 @@ $(document).ready(() => {
.parent()
.find("span.input-group-text");
$feedback = $(
'<div class="invalid-feedback">Passwords do not match.</div>'
'<div class="invalid-feedback">Passwords do not match.</div>',
).insertAfter($textSpan.length ? $textSpan : $confirmPasswordInput);
} else {
$feedback.text("Passwords do not match.");
@ -424,13 +424,13 @@ $(document).ready(() => {
if (typeof result === "string") {
$("#dns-check-title").text("Error");
$("#dns-check-result").html(
`Are you sure you want to proceed to the next step?<br/>Error: ${result}`
`Are you sure you want to proceed to the next step?<br/>Error: ${result}`,
);
modal.modal("show");
} else if (!result) {
$("#dns-check-title").text("Server name is not unique");
$("#dns-check-result").html(
`Are you sure you want to proceed to the next step?<br/>Server name "${serverName}" is not unique.`
`Are you sure you want to proceed to the next step?<br/>Server name "${serverName}" is not unique.`,
);
modal.modal("show");
} else {
@ -456,7 +456,7 @@ $(document).ready(() => {
debounce(function () {
const isValid = validatePassword();
updateValidationState(this, isValid);
}, 100)
}, 100),
);
// Real-time validation for other plugin settings
@ -471,7 +471,7 @@ $(document).ready(() => {
$this
.toggleClass("is-valid", isValid)
.toggleClass("is-invalid", !isValid);
}, 100)
}, 100),
);
// Remove validation state on focus out
@ -509,11 +509,11 @@ $(document).ready(() => {
formData.append("ui_url", ui_url);
formData.append(
"auto_lets_encrypt",
$("#AUTO_LETS_ENCRYPT").prop("checked") ? "yes" : "no"
$("#AUTO_LETS_ENCRYPT").prop("checked") ? "yes" : "no",
);
formData.append(
"lets_encrypt_staging",
$("#LETS_ENCRYPT_STAGING").prop("checked") ? "yes" : "no"
$("#LETS_ENCRYPT_STAGING").prop("checked") ? "yes" : "no",
);
formData.append("email_lets_encrypt", $("#EMAIL_LETS_ENCRYPT").val());
}

View file

@ -124,7 +124,7 @@ $(document).ready(() => {
if ($collapseContainer.length && !$collapseContainer.hasClass("show")) {
// Expand the multiple setting group if it's collapsed
const toggleButton = $(
`[data-bs-target="#${$collapseContainer.attr("id")}"]`
`[data-bs-target="#${$collapseContainer.attr("id")}"]`,
);
toggleButton.trigger("click");
}
@ -195,7 +195,7 @@ $(document).ready(() => {
let $feedback = $input.next(".invalid-feedback");
if (!$feedback.length) {
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
$input
$input,
);
}
@ -229,7 +229,7 @@ $(document).ready(() => {
type: "hidden",
name: name,
value: $("<div>").text(value).html(), // Sanitize the value
})
}),
);
};
@ -400,13 +400,13 @@ $(document).ready(() => {
if (visibleItems === 0) {
if ($pluginDropdownMenu.find(".no-plugin-items").length === 0) {
$pluginDropdownMenu.append(
'<li class="no-plugin-items dropdown-item text-muted">No Item</li>'
'<li class="no-plugin-items dropdown-item text-muted">No Item</li>',
);
}
} else {
$pluginDropdownMenu.find(".no-plugin-items").remove();
}
}, 50)
}, 50),
);
$("#select-template").on("click", () => $templateSearch.focus());
@ -431,13 +431,13 @@ $(document).ready(() => {
if (visibleItems === 0) {
if ($templateDropdownMenu.find(".no-template-items").length === 0) {
$templateDropdownMenu.append(
'<li class="no-template-items dropdown-item text-muted">No Item</li>'
'<li class="no-template-items dropdown-item text-muted">No Item</li>',
);
}
} else {
$templateDropdownMenu.find(".no-template-items").remove();
}
}, 50)
}, 50),
);
$(document).on("hidden.bs.dropdown", "#select-plugin", function () {
@ -453,21 +453,21 @@ $(document).ready(() => {
"shown.bs.tab",
(e) => {
handleModeChange($(e.target).data("bs-target"));
}
},
);
$('#plugins-dropdown-menu button[data-bs-toggle="tab"]').on(
"shown.bs.tab",
(e) => {
handleTabChange($(e.target).data("bs-target"));
}
},
);
$('#templates-dropdown-menu button[data-bs-toggle="tab"]').on(
"shown.bs.tab",
(e) => {
handleTabChange($(e.target).data("bs-target"));
}
},
);
$(document).on("input", ".plugin-setting", function () {
@ -553,7 +553,7 @@ $(document).ready(() => {
if (matchedPlugin) {
// Automatically switch to the plugin tab
$(`button[data-bs-target="#navs-plugins-${matchedPlugin}"]`).tab(
"show"
"show",
);
// Highlight all matched settings
@ -561,7 +561,7 @@ $(document).ready(() => {
highlightSettings(matchedSettings, 1000);
}
}
}, 100)
}, 100),
);
$(document).on("click", ".show-multiple", function () {
@ -569,7 +569,7 @@ $(document).ready(() => {
$(this).html(
`<i class="bx bx-${
toggleText === "SHOW" ? "hide" : "show-alt"
} bx-sm"></i>&nbsp;${toggleText}`
} bx-sm"></i>&nbsp;${toggleText}`,
);
});
@ -584,7 +584,7 @@ $(document).ready(() => {
$(this)
.find(".multiple-collapse")
.attr("id")
.replace(`${multipleId}-`, "")
.replace(`${multipleId}-`, ""),
);
})
.get()
@ -696,7 +696,7 @@ $(document).ready(() => {
$(this)
.find(".multiple-collapse")
.attr("id")
.replace(`${multipleId}-`, "")
.replace(`${multipleId}-`, ""),
);
if (containerSuffix > suffix) {
$(this).before(multipleClone); // Insert before the first container with a higher suffix
@ -797,7 +797,7 @@ $(document).ready(() => {
$(".toggle-draft").html(
`<i class="bx bx-sm bx-${
isDraft ? "globe" : "file-blank"
} bx-sm"></i>&nbsp;${isDraft ? "Online" : "Draft"}`
} bx-sm"></i>&nbsp;${isDraft ? "Online" : "Draft"}`,
);
});
@ -961,7 +961,7 @@ $(document).ready(() => {
.parent()
.attr(
"title",
"Cannot remove because one or more settings are disabled"
"Cannot remove because one or more settings are disabled",
);
new bootstrap.Tooltip(
@ -970,7 +970,7 @@ $(document).ready(() => {
.get(0),
{
placement: "top",
}
},
);
}
});
@ -986,7 +986,7 @@ $(document).ready(() => {
if (currentMode === "easy" && currentTemplate !== "high") {
$(`button[data-bs-target="#navs-templates-${currentTemplate}"]`).tab(
"show"
"show",
);
}
@ -1012,7 +1012,7 @@ $(document).ready(() => {
const hash = window.location.hash;
if (hash) {
const targetTab = $(
`button[data-bs-target="#navs-plugins-${hash.substring(1)}"]`
`button[data-bs-target="#navs-plugins-${hash.substring(1)}"]`,
);
if (targetTab.length) targetTab.tab("show");
}
@ -1029,7 +1029,7 @@ $(document).ready(() => {
$pluginTypeSelect.val("all");
} else
$(`button[data-bs-target="#navs-plugins-${currentPlugin}"]`).tab(
"show"
"show",
);
if (currentPlugin !== "general") {
@ -1053,7 +1053,7 @@ $(document).ready(() => {
feedbackToast
.find("div.toast-body")
.html(
"<p>As the service method is set to autoconf, the configuration is locked. <div class='fw-bolder'>Any changes made will not be saved.</div><div class='fst-italic'>This is to prevent conflicts with the autoconf and the web UI.</div></p>"
"<p>As the service method is set to autoconf, the configuration is locked. <div class='fw-bolder'>Any changes made will not be saved.</div><div class='fst-italic'>This is to prevent conflicts with the autoconf and the web UI.</div></p>",
);
feedbackToast.attr("data-bs-autohide", "false");
feedbackToast.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container

View file

@ -35,20 +35,20 @@ class News {
) {
sessionStorage.setItem(
"lastRefetch",
Math.round(Date.now() / 1000) + 3600
Math.round(Date.now() / 1000) + 3600,
);
sessionStorage.setItem("lastNews", JSON.stringify(lastNews));
const newsNumber = lastNews.length;
$("#news-pill").append(
DOMPurify.sanitize(
`<span class="badge rounded-pill badge-center-sm bg-danger ms-1_5">${newsNumber}</span>`
)
`<span class="badge rounded-pill badge-center-sm bg-danger ms-1_5">${newsNumber}</span>`,
),
);
$("#news-button").after(
DOMPurify.sanitize(
`<span class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">${newsNumber}<span class="visually-hidden">unread news</span></span>`
)
`<span class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">${newsNumber}<span class="visually-hidden">unread news</span></span>`,
),
);
}
@ -84,7 +84,7 @@ class News {
news.tags,
news.date,
isLast,
false // isHome is false
false, // isHome is false
);
newsRow.append(cardElement);
@ -98,7 +98,7 @@ class News {
news.tags,
news.date,
isLast,
true // isHome is true
true, // isHome is true
);
homeNewsRow.append(homeCardElement);
}
@ -131,13 +131,13 @@ class News {
class: "card-img card-img-left",
src: img,
alt: "News image",
})
}),
);
imgCol.append(imgLink);
const contentCol = $("<div>", { class: "col-md-7" }).appendTo(row);
const cardBody = $("<div>", { class: "card-body p-3" }).appendTo(
contentCol
contentCol,
);
const cardTitle = $("<h6>", { class: "card-title lh-sm mb-2" }).append(
@ -146,7 +146,7 @@ class News {
target: "_blank",
rel: "noopener",
text: title,
})
}),
);
const cardText = $("<small>", {
@ -163,7 +163,7 @@ class News {
$("<small>", {
class: "text-muted courier-prime",
text: `Posted on: ${date}`,
})
}),
)
.appendTo(cardFooter);
@ -181,7 +181,7 @@ class News {
$("<span>", {
class: "tf-icons bx bx-xs bx-purchase-tag me-1",
}),
tag.name
tag.name,
)
.appendTo(tagsContainer);
});
@ -201,7 +201,7 @@ class News {
class: "card-img-top",
src: img,
alt: "News image",
})
}),
);
const cardBody = $("<div>", { class: "card-body" });
@ -211,7 +211,7 @@ class News {
target: "_blank",
rel: "noopener",
text: title,
})
}),
);
const cardText = $("<p>", {
class: "card-text courier-prime",
@ -232,7 +232,7 @@ class News {
$("<span>", {
class: "tf-icons bx bx-xs bx-purchase-tag bx-18px me-2",
}),
tag.name
tag.name,
)
.appendTo(tagsContainer);
});
@ -241,7 +241,7 @@ class News {
$("<small>", {
class: "text-muted courier-prime",
text: `Posted on: ${date}`,
})
}),
);
cardBody.append(cardTitle, cardText, tagsContainer, dateText);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -61,4 +61,3 @@
}
}
}

View file

@ -60,4 +60,4 @@
"reset": "Tetapkan Semula Zum"
}
}
}
}

View file

@ -60,4 +60,4 @@
"reset": "Đặt lại thu phóng"
}
}
}
}

View file

@ -5,7 +5,7 @@
<div class="d-flex align-items-center justify-content-center h-100">
<div class="text-center text-primary">
<p id="config-waiting"
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Coming soon...</p>
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Plugin pages are not yet available during the beta phase.</p>
</div>
</div>
</div>

View file

@ -80,7 +80,6 @@ services:
volumes:
bw-data:
networks:
bw-universe:
name: bw-universe

View file

@ -70,7 +70,6 @@ services:
volumes:
bw-data:
networks:
bw-universe:
name: bw-universe

View file

@ -71,7 +71,6 @@ services:
volumes:
bw-data:
networks:
bw-universe:
name: bw-universe

View file

@ -89,7 +89,6 @@ volumes:
bw-data:
bw-db:
networks:
bw-universe:
name: bw-universe