Merge remote-tracking branch 'origin/dev' into ui
2
.github/workflows/staging-create-infra.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
26
README.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 81 KiB |
BIN
docs/assets/img/manage-account.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 120 KiB |
BIN
docs/assets/img/profile-2fa.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/assets/img/profile-totp.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/assets/img/profile-username-password.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ align=center, width="400" }
|
||||
<figcaption>Additional TOTP page</figcaption>
|
||||
</figure>
|
||||
|
||||
## Advanced installation
|
||||
|
||||
=== "Docker"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ansible==8.6.1
|
||||
ansible==9.1.0
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) *)*$",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
)
|
||||
|
||||
|
|
|
|||
1
src/ui/package-lock.json
generated
|
|
@ -4,7 +4,6 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ui",
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.12.5",
|
||||
"air-datepicker": "^3.3.1",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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");
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
2
src/ui/templates/banner.html
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
src/ui/templates/base.html
vendored
|
|
@ -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"
|
||||
|
|
|
|||
2
src/ui/templates/footer.html
vendored
|
|
@ -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">
|
||||
|
|
|
|||
4
src/ui/templates/head.html
vendored
|
|
@ -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>
|
||||
|
|
|
|||
6
src/ui/templates/header.html
vendored
|
|
@ -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}}
|
||||
|
|
|
|||
76
src/ui/templates/menu.html
vendored
|
|
@ -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"
|
||||
|
|
|
|||
2
src/ui/templates/plugins_modal.html
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
src/ui/templates/services.html
vendored
|
|
@ -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">
|
||||
|
|
|
|||
8
src/ui/templates/services_modal.html
vendored
|
|
@ -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"
|
||||
|
|
|
|||
31
src/ui/templates/totp.html
vendored
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']")
|
||||
|
|
|
|||