Merge remote-tracking branch 'origin/dev' into ui

This commit is contained in:
Jordan Blasenhauer 2024-01-09 10:56:53 +01:00
commit 0b2a784253
41 changed files with 497 additions and 4357 deletions

View file

@ -40,7 +40,7 @@ jobs:
run: pip install --no-cache-dir --require-hashes -r misc/requirements-ansible.txt
if: inputs.TYPE != 'k8s'
- name: Install ansible libs
run: ansible-galaxy install --timeout 120 monolithprojects.github_actions_runner,1.18.1 && ansible-galaxy collection install --timeout 120 community.general
run: ansible-galaxy install --timeout 120 monolithprojects.github_actions_runner,1.18.1 && ansible-galaxy collection install --timeout 120 community.general && ansible-galaxy collection install --timeout 120 community.docker
if: inputs.TYPE != 'k8s'
# Create infra
- run: ./tests/create.sh ${{ inputs.TYPE }}

View file

@ -251,10 +251,10 @@ You will find more information in the [Kubernetes section](https://docs.bunkerwe
List of supported Linux distros :
- Debian 11 "Bullseye"
- Debian 12 "Bookworm"
- Ubuntu 22.04 "Jammy"
- Fedora 38
- RHEL 8.7
- Fedora 39
- RHEL 8.9
Repositories of Linux packages for BunkerWeb are available on [PackageCloud](https://packagecloud.io/bunkerity/bunkerweb), they provide a bash script to automatically add and trust the repository (but you can also follow the [manual installation](https://packagecloud.io/bunkerity/bunkerweb/install) instructions if you prefer).
@ -268,10 +268,10 @@ You will find more information in the [Linux section](https://docs.bunkerweb.io/
List of supported Linux distros :
- Debian 11 "Bullseye"
- Debian 12 "Bookworm"
- Ubuntu 22.04 "Jammy"
- Fedora 38
- RHEL 8.7
- Fedora 39
- RHEL 8.9
[Ansible](https://www.ansible.com/) is an IT automation tool. It can configure systems, deploy software, and orchestrate more advanced IT tasks such as continuous deployments or zero downtime rolling updates.
@ -343,13 +343,13 @@ Here is the list of "official" plugins that we maintain (see the [bunkerweb-plug
| Name | Version | Description | Link |
| :------------: | :-----: | :------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------: |
| **ClamAV** | 1.2 | Automatically scans uploaded files with the ClamAV antivirus engine and denies the request when a file is detected as malicious. | [bunkerweb-plugins/clamav](https://github.com/bunkerity/bunkerweb-plugins/tree/main/clamav) |
| **Coraza** | 1.2 | Inspect requests using a the Coraza WAF (alternative of ModSecurity). | [bunkerweb-plugins/coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) |
| **CrowdSec** | 1.2 | CrowdSec bouncer for BunkerWeb. | [bunkerweb-plugins/crowdsec](https://github.com/bunkerity/bunkerweb-plugins/tree/main/crowdsec) |
| **Discord** | 1.2 | Send security notifications to a Discord channel using a Webhook. | [bunkerweb-plugins/discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) |
| **Slack** | 1.2 | Send security notifications to a Slack channel using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) |
| **VirusTotal** | 1.2 | Automatically scans uploaded files with the VirusTotal API and denies the request when a file is detected as malicious. | [bunkerweb-plugins/virustotal](https://github.com/bunkerity/bunkerweb-plugins/tree/main/virustotal) |
| **WebHook** | 1.2 | Send security notifications to a custom HTTP endpoint using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) |
| **ClamAV** | 1.3 | Automatically scans uploaded files with the ClamAV antivirus engine and denies the request when a file is detected as malicious. | [bunkerweb-plugins/clamav](https://github.com/bunkerity/bunkerweb-plugins/tree/main/clamav) |
| **Coraza** | 1.3 | Inspect requests using a the Coraza WAF (alternative of ModSecurity). | [bunkerweb-plugins/coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) |
| **CrowdSec** | 1.3 | CrowdSec bouncer for BunkerWeb. | [bunkerweb-plugins/crowdsec](https://github.com/bunkerity/bunkerweb-plugins/tree/main/crowdsec) |
| **Discord** | 1.3 | Send security notifications to a Discord channel using a Webhook. | [bunkerweb-plugins/discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) |
| **Slack** | 1.3 | Send security notifications to a Slack channel using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) |
| **VirusTotal** | 1.3 | Automatically scans uploaded files with the VirusTotal API and denies the request when a file is detected as malicious. | [bunkerweb-plugins/virustotal](https://github.com/bunkerity/bunkerweb-plugins/tree/main/virustotal) |
| **WebHook** | 1.3 | Send security notifications to a custom HTTP endpoint using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) |
You will find more information in the [plugins section](https://docs.bunkerweb.io/1.5.5/plugins/?utm_campaign=self&utm_source=github) of the documentation.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

View file

@ -8,13 +8,13 @@ Here is the list of "official" plugins that we maintain (see the [bunkerweb-plug
| Name | Version | Description | Link |
| :------------: | :-----: | :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------: |
| **ClamAV** | 1.2 | Automatically scans uploaded files with the ClamAV antivirus engine and denies the request when a file is detected as malicious. | [bunkerweb-plugins/clamav](https://github.com/bunkerity/bunkerweb-plugins/tree/main/clamav) |
| **Coraza** | 1.2 | Inspect requests using a the Coraza WAF (alternative of ModSecurity). | [bunkerweb-plugins/coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) |
| **CrowdSec** | 1.2 | CrowdSec bouncer for BunkerWeb. | [bunkerweb-plugins/crowdsec](https://github.com/bunkerity/bunkerweb-plugins/tree/main/crowdsec) |
| **Discord** | 1.2 | Send security notifications to a Discord channel using a Webhook. | [bunkerweb-plugins/discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) |
| **Slack** | 1.2 | Send security notifications to a Slack channel using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) |
| **VirusTotal** | 1.2 | Automatically scans uploaded files with the VirusTotal API and denies the request when a file is detected as malicious. | [bunkerweb-plugins/virustotal](https://github.com/bunkerity/bunkerweb-plugins/tree/main/virustotal) |
| **WebHook** | 1.2 | Send security notifications to a custom HTTP endpoint using a Webhook. | [bunkerweb-plugins/webhook](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) |
| **ClamAV** | 1.3 | Automatically scans uploaded files with the ClamAV antivirus engine and denies the request when a file is detected as malicious. | [bunkerweb-plugins/clamav](https://github.com/bunkerity/bunkerweb-plugins/tree/main/clamav) |
| **Coraza** | 1.3 | Inspect requests using a the Coraza WAF (alternative of ModSecurity). | [bunkerweb-plugins/coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) |
| **CrowdSec** | 1.3 | CrowdSec bouncer for BunkerWeb. | [bunkerweb-plugins/crowdsec](https://github.com/bunkerity/bunkerweb-plugins/tree/main/crowdsec) |
| **Discord** | 1.3 | Send security notifications to a Discord channel using a Webhook. | [bunkerweb-plugins/discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) |
| **Slack** | 1.3 | Send security notifications to a Slack channel using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) |
| **VirusTotal** | 1.3 | Automatically scans uploaded files with the VirusTotal API and denies the request when a file is detected as malicious. | [bunkerweb-plugins/virustotal](https://github.com/bunkerity/bunkerweb-plugins/tree/main/virustotal) |
| **WebHook** | 1.3 | Send security notifications to a custom HTTP endpoint using a Webhook. | [bunkerweb-plugins/webhook](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) |
## How to use a plugin
@ -22,7 +22,7 @@ Here is the list of "official" plugins that we maintain (see the [bunkerweb-plug
If you want to quickly install external plugins, you can use the `EXTERNAL_PLUGIN_URLS` setting. It takes a list of URLs, separated with space, pointing to compressed (zip format) archive containing one or more plugin(s).
You can use the following value if you want to automatically install the official plugins : `EXTERNAL_PLUGIN_URLS=https://github.com/bunkerity/bunkerweb-plugins/archive/refs/tags/v1.2.zip`
You can use the following value if you want to automatically install the official plugins : `EXTERNAL_PLUGIN_URLS=https://github.com/bunkerity/bunkerweb-plugins/archive/refs/tags/v1.3.zip`
### Manual

View file

@ -459,10 +459,10 @@ The country security feature allows you to apply policy based on the country of
Here is the list of related settings :
| Setting | Default | Description |
| :-----------------: | :-----: | :------------------------------------------- |
| `BLACKLIST_COUNTRY` | | List of 2 letters country code to blacklist. |
| `WHITELIST_COUNTRY` | | List of 2 letters country code to whitelist. |
| Setting |Default| Context |Multiple| Description |
|-------------------|-------|---------|--------|--------------------------------------------------------------------------------------------------------------|
|`BLACKLIST_COUNTRY`| |multisite|no |Deny access if the country of the client is in the list (ISO 3166-1 alpha-2 format separated with spaces). |
|`WHITELIST_COUNTRY`| |multisite|no |Deny access if the country of the client is not in the list (ISO 3166-1 alpha-2 format separated with spaces).|
Using both country blacklist and whitelist at the same time makes no sense. If you do, please note that only the whitelist will be executed.

View file

@ -194,10 +194,10 @@ STREAM support :white_check_mark:
Deny access based on the country of the client IP.
| Setting |Default| Context |Multiple| Description |
|-------------------|-------|---------|--------|-----------------------------------------------------------------------------|
|`BLACKLIST_COUNTRY`| |multisite|no |Deny access if the country of the client is in the list (2 letters code). |
|`WHITELIST_COUNTRY`| |multisite|no |Deny access if the country of the client is not in the list (2 letters code).|
| Setting |Default| Context |Multiple| Description |
|-------------------|-------|---------|--------|--------------------------------------------------------------------------------------------------------------|
|`BLACKLIST_COUNTRY`| |multisite|no |Deny access if the country of the client is in the list (ISO 3166-1 alpha-2 format separated with spaces). |
|`WHITELIST_COUNTRY`| |multisite|no |Deny access if the country of the client is not in the list (ISO 3166-1 alpha-2 format separated with spaces).|
### Custom HTTPS certificate
@ -550,3 +550,4 @@ Allow access based on internal and external IP/network/rDNS/ASN whitelists.
|`WHITELIST_USER_AGENT_URLS`| |global |no |List of URLs, separated with spaces, containing good User-Agent to whitelist. |
|`WHITELIST_URI` | |multisite|no |List of URI (PCRE regex), separated with spaces, to whitelist. |
|`WHITELIST_URI_URLS` | |global |no |List of URLs, separated with spaces, containing bad URI to whitelist. |

View file

@ -288,3 +288,181 @@ If you have bots that need to access your website, the recommended way to avoid
## Timezone
When using container-based integrations, the timezone of the container may not match the one of the host machine. To resolve that, you can set the `TZ` environment variable to the timezone of your choice on your containers (e.g. `TZ=Europe/Paris`). You will find the list of timezone identifiers [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List).
## Web UI
In case you lost your UI credentials or have 2FA issues, you can connect to the database to retrieve access.
**Access database**
=== "SQLite"
=== "Linux"
Install SQLite (Debian/Ubuntu) :
```shell
sudo apt install sqlite3
```
Install SQLite (Fedora/RedHat) :
```shell
sudo dnf install sqlite
```
=== "Docker"
Get a shell into your scheduler container :
!!! note "Docker arguments"
- the `-u 0` option is to run the command as root (mandatory)
- the `-it` options are to run the command interactively (mandatory)
- `<bunkerweb_scheduler_container>` : the name or ID of your scheduler container
```shell
docker exec -u 0 -it <bunkerweb_scheduler_container> bash
```
Install SQLite :
```bash
apk add sqlite
```
Access your database :
!!! note "Database path"
We assume that you are using the default database path. If you are using a custom path, you will need to adapt the command.
```bash
sqlite3 /var/lib/bunkerweb/db.sqlite3
```
You should see something like this :
```text
SQLite version <VER> <DATE>
Enter ".help" for usage hints.
sqlite>
```
=== "MariaDB / MySQL"
!!! note "MariaDB / MySQL only"
The following steps are only valid for MariaDB / MySQL databases. If you are using another database, please refer to the documentation of your database.
!!! note "Credentials and database name"
You will need to use the same credentials and database named used in the `DATABASE_URI` setting.
=== "Linux"
Access your local database :
```bash
mysql -u <user> -p <database>
```
Then enter your password of the database user and you should be able to access your database.
=== "Docker"
Access your database container :
!!! note "Docker arguments"
- the `-u 0` option is to run the command as root (mandatory)
- the `-it` options are to run the command interactively (mandatory)
- `<bunkerweb_db_container>` : the name or ID of your database container
- `<user>` : the database user
- `<database>` : the database name
```shell
docker exec -u 0 -it <bunkerweb_db_container> mysql -u <user> -p <database>
```
Then enter your password of the database user and you should be able to access your database.
**Troubleshooting actions**
!!! info "Table schema"
The schema of the `bw_ui_users` table is the following :
```sql
id INTEGER PRIMARY KEY AUTOINCREMENT
username VARCHAR(256) NOT NULL UNIQUE
password VARCHAR(60) NOT NULL
is_two_factor_enabled BOOLEAN NOT NULL DEFAULT 0
secret_token VARCHAR(32) DEFAULT NULL
method ("manual" or "ui") NOT NULL DEFAULT 'manual'
```
=== "Retrieve username"
Execute the following command to extract data from the `bw_ui_users` table :
```sql
SELECT * FROM bw_ui_users;
```
You should see something like this :
```text
1|<username>|<password_hash>|1|<secret_totp_token>|(manual or ui)
```
=== "Update password"
You first need to hash the new password using the bcrypt algorithm.
Install the Python bcrypt library :
```shell
pip install bcrypt
```
Generate your hash (replace `mypassword` with your own password) :
```shell
python -c 'from bcrypt import hashpw, gensalt ; print(hashpw("mypassword".encode("utf-8"), gensalt(rounds=13)).decode())'
```
You can update your username / password executing this command :
```sql
UPDATE bw_ui_users SET username = <username>, password = <password_hash> WHERE id = 1;
```
If you check again your `bw_ui_users` table following this command :
```sql
SELECT * FROM bw_ui_users;
```
You should see something like this :
```text
1|<username>|<password_hash>|0||(manual or ui)
```
You should now be able to use the new credentials to log into the web UI.
=== "Disable 2FA authentication"
You can deactivate 2FA by executing this command :
```sql
UPDATE bw_ui_users SET is_two_factor_enabled = 0, secret_token = NULL WHERE id = 1;
```
If you check again your `bw_ui_users` table by following this command :
```sql
SELECT * FROM bw_ui_users;
```
You should see something like this :
```text
1|<username>|<password_hash>|0||(manual or ui)
```
You should now be able to log into the web UI only using your username and password.

View file

@ -727,6 +727,67 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th
systemctl restart bunkerweb-ui
```
## Account management
You can change the username, password needed and manage two-factor authentication by **accessing the account page** of the web UI from the menu.
You need to click on `manage account` inside the sidebar menu.
<figure markdown>
![Overview](assets/img/manage-account.webp){ align=center, width="350" }
<figcaption>Account page access from menu</figcaption>
</figure>
### Username / Password
!!! warning "Lost password/username"
In case you forgot your UI credentials, you can reset them from the CLI following [the steps described in the troubleshooting section](troubleshooting.md#web-ui).
You can update your username or password by filling the dedicated forms. For security reason, you need to enter your current password even if you are connected.
Please note that when your username or password is updated, you will be logout from the web UI to log in again.
<figure markdown>
![Overview](assets/img/profile-username-password.webp){ align=center, width="800" }
<figcaption>Username / Password forms</figcaption>
</figure>
### Two-Factor Authentication
!!! warning "Lost secret key"
In case you lost your secret key, you can disable 2FA from the CLI following [the steps described in the troubleshooting section](troubleshooting.md#web-ui).
You can power-up your login security by adding **Two-Factor Authentication (2FA)** to your account. By doing so, an extra code will be needed in addition to your password.
The web UI uses [Time based One Time Password (TOTP)](https://en.wikipedia.org/wiki/Time-based_one-time_password) as 2FA implementation : using a **secret key**, the algorithm will generate **one time passwords only valid for a short period of time**.
Any TOTP client such as Google Authenticator, Authy, FreeOTP, ... can be used to store the secret key and generate the codes. Please note that once TOTP is enabled, **you won't be able to retrieve it from the web UI**.
The following steps are needed to enable the TOTP feature from the web UI :
- Copy the secret key or use the QR code on your authenticator app
- Enter the current TOTP code in the 2FA input
- Enter your current password
!!! info "Secret key refresh"
A new secret key is **generated each time** you visit the page or submit the form. In case something went wrong (e.g. : expired TOTP code), you will need to copy the new secret key to your authenticator app until 2FA is successfuly enabled.
Once enabled, 2FA authentication can be disabled at the same place.
<figure markdown>
![Overview](assets/img/profile-totp.webp){ align=center, width="800" }
<figcaption>TOTP enable / disable forms</figcaption>
</figure>
After a successful login/password combination, you will be prompted to enter your TOTP code :
<figure markdown>
![Overview](assets/img/profile-2fa.webp){ align=center, width="400" }
<figcaption>Additional TOTP page</figcaption>
</figure>
## Advanced installation
=== "Docker"

View file

@ -1 +1 @@
ansible==8.6.1
ansible==9.1.0

View file

@ -1,16 +1,16 @@
#
# This file is autogenerated by pip-compile with Python 3.9
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --allow-unsafe --generate-hashes --strip-extras requirements-ansible.in
#
ansible==8.6.1 \
--hash=sha256:18b397580c1f05ce5de1fe238508dd81218d278667956d2f7709320176c3ed4a \
--hash=sha256:222735c32d2d2749f207e55ef740638bb97c7aaaa8b63bb7c7592d447da47584
ansible==9.1.0 \
--hash=sha256:5ad94991fb0e0e53a770a9ffcf1b68047f61b2282d948a7d2682ecd8fb8fa1bf \
--hash=sha256:bd88f16ca4b4dadfec78723f982c0f04e5481c6be497ccb43ea3b40fded39126
# via -r requirements-ansible.in
ansible-core==2.15.8 \
--hash=sha256:55e6f4350fb98ac5441620ba981b1d9f7b90aa5f320885965af996e149bd3caa \
--hash=sha256:8aa49cb1ddbf33d88c2bb4bf09ecd4b0dd8b788e174adca8b88dda6e6bdbf59b
ansible-core==2.16.2 \
--hash=sha256:494f002edcb17b02baef661ff27b8c9c750a534bdc0537ab29dc02e680817d92 \
--hash=sha256:e4ab559e7e525b1c6f99084fca873bb014775d5ecbe845b7c07b8e9d6c9c048b
# via ansible
cffi==1.16.0 \
--hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \
@ -91,10 +91,6 @@ cryptography==41.0.7 \
--hash=sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7 \
--hash=sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d
# via ansible-core
importlib-resources==5.0.7 \
--hash=sha256:2238159eb743bd85304a16e0536048b3e991c531d1cd51c4a834d1ccf2829057 \
--hash=sha256:4df460394562b4581bb4e4087ad9447bd433148fba44241754ec3152499f1d1b
# via ansible-core
jinja2==3.1.2 \
--hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
--hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61

View file

@ -47,8 +47,8 @@ markdown_extensions:
- pymdownx.tabbed:
alternate_style: true
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
extra:
version:

View file

@ -8,7 +8,7 @@
"BLACKLIST_COUNTRY": {
"context": "multisite",
"default": "",
"help": "Deny access if the country of the client is in the list (2 letters code).",
"help": "Deny access if the country of the client is in the list (ISO 3166-1 alpha-2 format separated with spaces).",
"id": "country-blacklist",
"label": "Country blacklist",
"regex": "^(?! )( *([A-Z]{2})(?!.*\\2) *)*$",
@ -17,7 +17,7 @@
"WHITELIST_COUNTRY": {
"context": "multisite",
"default": "",
"help": "Deny access if the country of the client is not in the list (2 letters code).",
"help": "Deny access if the country of the client is not in the list (ISO 3166-1 alpha-2 format separated with spaces).",
"id": "country-whitelist",
"label": "Country whitelist",
"regex": "^(?! )( *([A-Z]{2})(?!.*\\2) *)*$",

View file

@ -250,15 +250,18 @@ class Database:
"""Initialize the database"""
with self.__db_session() as session:
try:
session.add(
Metadata(
is_initialized=True,
first_config_saved=False,
scheduler_first_start=True,
version=version,
integration=integration,
if session.query(Metadata).get(1):
session.query(Metadata).filter_by(id=1).update({Metadata.version: version, Metadata.integration: integration})
else:
session.add(
Metadata(
is_initialized=True,
first_config_saved=False,
scheduler_first_start=True,
version=version,
integration=integration,
)
)
)
session.commit()
except BaseException:
return format_exc()
@ -332,19 +335,38 @@ class Database:
return ""
def init_tables(self, default_plugins: List[dict]) -> Tuple[bool, str]:
def init_tables(self, default_plugins: List[dict], bunkerweb_version: str) -> Tuple[bool, str]:
"""Initialize the database tables and return the result"""
inspector = inspect(self.__sql_engine)
if len(Base.metadata.tables.keys()) <= len(inspector.get_table_names()):
has_all_tables = True
db_version = None
has_all_tables = True
if inspector and len(inspector.get_table_names()):
db_version = self.get_metadata()["version"]
for table in Base.metadata.tables:
if not inspector.has_table(table):
has_all_tables = False
break
if db_version != bunkerweb_version:
self.__logger.warning(f"Database version ({db_version}) is different from Bunkerweb version ({bunkerweb_version}), checking if it needs to be updated")
for table in Base.metadata.tables:
if not inspector.has_table(table):
has_all_tables = False
else:
missing_columns = []
if has_all_tables:
return False, ""
db_columns = inspector.get_columns(table)
for column in Base.metadata.tables[table].columns:
if not any(db_column["name"] == column.name for db_column in db_columns):
missing_columns.append(column)
try:
with self.__db_session() as session:
if missing_columns:
for column in missing_columns:
session.execute(text(f"ALTER TABLE {table} ADD COLUMN {column.name} {column.type}"))
session.commit()
except BaseException:
return False, format_exc()
if has_all_tables and db_version and db_version == bunkerweb_version:
return False, ""
try:
Base.metadata.create_all(self.__sql_engine, checkfirst=True)

View file

@ -258,7 +258,7 @@ class Users(Base):
username = Column(String(256), nullable=False, unique=True)
password = Column(String(60), nullable=False)
is_two_factor_enabled = Column(Boolean, nullable=False, default=False)
secret_token = Column(String(32), nullable=True, unique=True)
secret_token = Column(String(32), nullable=True, unique=True, default=None)
method = Column(METHODS_ENUM, nullable=False, default="manual")

View file

@ -271,7 +271,10 @@ if __name__ == "__main__":
)
config_files = config.get_config()
if not db.is_initialized():
bunkerweb_version = Path(sep, "usr", "share", "bunkerweb", "VERSION").read_text().strip()
db_initialized = db.is_initialized()
if not db_initialized:
logger.info(
"Database not initialized, initializing ...",
)
@ -280,7 +283,8 @@ if __name__ == "__main__":
config.get_settings(),
config.get_plugins("core"),
config.get_plugins("external"),
]
],
bunkerweb_version,
)
# Initialize database tables
@ -295,19 +299,6 @@ if __name__ == "__main__":
)
else:
logger.info("Database tables initialized")
err = db.initialize_db(
version=Path(sep, "usr", "share", "bunkerweb", "VERSION").read_text().strip(),
integration=integration,
)
if err:
logger.error(
f"Can't Initialize database : {err}",
)
sys_exit(1)
else:
logger.info("Database initialized")
else:
logger.info("Database is already initialized, checking for changes ...")
@ -316,7 +307,8 @@ if __name__ == "__main__":
config.get_settings(),
config.get_plugins("core"),
config.get_plugins("external"),
]
],
bunkerweb_version,
)
if not ret and err:
@ -327,6 +319,14 @@ if __name__ == "__main__":
else:
logger.info("Database tables successfully updated")
err = db.initialize_db(version=bunkerweb_version, integration=integration)
if err:
logger.error(f"Can't {'initialize' if not db_initialized else 'update'} database metadata : {err}")
sys_exit(1)
else:
logger.info("Database metadata successfully " + ("initialized" if not db_initialized else "updated"))
if args.init:
sys_exit(0)

View file

@ -321,6 +321,12 @@ def handle_csrf_error(_):
def before_request():
if current_user.is_authenticated:
passed = True
# Go back from totp to login
if not session.get("totp_validated", False) and current_user.is_two_factor_enabled and "/totp" not in request.path and not request.path.startswith(("/css", "/images", "/js", "/json", "/webfonts")) and request.path.endswith("/login"):
return redirect(url_for("login", next=request.path))
# Case not login page, keep on 2FA before any other access
if not session.get("totp_validated", False) and current_user.is_two_factor_enabled and "/totp" not in request.path and not request.path.startswith(("/css", "/images", "/js", "/json", "/webfonts")):
return redirect(url_for("totp", next=request.form.get("next")))
elif session.get("ip") != request.remote_addr:
@ -333,7 +339,7 @@ def before_request():
session.clear()
@app.route("/")
@app.route("/", strict_slashes=False)
def index():
if app.config["USER"]:
if current_user.is_authenticated: # type: ignore
@ -531,25 +537,26 @@ def home():
services_scheduler_count=services_scheduler_count,
services_ui_count=services_ui_count,
services_autoconf_count=services_autoconf_count,
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@app.route("/profile", methods=["GET", "POST"])
@app.route("/account", methods=["GET", "POST"])
@login_required
def profile():
def account():
if request.method == "POST":
# Check form data validity
if not request.form:
flash("Missing form data.", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
elif "operation" not in request.form:
flash("Missing operation parameter.", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
if "curr_password" not in request.form or not current_user.check_password(request.form["curr_password"]):
flash(f"The current password is incorrect. ({request.form['operation']})", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
username = current_user.get_id()
password = request.form["curr_password"]
@ -559,10 +566,10 @@ def profile():
if request.form["operation"] == "username":
if "admin_username" not in request.form:
flash("Missing admin_username parameter. (username)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
elif len(request.form["admin_username"]) > 256:
flash("The admin username is too long. It must be less than 256 characters. (username)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
username = request.form["admin_username"]
@ -571,20 +578,20 @@ def profile():
elif request.form["operation"] == "password":
if "admin_password" not in request.form:
flash("Missing admin_password parameter. (password)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
elif request.form.get("admin_password"):
if not request.form.get("admin_password_check"):
flash("Missing admin_password_check parameter. (password)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
elif request.form["admin_password"] != request.form["admin_password_check"]:
flash("The passwords does not match. (password)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
elif not USER_PASSWORD_RX.match(request.form["admin_password"]):
flash("The admin password is not strong enough. It must contain at least 8 characters, including at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character (#@?!$%^&*-). (password)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
elif request.form.get("admin_password_check"):
flash("Missing admin_password parameter. (password)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
password = request.form["admin_password"]
@ -593,10 +600,10 @@ def profile():
elif request.form["operation"] == "totp":
if "totp_token" not in request.form:
flash("Missing totp_token parameter. (totp)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
elif not current_user.check_otp(request.form["totp_token"], secret=app.config["CURRENT_TOTP_TOKEN"]):
flash("The totp token is invalid. (totp)", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
session["totp_validated"] = not current_user.is_two_factor_enabled
is_two_factor_enabled = session["totp_validated"]
@ -604,20 +611,20 @@ def profile():
app.config["CURRENT_TOTP_TOKEN"] = None
else:
flash("Invalid operation parameter.", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
user = User(username, password, is_two_factor_enabled=is_two_factor_enabled, secret_token=secret_token, method=current_user.method)
ret = db.update_ui_user(username, user.password_hash, is_two_factor_enabled, secret_token, current_user.method if request.form["operation"] == "totp" else "ui")
if ret:
app.logger.error(f"Couldn't update the admin user in the database: {ret}")
flash(f"Couldn't update the admin user in the database: {ret}", "error")
return redirect(url_for("profile"))
return redirect(url_for("account"))
flash(
f"The {request.form['operation']} has been successfully updated." if request.form["operation"] != "totp" else f"The two-factor authentication was successfully {'disabled' if current_user.is_two_factor_enabled else 'enabled'}.",
)
return redirect(url_for("profile" if request.form["operation"] == "totp" else "login"))
return redirect(url_for("account" if request.form["operation"] == "totp" else "login"))
secret_token = ""
totp_qr_image = ""
@ -628,7 +635,7 @@ def profile():
totp_qr_image = get_b64encoded_qr_image(current_user.get_authentication_setup_uri())
app.config["CURRENT_TOTP_TOKEN"] = secret_token
return render_template("profile.html", username=current_user.get_id(), is_totp=current_user.is_two_factor_enabled, secret_token=secret_token, totp_qr_image=totp_qr_image, dark_mode=app.config["DARK_MODE"])
return render_template("account.html", username=current_user.get_id(), is_totp=current_user.is_two_factor_enabled, secret_token=secret_token, totp_qr_image=totp_qr_image, dark_mode=app.config["DARK_MODE"])
@app.route("/instances", methods=["GET", "POST"])
@ -674,6 +681,7 @@ def instances():
"instances.html",
title="Instances",
instances=instances,
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@ -796,6 +804,7 @@ def services():
}
for service in services
],
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@ -851,6 +860,7 @@ def global_config():
# Display global config
return render_template(
"global_config.html",
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@ -945,6 +955,7 @@ def configs():
services=app.config["CONFIG"].get_config(methods=False).get("SERVER_NAME", "").split(" "),
)
],
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@ -1193,6 +1204,7 @@ def plugins():
return template.render(
csrf_token=generate_csrf,
url_for=url_for,
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
**(plugin_args["args"] if plugin_args.get("plugin", None) == plugin_id else {}),
)
@ -1213,6 +1225,7 @@ def plugins():
plugins_internal=plugins_internal,
plugins_external=plugins_external,
plugins_errors=db.get_plugins_errors(),
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@ -1350,6 +1363,7 @@ def cache():
services=app.config["CONFIG"].get_config(methods=False).get("SERVER_NAME", "").split(" "),
)
],
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@ -1360,6 +1374,7 @@ def logs():
return render_template(
"logs.html",
instances=app.config["INSTANCES"].get_instances(),
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)
@ -1587,6 +1602,7 @@ def jobs():
"jobs.html",
jobs=db.get_jobs(),
jobs_errors=db.get_plugins_errors(),
username=current_user.get_id(),
dark_mode=app.config["DARK_MODE"],
)

View file

@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "ui",
"dependencies": {
"ace-builds": "^1.12.5",
"air-datepicker": "^3.3.1",

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
import { Tabs, Popover } from "./utils/settings.js";
class SubmitProfile {
class SubmitAccount {
constructor() {
this.pwEl = document.querySelector("#admin_password");
this.pwCheckEl = document.querySelector("#admin_password_check");
@ -45,7 +45,7 @@ class SubmitProfile {
"focus:valid:!ring-red-500",
"active:!border-red-500",
"active:valid:!border-red-500",
"valid:!border-red-500",
"valid:!border-red-500"
);
this.pwAlertEl.classList.add("opacity-0");
this.pwAlertEl.setAttribute("aria-hidden", "true");
@ -59,7 +59,7 @@ class SubmitProfile {
"focus:valid:!ring-red-500",
"active:!border-red-500",
"active:valid:!border-red-500",
"valid:!border-red-500",
"valid:!border-red-500"
);
this.pwAlertEl.classList.remove("opacity-0");
this.pwAlertEl.setAttribute("aria-hidden", "false");
@ -77,14 +77,14 @@ class PwBtn {
const passwordContainer = e.target.closest("[data-input-group]");
const inpEl = passwordContainer.querySelector("input");
const invBtn = passwordContainer.querySelector(
'[data-setting-password="invisible"]',
'[data-setting-password="invisible"]'
);
const visBtn = passwordContainer.querySelector(
'[data-setting-password="visible"]',
'[data-setting-password="visible"]'
);
inpEl.setAttribute(
"type",
inpEl.getAttribute("type") === "password" ? "text" : "password",
inpEl.getAttribute("type") === "password" ? "text" : "password"
);
if (inpEl.getAttribute("type") === "password") {
@ -129,7 +129,7 @@ class SwitchTabForm {
}
const setPWBtn = new PwBtn();
const setSubmit = new SubmitProfile();
const setSubmit = new SubmitAccount();
const setTabs = new Tabs();
const setPopover = new Popover();
const setSwitchTabForm = new SwitchTabForm();

23
src/ui/static/js/totp.js Normal file
View file

@ -0,0 +1,23 @@
class BackLogin {
constructor(currEndpoint, backEndpoint) {
this.init();
this.currEndpoint = currEndpoint;
this.backEndpoint = backEndpoint;
}
init() {
window.addEventListener("load", () => {
document.querySelectorAll("[data-back-login]").forEach((el) => {
el.setAttribute(
"href",
window.location.href.replace(
`/${this.currEndpoint}`,
`/${this.backEndpoint}`
)
);
});
});
}
}
const setBackLogin = new BackLogin("totp", "login");

View file

@ -926,7 +926,7 @@ module.exports = {
"5/6": "83.333333%",
full: "100%",
// sidenav: "calc(100vh - 310px)",
sidenav: "calc(100vh - 360px)", // for pro btn
sidenav: "calc(100vh - 450px)", // for pro btn
screen: "100vh",
min: "min-content",
max: "max-content",

View file

@ -177,7 +177,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
<form
class="col-span-12 grid grid-cols-12 w-full justify-items-center"
id="username-form"
action="profile"
action="account"
method="POST"
autocomplete="off"
>
@ -282,7 +282,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
data-plugin-item="password"
class="hidden col-span-12 grid grid-cols-12 w-full justify-items-center mt-4"
id="password-form"
action="profile"
action="account"
method="POST"
autocomplete="off"
>
@ -471,13 +471,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
</div>
<div class="col-span-12 flex justify-center">
<button
type="submit"
id="pw-button"
name="pw-button"
value="profile"
class="edit-btn"
>
<button type="submit" id="pw-button" name="pw-button" class="edit-btn">
Edit
</button>
</div>
@ -487,7 +481,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
data-plugin-item="totp"
class="hidden grid grid-cols-12 w-full justify-items-center"
id="totp-form"
action="profile"
action="account"
method="POST"
autocomplete="off"
>

View file

@ -2,7 +2,7 @@
id="banner"
tabindex="-1"
role="list"
class="relative flex justify-center z-50 gap-8 px-4 w-full h-[4rem] z-100"
class="relative flex justify-center gap-8 px-4 w-full h-[4rem] z-100 overflow-hidden"
>
<!-- background-->
<div

View file

@ -31,7 +31,7 @@
<!-- info -->
<main
class="xl:pl-75 w-full px-2 sm:px-6 pb-0 pt-20 sm:pt-6 min-h-[85vh] h-full flex flex-col justify-between"
class="xl:pl-75 w-full px-2 sm:px-6 pb-0 pt-20 sm:pt-6 min-h-[85vh] flex flex-col justify-between"
>
<div
class="max-w-[1920px] grid gap-y-4 gap-3 sm:gap-4 lg:gap-6 grid-cols-12 w-full"

View file

@ -22,7 +22,7 @@
href="https://www.bunkerweb.io/?utm_campaign=self&utm_source=ui"
class="hover:italic hover:brightness-90 block sm:px-4 pt-1 pb-0 lg:pb-1 text-xs tracking-wide font-normal transition duration-300 ease-in-out text-white dark:text-white"
target="_blank"
>Bunkerweb</a
>BunkerWeb</a
>
</li>
<li class="nav-item">

View file

@ -43,7 +43,7 @@
/>
{% elif current_endpoint == "jobs" %}
<script type="module" src="./js/jobs.js"></script>
{% elif current_endpoint == "profile" %}
<script type="module" src="./js/profile.js"></script>
{% elif current_endpoint == "account" %}
<script type="module" src="./js/account.js"></script>
{% endif %}
</head>

View file

@ -13,16 +13,14 @@
<h6 class="mb-0 text-lg font-bold text-white capitalize">
{{current_endpoint}}
</h6>
<ul
class="hidden xs:flex flex-col xs:flex-row flex-wrap pt-1 mr-12 bg-transparent rounded-lg sm:mr-16"
>
<ul class="flex flex-wrap pt-1 mr-12 bg-transparent rounded-lg sm:mr-16">
<li class="text-sm leading-normal">
<a class="text-white opacity-50 dark:opacity-75" href="javascript:;"
>BunkerWeb</a
>
</li>
<li
class="text-sm pl-0 xs:pl-2 capitalize leading-normal text-white before:float-left before:pr-2 before:text-white before:content-['/']"
class="hidden sm:inline text-sm pl-0 xs:pl-2 capitalize leading-normal text-white before:float-left before:pr-2 before:text-white before:content-['/']"
aria-current="page"
>
{{current_endpoint}}

View file

@ -30,9 +30,14 @@
id="sidebar-menu"
>
<!-- close btn-->
<button aria-controls="sidebar-menu" aria-expanded="false" aria-label="close menu sidebar" data-sidebar-menu-close>
<button
aria-controls="sidebar-menu"
aria-expanded="false"
aria-label="close menu sidebar"
data-sidebar-menu-close
>
<svg
class="xl:hidden fill-gray-600 dark:fill-gray-300 dark:opacity-80 absolute h-6 w-6 top-4 right-4"
class="xl:hidden fill-gray-600 dark:fill-gray-300 dark:opacity-80 absolute h-6 w-6 top-4 right-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
>
@ -49,7 +54,7 @@
<a
aria-label="link to home"
class="flex justify-center px-8 py-6 m-0 text-sm whitespace-nowrap dark:text-white text-slate-700"
href="{% if current_endpoint == 'home' %}javascript:void(0){% else %}loading?next={{ url_for('home') }}{% endif %}"
href="{% if current_endpoint == 'home' %}#{% else %}loading?next={{ url_for('home') }}{% endif %}"
>
<img
src="images/logo-menu-2.png"
@ -64,6 +69,19 @@
</a>
</div>
<div class="w-full px-1">
<h1
class="mb-0.5 tracking-normal text-primary text-center text-lg break-words whitespace-normal dark:text-gray-300"
>
{{ username }}
</h1>
<a
class="block underline mb-2 text-gray-600 dark:text-gray-400 text-sm text-center hover:brightness-90"
href="{% if current_endpoint == 'account' %}#{% else %}loading?next={{ url_for('account') }}{% endif %}"
>manage account
</a>
</div>
<hr
class="h-px mt-0 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent dark:bg-gradient-to-r dark:from-transparent dark:via-white dark:to-transparent"
/>
@ -79,7 +97,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'home' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} dark:text-white dark:opacity-80 py-1 ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap rounded-lg px-4 transition text-sm"
href="{% if current_endpoint == 'home' %}javascript:void(0){% else %}loading?next={{ url_for('home') }}{% endif %}"
href="{% if current_endpoint == 'home' %}#{% else %}loading?next={{ url_for('home') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -110,7 +128,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'instances' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'instances' %}javascript:void(0){% else %}loading?next={{ url_for('instances') }}{% endif %}"
href="{% if current_endpoint == 'instances' %}#{% else %}loading?next={{ url_for('instances') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -140,7 +158,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'global_config' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'global_config' %}javascript:void(0){% else %}loading?next={{ url_for('global_config') }}{% endif %}"
href="{% if current_endpoint == 'global_config' %}#{% else %}loading?next={{ url_for('global_config') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -170,7 +188,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'services' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'services' %}javascript:void(0){% else %}loading?next={{ url_for('services') }}{% endif %}"
href="{% if current_endpoint == 'services' %}#{% else %}loading?next={{ url_for('services') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -200,7 +218,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'configs' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'configs' %}javascript:void(0){% else %}loading?next={{ url_for('configs') }}{% endif %}"
href="{% if current_endpoint == 'configs' %}#{% else %}loading?next={{ url_for('configs') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -235,7 +253,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'plugins' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'plugins' %}javascript:void(0){% else %}loading?next={{ url_for('plugins') }}{% endif %}"
href="{% if current_endpoint == 'plugins' %}#{% else %}loading?next={{ url_for('plugins') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -265,7 +283,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'cache' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'cache' %}javascript:void(0){% else %}loading?next={{ url_for('cache') }}{% endif %}"
href="{% if current_endpoint == 'cache' %}#{% else %}loading?next={{ url_for('cache') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -295,7 +313,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'logs' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'logs' %}javascript:void(0){% else %}loading?next={{ url_for('logs') }}{% endif %}"
href="{% if current_endpoint == 'logs' %}#{% else %}loading?next={{ url_for('logs') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -325,7 +343,7 @@
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'jobs' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'jobs' %}javascript:void(0){% else %}loading?next={{ url_for('jobs') }}{% endif %}"
href="{% if current_endpoint == 'jobs' %}#{% else %}loading?next={{ url_for('jobs') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
@ -350,38 +368,6 @@
>
</a>
</li>
<!-- end item -->
<li class="mt-0.5 w-full">
<a
class="{% if current_endpoint == 'profile' %}font-semibold text-slate-700 dark:bg-primary/50 rounded-lg dark:hover:bg-primary/60 bg-primary/20 hover:bg-primary/30{% else %}dark:hover:bg-primary/20 hover:bg-primary/5 {% endif %} hover:rounded-lg dark:text-white dark:opacity-80 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{% if current_endpoint == 'profile' %}javascript:void(0){% else %}loading?next={{ url_for('profile') }}{% endif %}"
>
<div
class="mr-2 flex items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="stroke-teal-600 h-6 w-6 relative"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<span
class="ml-1 duration-300 opacity-100 pointer-events-none ease"
>
Profile
</span>
</a>
</li>
<!-- end item -->
</ul>
<!-- end default anchor -->
@ -482,7 +468,7 @@
<!-- end dark/light mode -->
<!-- social-->
<ul class="flex justify-center align-middle w-full mb-4">
<ul class="mb-3 flex justify-center align-middle w-full">
<li class="mx-2 w-6">
<a
aria-label="link to twitter"

View file

@ -5,7 +5,7 @@
>
<div
data-plugins-modal-card
class="min-w-[500px ]overflow-y-auto mx-3 ml-2 mr-6 sm:mx-6 lg:mx-8 my-3 px-4 pt-4 pb-8 w-full max-w-[400px] 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-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"
>
<div class="w-full flex justify-between mb-2">
<p

View file

@ -15,7 +15,7 @@
<!-- end actions -->
<!-- services container-->
<div
class="gap-8 p-4 grid grid-cols-12 col-span-12 relative min-w-0 break-words rounded-2xl bg-clip-border"
class="p-0 my-4 sm:mx-4 md:px-8 grid grid-cols-12 col-span-12 relative min-w-0 break-words rounded-2xl bg-clip-border"
>
{% if services|length == 0 %}
<div class="col-span-12 sm:col-span-4 sm:col-start-5">

View file

@ -6,7 +6,7 @@
>
<div
data-services-modal-card
class="overflow-y-auto mx-3 ml-2 mr-6 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-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"
>
<div class="w-full flex justify-between mb-2">
<p
@ -33,7 +33,7 @@
</div>
<div
data-services-tabs-header
class="flex justify-start items-center gap-x-4 gap-y-2 my-3"
class="flex flex-col sm:flex-row justify-start items-start sm:items-center gap-x-4 gap-y-2 my-3"
>
<h5
class="transition duration-300 ease-in-out dark:opacity-90 ml-2 font-bold text-md uppercase dark:text-white mb-0"
@ -41,9 +41,7 @@
CONFIGS
</h5>
<!-- search inpt-->
<div
class="flex relative col-span-12 sm:col-span-6 lg:col-span-4 3xl:col-span-3"
>
<div class="flex relative">
<label class="sr-only" for="settings-filter">search</label>
<input
type="text"

View file

@ -10,6 +10,7 @@
<link href="images/favicon.ico" rel="icon" type="image/x-icon" />
<link rel="stylesheet" href="css/dashboard.css" />
<link rel="stylesheet" href="css/login.css" />
<script defer src="./js/totp.js"></script>
</head>
<body>
<div
@ -31,7 +32,7 @@
role="alert"
aria-description="login message alert"
data-flash-message
class="p-4 mb-1 md:mb-3 md:mr-3 z-[1001] flex flex-col fixed bottom-0 right-0 w-full md:w-1/2 max-w-[300px] min-h-20 bg-white rounded-lg dark:brightness-110 hover:scale-102 transition shadow-md break-words dark:bg-slate-850 dark:shadow-dark-xl bg-clip-border"
class="p-4 mb-1 md:mb-3 md:mr-3 z-[1001] flex flex-col fixed bottom-0 right-0 w-full md:w-1/2 max-w-[300px] min-h-20 bg-white rounded-lg hover:scale-102 transition shadow-md break-words bg-clip-border"
>
<button
data-close-flash-message
@ -39,7 +40,7 @@
class="absolute right-7 top-1.5"
>
<svg
class="cursor-pointer fill-gray-600 dark:fill-gray-300 dark:opacity-80 absolute h-5 w-5"
class="cursor-pointer fill-gray-600 absolute h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
>
@ -50,12 +51,12 @@
</button>
{% if category == 'error' or (message|safe).startswith("Please log in") %}
<h5 class="text-lg mb-0 text-red-500">Error</h5>
<p class="text-gray-700 dark:text-gray-300 mb-0 text-sm">
<p class="text-gray-700 mb-0 text-sm">
{{ message|safe }}
</p>
{% else %}
<h5 class="text-lg mb-0 text-green-500">Success</h5>
<p class="text-gray-700 dark:text-gray-300 mb-0 text-sm">
<p class="text-gray-700 mb-0 text-sm">
{{ message|safe }}
</p>
{% endif %}
@ -71,9 +72,15 @@
class="mx-4 col-span-2 bg-none h-full flex flex-col items-center justify-center"
>
<div
class="bg-gray-50 rounded px-8 sm:px-12 py-16 w-full max-w-[400px]"
class="bg-gray-50 rounded pb-16 w-full max-w-[400px]"
>
<div class="flex justify-center">
<a data-back-login class="hover:brightness-75 block text-gray-700 text-sm mx-2 mt-1 flex justify-start items-center" href="/login">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 stroke-gray-700 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
</svg>
<span>back to login</span></a>
<div class="mt-12 flex justify-center">
<img
class="w-full max-w-60 max-h-30 mb-6"
src="images/BUNKERWEB-print-hd.png"
@ -81,10 +88,10 @@
class="logo"
/>
</div>
<h1 class="hidden text-center font-bold dark:text-white mb-8">
Log in
<h1 class="hidden text-center font-bold mb-8">
2FA
</h1>
<form action="totp" method="POST" autocomplete="off">
<form class="px-8 sm:px-12" action="totp" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input
type="hidden"
@ -94,7 +101,7 @@
<!-- totp -->
<div class="flex flex-col relative col-span-12 my-3">
<h5
class="text-center my-1 transition duration-300 ease-in-out dark:opacity-90 text-md font-bold m-0 dark:text-gray-300"
class="text-center my-1 transition duration-300 ease-in-out text-md font-bold m-0 "
>
2FA
</h5>
@ -103,7 +110,7 @@
type="text"
id="totp_token"
name="totp_token"
class="col-span-12 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-4 py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
class="col-span-12 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-4 py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
placeholder="enter totp"
pattern="(.*?)"
required
@ -116,7 +123,7 @@
id="login"
name="login"
value="login"
class="my-4 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-primary hover:bg-primary/80 focus:bg-primary/80 leading-normal text-sm ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
class="my-4 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-primary hover:bg-primary/80 focus:bg-primary/80 leading-normal text-sm ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
>
Log in
</button>

View file

@ -20,16 +20,17 @@
name: docker[tls]
state: forcereinstall
executable: pip3
version: "6.1.3"
- name: Pin version for urllib
pip:
name: urllib3<2
state: forcereinstall
executable: pip3
extra_args:
# - name: Pin version for urllib
# pip:
# name: urllib3<2
# state: forcereinstall
# executable: pip3
# extra_args:
- name: Init Docker Swarm
community.general.docker_swarm:
community.docker.docker_swarm:
advertise_addr: "{{ local_ip }}"
listen_addr: "{{ local_ip }}"
ssl_version: "1.3"
@ -47,7 +48,7 @@
join_token_worker: "{{ hostvars[groups['managers'][0]].result.swarm_facts.JoinTokens.Worker }}"
- name: Join Swarm as managers
community.general.docker_swarm:
community.docker.docker_swarm:
advertise_addr: "{{ local_ip }}"
listen_addr: "{{ local_ip }}"
ssl_version: "1.3"
@ -60,7 +61,7 @@
- inventory_hostname != groups['managers'][0]
- name: Join Swarm as workers
community.general.docker_swarm:
community.docker.docker_swarm:
advertise_addr: "{{ local_ip }}"
listen_addr: "{{ local_ip }}"
ssl_version: 1.3

View file

@ -299,7 +299,7 @@ with driver_func() as driver:
access_page(
driver,
driver_wait,
"/html/body/aside[1]/div[1]/div[2]/ul/li[2]/a",
"/html/body/aside[1]/div[1]/div[3]/ul/li[2]/a",
"instances",
)
@ -369,7 +369,7 @@ with driver_func() as driver:
access_page(
driver,
driver_wait,
"/html/body/aside[1]/div[1]/div[2]/ul/li[3]/a",
"/html/body/aside[1]/div[1]/div[3]/ul/li[3]/a",
"global config",
)
@ -529,7 +529,7 @@ with driver_func() as driver:
access_page(
driver,
driver_wait,
"/html/body/aside[1]/div[1]/div[2]/ul/li[4]/a",
"/html/body/aside[1]/div[1]/div[3]/ul/li[4]/a",
"services",
)
@ -891,7 +891,7 @@ with driver_func() as driver:
access_page(
driver,
driver_wait,
"/html/body/aside[1]/div[1]/div[2]/ul/li[5]/a",
"/html/body/aside[1]/div[1]/div[3]/ul/li[5]/a",
"configs",
)
@ -1020,7 +1020,7 @@ location /hello {
access_page(
driver,
driver_wait,
"/html/body/aside[1]/div[1]/div[2]/ul/li[6]/a",
"/html/body/aside[1]/div[1]/div[3]/ul/li[6]/a",
"plugins",
)
@ -1163,7 +1163,7 @@ location /hello {
print("The plugin has been deleted, trying cache page ...", flush=True)
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/ul/li[7]/a", "cache")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[7]/a", "cache")
### CACHE PAGE
@ -1188,7 +1188,7 @@ location /hello {
print("The cache file content is correct, trying logs page ...", flush=True)
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/ul/li[8]/a", "logs")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[8]/a", "logs")
### LOGS PAGE
@ -1310,7 +1310,7 @@ location /hello {
print("Date filter is working, trying jobs page ...", flush=True)
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/ul/li[9]/a", "jobs")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[3]/ul/li[9]/a", "jobs")
### JOBS PAGE
@ -1441,11 +1441,11 @@ location /hello {
print("The cache download is not working, exiting ...", flush=True)
exit(1)
print("Cache download is working, trying profile page ...", flush=True)
print("Cache download is working, trying account page ...", flush=True)
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/ul/li[10]/a", "profile")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/a", "account")
### PROFILE PAGE
### ACCOUNT PAGE
username_input = safe_get_element(driver, By.ID, "admin_username")
@ -1485,7 +1485,7 @@ location /hello {
access_page(driver, driver_wait, "//button[@value='login']", "home")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/ul/li[10]/a", "profile")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/a", "account")
username_input = safe_get_element(driver, By.ID, "admin_username")
@ -1542,7 +1542,7 @@ location /hello {
access_page(driver, driver_wait, "//button[@value='login']", "home")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/ul/li[10]/a", "profile")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/a", "account")
print("Successfully logged in with new password, trying 2FA ...", flush=True)
@ -1578,7 +1578,7 @@ location /hello {
password_input.send_keys("P@ssw0rd")
access_page(driver, driver_wait, "//button[@id='totp-button' and @class='valid-btn']", "profile")
access_page(driver, driver_wait, "//button[@id='totp-button' and @class='valid-btn']", "account")
assert_button_click(driver, "//button[@data-tab-handler='totp']")
@ -1642,7 +1642,7 @@ location /hello {
print("Successfully logged in with 2FA, trying to deactivate 2FA ...", flush=True)
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/ul/li[10]/a", "profile")
access_page(driver, driver_wait, "/html/body/aside[1]/div[1]/div[2]/a", "account")
assert_button_click(driver, "//button[@data-tab-handler='totp']")
@ -1656,7 +1656,7 @@ location /hello {
driver,
driver_wait,
"//button[@id='totp-button' and @class='delete-btn']",
"profile",
"account",
)
assert_button_click(driver, "//button[@data-tab-handler='totp']")